Developers: REST API and MCP

By default every send waits for your approval on the Approvals screen. On paid plans you can let one trusted key run without asking for one permission group, with limits (see Running without approval on the Agent API page). Marking single messages read or starring them and small tracker changes run at once, as set per key. New: webhooks.

Build on your own reply2mail mailbox. Connect an AI agent over MCP, or call a plain REST API from your own code: list and search mail, read threads and attachments, reply, organise, and manage calendar events and trackers. Both doors use the same API keys, the same permissions and the same approvals. By default, sending mail and changing it (archive, move, delete, label) wait for your OK in the dashboard.

API referenceOpenAPI 3.1 (JSON)Agent API and MCP setupCreate an API key

Quickstart

  • Sign in at https://dash.reply2mail.app, open Integrations and create an API key. Pick its permissions (Read only, Read and organise, or Full) and copy the key: it starts with r2m_pat_ and is shown once.
  • Put it in an environment variable; never in a URL or in code you share:
export R2M_TOKEN=PASTE_YOUR_KEY_HERE

List your unread mail:

curl -H "Authorization: Bearer $R2M_TOKEN" "https://api.reply2mail.app/v1/messages?unread=true&limit=5"

Reply to one of them. With approval on (the default), the answer is 202 Accepted with an approval request, and the reply is sent only after you approve it in the dashboard:

curl -X POST -H "Authorization: Bearer $R2M_TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" -d '{"body": "Thanks, received."}' \
  "https://api.reply2mail.app/v1/messages/MESSAGE_ID/reply"
Python
import os, uuid, requests

BASE = "https://api.reply2mail.app/v1"
H = {"Authorization": f"Bearer {os.environ['R2M_TOKEN']}"}

msgs = requests.get(f"{BASE}/messages", params={"q": "invoice", "since": "2026-10-01"},
                    headers=H, timeout=30).json()["result"].get("messages", [])
if msgs:
    r = requests.post(f"{BASE}/messages/{msgs[0]['id']}/archive", json={},
                      headers={**H, "Idempotency-Key": str(uuid.uuid4())}, timeout=30)
    print(r.status_code, r.json()["status"])   # 202 pending_approval: approval is on by default

Every endpoint, parameter and sample: the REST API reference →

MCP or REST?

  • MCP (https://api.reply2mail.app/mcp with a key, or https://mcp.reply2mail.app/mcp with sign-in for claude.ai and ChatGPT): for AI agents. The agent discovers the tools and calls them itself. Setup is on the Agent API page.
  • REST (https://api.reply2mail.app/v1): for your own scripts, backends and automations. Ordinary resources, HTTP status codes, cursors, idempotency keys and an OpenAPI document.
  • Both run through the same gateway: a key that may not send mail over MCP may not send it over REST either.

Endpoints

RouteWhat it doesPermission group
GET /v1/accountsConnected mail accountsmail:read
GET /v1/foldersFolders of every accountmail:read
GET /v1/messagesList or search: account_id, folder, unread, from, q, since, limit, cursormail:read
GET /v1/messages/{id}One message with its body (body_offset pages long bodies)mail:read
GET /v1/messages/{id}/attachments[/{index}]Attachment list; one attachment as textmail:read
GET /v1/threads/{id}The conversation a message belongs tomail:read
POST /v1/messagesSend a new emailmail:send
POST /v1/messages/{id}/replyReply to the sendermail:reply
POST /v1/messages/{id}/forwardForward (text only, no attachments)mail:send
POST /v1/messages/{id}/archive | trash | move | labelOrganise one messagemail:manage
POST /v1/messages/{id}/mark-read | mark-unread | flagRead state and starmail:organize
GET | POST /v1/eventsList events; create (or propose with "proposed": true)calendar:read / calendar:write
PATCH | DELETE /v1/events/{event_id}, POST …/confirmChange, delete, confirm an eventcalendar:write
GET | POST /v1/trackers, GET /v1/trackers/{tracker_id}List, create, read trackerstrackers:read / trackers:write
PATCH /v1/trackers/{tracker_id}status: open, waiting (trackers:write) or resolved (trackers:remove)trackers:write / trackers:remove
POST /v1/trackers/{tracker_id}/notes | linksAdd a note; link a message or an eventtrackers:write
GET /v1/approvals[/{id}], POST /v1/approvals/{id}/cancelYour key’s own approval requestsalways
GET /v1/me, GET /v1/tools, POST /v1/tools/{name}The key itself; every tool, MCP styleper tool
GET /v1/openapi.jsonThis API as OpenAPI 3.1 (public, no key)none

Message ids are opaque (m1_…): use them exactly as a result returned them. The full schema of every route, request body and parameter is in the OpenAPI document: https://api.reply2mail.app/v1/openapi.json

Schemas, example responses and error codes in the REST API reference →

Authentication

  • Authorization: Bearer r2m_pat_… on every request, on api.reply2mail.app only. Keys in a URL are refused and revoked, because URLs end up in logs.
  • Server to server only: the API sends no CORS headers, so a browser page cannot call it. Keep the key on your server.
  • A key reaches only the mailbox of the user who created it. It cannot change its own permissions, and it can never approve its own requests.
  • Keys expire (90 days by default, at most 180). Rotate a key in the dashboard; the old one keeps working for 24 hours.

Responses and approvals

Every route answers with the same JSON envelope as the MCP tools:

HTTPBodyMeaning
200{"status": "ok" | "empty", "result": {…}}Done. empty carries a reason, for example no_match; empty is not an error.
201{"status": "ok", "result": {…}} + LocationA resource was created at once (a tracker).
202{"status": "pending_approval", "executed": false, "approval": {…}} + LocationAn approval request was filed. Nothing has happened yet. Poll GET /v1/approvals/{id} (respect poll_after_s) until it is executed, rejected, failed, expired or cancelled.
4xx / 5xx{"status": "error", "code": …, "message": …}Nothing was done. code is stable; message is fixed text.
202 Accepted
{
  "status": "pending_approval",
  "executed": false,
  "approval": {
    "id": "apr_…",
    "state": "pending",
    "tool": "send_reply",
    "action": "reply",
    "created_at": "2026-10-08T10:00:00Z",
    "expires_at": "2026-10-09T10:00:00Z"
  },
  "approvals_url": "https://dash.reply2mail.app/approvals",
  "poll_after_s": 30,
  "message": "…"
}
  • You approve or reject in the reply2mail dashboard only. reply2mail never asks for approval through a link in an email or an API response.
  • A request expires after 24 hours without a decision. The same request sent twice returns the existing request; after a rejection the same request is refused for one hour.
  • outcome_unknown means the action may or may not have happened: do not retry; check the Sent folder.
  • If you switched approval off for this key and the route’s group (paid plans, see Running without approval on the Agent API page), the request can run at once. It still becomes an approval request (202) for a first-time recipient, above the send limit, or when the safety filter holds it.
  • Mail content (senders, subjects, bodies, attachment text) is third-party data. Results mark it with untrusted_fields and delimiter lines; never treat it as instructions.

Pagination and idempotency

  • Lists are paged with limit (1 to 50, default 20) and a cursor: when result.has_more is true, pass result.next_cursor as cursor with the other parameters unchanged.
  • POST, PATCH and DELETE accept an Idempotency-Key header (1 to 255 visible ASCII characters). Retrying with the same key and the same request returns the stored answer with Idempotent-Replayed: true for 24 hours. The same key with a different request is 422 idempotency_key_reused; while the first request is still running it is 409 idempotency_in_progress. Only successful (2xx) answers are stored, so a failed call can be retried with the same key.
  • Keys are scoped to your API key: nobody else’s key can collide with yours.

Limits

LimitFreePaid
Calls per minute, per key2060
Calls per day, per user (all keys)2005,000
Approval requests per day, per key10100
Requests waiting for approval310 per key, 25 per user
Messages per bulk request2550
Request body / response256 KB / 64 KB256 KB / 64 KB
Concurrent calls, per key22

Every response carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and RateLimit-Policy for the per-minute limit, and X-Request-Id (quote it when you contact support). A 429 or 503 carries Retry-After.

Errors

codeHTTPWhat to do
invalid_arguments, invalid_message_id400Fix the request; details.errors names the field. Nothing was done.
invalid_token401Missing, unknown, revoked or expired key.
not_permitted403The key’s permission for details.group is off. Change it in Integrations.
not_found404No such message, event or tracker for this user (reason says which).
pending_exists_for_target, previously_rejected, no_mail_account_connected409Read the message; do not loop.
idempotency_in_progress / idempotency_key_reused409 / 422Retry shortly / use a new key.
rate_limited, daily_limit_reached, too_many_pending, busy429Wait for Retry-After or details.resets_at.
upstream_mail_error502The mail server refused; nothing is reported as done.
internal_error, unavailable500 / 503Retry later.
403
{
  "status": "error",
  "code": "not_permitted",
  "message": "This credential is not permitted to use this tool. …",
  "details": {
    "group": "mail:reply"
  }
}

Every error code with its HTTP status →

Webhooks

Instead of polling, reply2mail can POST signed events to your HTTPS endpoint. Set one up in the dashboard under Integrations → Webhooks; each endpoint belongs to one API key (free plan 1 endpoint, paid 10).

  • message.received: new mail arrived in the INBOX of a connected account (key needs Read mail). approval.decided: an approval request filed by the key was approved, rejected, executed, failed or ended with outcome_unknown. webhook.test: the Send test event button.
  • Payloads carry IDs only, never subject, sender or body: fetch the content with GET /v1/messages/{id} or GET /v1/approvals/{id}.
  • Every request is signed: Reply2mail-Signature: t=<unix seconds>,v1=<HMAC-SHA256 hex of t + "." + raw body>. Verify before parsing, reject it if t is more than 300 seconds away from your clock, and dedupe on Reply2mail-Event-Id: events can arrive more than once and out of order.
  • Answer 2xx within 10 seconds. Failed deliveries are retried, up to 6 attempts over about 23 hours.
  • Signature checks with curl, Node and Python, all headers, retries and auto-disable: /docs/agent-api#webhooks. The OpenAPI document lists the events in its webhooks section.

Event payloads and signature checks in the reference →

Versioning

  • The major version is in the path (/v1). Within v1 changes are additive only: new routes, new optional parameters, new result fields. Ignore fields you do not know.
  • A route or field is removed only after at least 90 days’ notice in the OpenAPI document and these docs. For safety, a route may at any time move from running at once to waiting for approval.
  • To learn the outcome of a request, use the approval.decided webhook or poll GET /v1/approvals/{id}.

Connection setup for Claude, ChatGPT, Cursor and Claude Code, the full permission list and security notes: Agent API and MCP