Agent API and MCP server

By default every send waits for your approval. On paid plans you can let one trusted key or app run without asking, within limits: Running without approval. New: Webhooks.

reply2mail can give an outside AI product (Claude, ChatGPT, Cursor or any MCP or REST client) access to your reply2mail mail, calendar and trackers. The product calls reply2mail tools over the Model Context Protocol (MCP) or a plain REST API. You decide which products connect and what they may do, and you can revoke them at any time under Integrations in the reply2mail dashboard. If Integrations does not appear in your dashboard yet, it is not enabled for your account.

1. Connect Claude or ChatGPT (sign in, nothing to paste)

For claude.ai, the Claude apps and ChatGPT, add reply2mail as a custom connector and give it this URL:

https://mcp.reply2mail.app/mcp

The product sends you to reply2mail to sign in. You then see a consent screen in the reply2mail dashboard that lists the permissions the product asks for; untick what you do not want to give and choose Allow. No key is copied anywhere. The connection then appears under Integrations, where you can change its permissions or disconnect it.

2. Connect with an API key (Claude Code, Cursor, any MCP or REST client)

In the dashboard, open Integrations and create an API key. Choose its permissions and how long it is valid. The key starts with r2m_pat_ and is shown once: copy it then, because reply2mail stores only a one-way hash and cannot show it again.

Put the key in an environment variable. The config snippets below only refer to the variable, so a config file never contains the key itself:

macOS / Linux
export R2M_TOKEN=PASTE_YOUR_KEY_HERE
Windows PowerShell
$env:R2M_TOKEN = 'PASTE_YOUR_KEY_HERE'

API keys use their own host:

https://api.reply2mail.app/mcp

Claude Code, one command (run it in the terminal where R2M_TOKEN is set):

claude mcp add --transport http reply2mail https://api.reply2mail.app/mcp --header "Authorization: Bearer $R2M_TOKEN"

Claude Code, project file .mcp.json (the variable is read when Claude Code starts):

.mcp.json
{
  "mcpServers": {
    "reply2mail": {
      "type": "http",
      "url": "https://api.reply2mail.app/mcp",
      "headers": {
        "Authorization": "Bearer ${R2M_TOKEN}"
      }
    }
  }
}

Cursor, ~/.cursor/mcp.json (or .cursor/mcp.json in a project):

mcp.json
{
  "mcpServers": {
    "reply2mail": {
      "url": "https://api.reply2mail.app/mcp",
      "headers": {
        "Authorization": "Bearer ${env:R2M_TOKEN}"
      }
    }
  }
}

Any other MCP client: use the URL above with the HTTP header Authorization: Bearer <your key>. No bridge or proxy is needed.

3. REST API

API keys also work with the REST API at https://api.reply2mail.app/v1. REST and MCP return the same result object.

  • GET /v1/me: what this key is and what it may do.
  • GET /v1/tools: the tools this key can call (the same list MCP clients see).
  • POST /v1/tools/<name>: call one tool. The body is {"arguments": {...}}, or empty for a tool without arguments.
  • GET /v1/openapi.json: the whole API as a public OpenAPI 3.1 document (no key needed).
  • Resources for your own code: /v1/messages, /v1/threads, /v1/events, /v1/trackers, /v1/accounts and more, with the same permissions and approvals. See the Developers page (/developers).
curl -H "Authorization: Bearer $R2M_TOKEN" https://api.reply2mail.app/v1/me
curl -H "Authorization: Bearer $R2M_TOKEN" https://api.reply2mail.app/v1/tools
curl -X POST -H "Authorization: Bearer $R2M_TOKEN" -H "Content-Type: application/json" \
  -d '{"arguments": {"query": "invoice", "newer_than_days": 30}}' \
  https://api.reply2mail.app/v1/tools/search_inbox

Within /v1, changes are additive only: new tools, new optional parameters, new result fields. Clients should ignore result fields they do not know.

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

Permissions

Each key or connection has its own permissions, grouped as follows. You can change them at any time under Integrations.

GroupWhat it allowsHow it runs
mail:readSearch, list and count mail; read a message, thread or attachment text; folders; accounts; reply and email previews (nothing is stored or sent); daily briefRead only
calendar:readList events, find conflicts, list proposed eventsRead only
trackers:readList trackers and read oneRead only
mail:organizeMark a message read or unread, star itRuns immediately, one message at a time (default); can be set to ask first
mail:replySend a reply to a messageWaits for your approval (default); on paid plans can run without approval for one key or app
mail:sendSend a new email, forward an emailWaits for your approval (default); on paid plans can run without approval for one key or app
mail:manageArchive, move to Trash, move or label one messageWaits for your approval (default); on paid plans can run without approval for one key or app
mail:bulkArchive, move to Trash, label or mark read several messages; mark all unread mail read; restore recently trashed mailWaits for your approval (default); on paid plans can run without approval for one key or app
mail:foldersCreate or delete a folderWaits for your approval (default); on paid plans can run without approval for one key or app
calendar:writeCreate, propose, update, confirm and delete events; update or delete several eventsWaits for your approval (default); on paid plans can run without approval for one key or app
trackers:writeCreate a tracker, link messages or events to it, add a note, set it to open or waitingRuns immediately (default); can be set to ask first
trackers:removeResolve or delete trackers, one or severalWaits for your approval (default); on paid plans can run without approval for one key or app
memory:readRead what reply2mail remembersOff by default; read only when turned on
memory:writeAdd, edit or remove memory itemsOff by default; when turned on, waits for your approval (can run without approval for one key or app on paid plans)
rules:readList your rulesOff by default; read only when turned on
rules:writeAdd, turn on, turn off, remove or replace rulesOff by default; when turned on, waits for your approval (can run without approval for one key or app on paid plans)
profile:readRead your persona, signature, brand voice, language and reply templatesOff by default; read only when turned on
profile:writeChange your persona, signature, brand voice or language; add a reply templateOff by default; when turned on, waits for your approval (can run without approval for one key or app on paid plans)
account:readPlan and credits, usage, setup status, what reply2mail’s own assistant didOff by default; read only when turned on

With approval on, which is the default, nothing sends or changes mail without your approval of that specific request. If you switch approval off for a key or app, see Running without approval below. Bulk selections are turned into an explicit list of messages when the request is made: at most 50 messages, 25 on the free plan. Deleting a folder shows how many messages it holds; Inbox, Sent, Drafts, Trash, Spam and All Mail cannot be deleted.

Memory, rules and profile changes are standing changes: they keep shaping later mail. Their approval card shows the text before and after the change and points out links, email addresses and order-like wording. reply2mail’s own assistant treats text written through an agent as data, never as your instructions.

Presets when you create a key or approve a connection:

  • Read only: mail:read, calendar:read, trackers:read.
  • Read and organise: Read only plus mail:organize.
  • Full: every group that is not off by default. Groups that need approval still ask first; trackers:write runs immediately.

The memory, rules, profile and account groups are in no preset. Turn them on for each key or app separately.

Approvals

  • An action that needs approval is never reported as sent or done until you approve it. The product receives a "pending approval" result instead.
  • You approve or reject in the reply2mail dashboard (Approvals page), nowhere else.
  • reply2mail never asks for an approval through a link or button sent by an agent or contained in an email. Treat any such link as suspicious.
  • A request expires after 24 hours if you do not decide.
  • Sending the same request again returns the existing request instead of a new one. After you reject a request, the same request is refused for one hour.
  • If the target changed between the request and your approval (the message was moved, the event or rule was edited, the signature was changed), nothing runs and the request is reported as failed.
  • The product checks the result with get_approval (MCP) or GET /v1/approvals/<id> (REST). A result of outcome_unknown means the action may or may not have happened; it must not be retried.

Running without approval (direct mode)

Approval is on by default for every group that sends or changes mail, calendar events, trackers, memory, rules or your profile. On a paid plan you can switch approval off ("direct") for one key or connected app and one group at a time. Requests in that group then run at once, without asking you, within the limits below. Do this only for an agent you would trust with your mailbox.

How to turn it on:

  • Only in the reply2mail dashboard: open the key or app under Integrations, set the group to run without approval and confirm in the review dialog. It cannot be turned on when you create a key, on the consent screen in Claude or ChatGPT, or by the agent. A key or app cannot change its own permissions or approve its own requests.
  • Paid plans only. If your plan goes back to free, every group returns to approval.
  • For mail:reply, mail:send and mail:bulk you sign in again and, in the same step, enter your two-factor code, or your account password if you do not use two-factor. If you sign in only with Google or Microsoft and have no two-factor, turn two-factor on first.
  • Sending without approval (mail:reply, mail:send) is available for API keys and for the Claude and ChatGPT connections only.

Defaults you can change when you turn it on:

SettingDefaultYou can
RecipientsA reply, new email or forward goes out without asking only to addresses the sending account has written to before (exact address, from that account’s Sent mail). Anything else, including a reply to an email from a stranger, becomes an approval request.Allow a list of addresses, or anyone
Send limit10 an hour and 50 a day per key or app. Sends beyond that become approval requests.Change the numbers
How long30 days, then the group returns to approvalChoose 24 hours, 7 days, or until you turn it off
API keys that sendIts lifetime is shortened to 30 daysKeep the original lifetime
Mail accountsYou can limit a key or app to some of your mail accounts. A send from, or a change in, any other account is refused.Choose the accounts

What always applies, whatever you choose:

  • A mail the safety filter holds always waits for you. If the filter finds something (such as a password, key or ID number), fails, or is switched off, the send becomes an approval request exactly as with approval on, and approving it takes two steps. Sends without approval are checked with the strict filter.
  • The same request repeated within 10 minutes returns the first result; it does not run twice.
  • If the filter holds three mails from one key or app within 24 hours, that key or app returns to approval and you are told.
  • Some actions always ask, even in a group that runs without approval: deleting a folder, deleting trackers, cancelling events, removing or editing memory items, and removing or replacing rules.
  • Changing your password or turning off two-factor returns every key and app to approval, and you are told.
  • To stop it at once: pause the key or app, or turn on Require approval for everything under Integrations.

Afterwards: everything that ran without approval appears in History on the Approvals page, marked as run without approval, and you get a short notification that does not contain the mail. reply2mail’s own assistant and automatic replies are not affected: they always wait for your approval.

The risks, in plain words:

  • Anyone who can send you an email can try to make the agent send mail as you. The filter catches passwords and ID numbers; it does not catch a contract, a price list or a convincing harmful message.
  • You learn about it afterwards. Sent mail cannot be recalled.
  • A leaked key sends as you until it expires or you pause it.
  • Rule, memory and signature changes made this way keep shaping later mail after you disconnect the agent.
  • For that key or app, “reply2mail never sends without your approval” is no longer true.

Webhooks

Instead of polling, your server can receive an HTTPS POST from reply2mail when new mail arrives or when an approval request filed by your key is decided.

Setup:

  • In the dashboard: Integrations → Webhooks. Each endpoint belongs to one API key.
  • The address must use HTTPS and resolve to a public internet address. Redirects are not followed.
  • Free plan: 1 endpoint. Paid plans: 10.
  • The signing secret (whsec_…) is shown once, when you create the endpoint. You can rotate it in the dashboard.

Events:

EventWhen it is sent
message.receivedNew mail arrived in the INBOX of any connected account. Only mail that arrives after the endpoint was created, not the initial import, and only if the key has the Read mail (mail:read) permission.
approval.decidedAn approval request filed by that key was approved, rejected, executed, failed or ended with outcome_unknown. data.state is one of approved, rejected, executed, failed, outcome_unknown.
webhook.testSent when you press Send test event in the dashboard.

The payload carries IDs and minimal metadata only, never the subject, sender or body. Fetch the content over REST with the same key: GET /v1/messages/{id} or GET /v1/approvals/{id}.

message.received
{"id":"evt_3kTq…","type":"message.received","created_at":"2026-10-08T12:00:00Z","api_version":"v1","data":{"account_id":12,"message_id":"m1_…"}}
approval.decided
{"id":"evt_…","type":"approval.decided","created_at":"…","api_version":"v1","data":{"approval_id":"apr_…","state":"executed","account_id":12}}
webhook.test
{"id":"evt_…","type":"webhook.test","created_at":"…","api_version":"v1","data":{"endpoint_id":"wh_12"}}

The request:

HeaderValue
(method)POST
Content-Typeapplication/json
User-Agentreply2mail-webhooks/1.0
Reply2mail-Event-IdUnique ID of the event. Use it to drop duplicates.
Reply2mail-Event-Typemessage.received, approval.decided or webhook.test
Reply2mail-Delivery-Attempt1 to 6
Reply2mail-Signaturet=<unix seconds>,v1=<hex>

Verifying the signature:

  • v1 is the hex HMAC-SHA256 of t + "." + the raw request body, keyed with the endpoint secret (the whole whsec_… string, as UTF-8).
  • Verify on the raw body bytes, before you parse the JSON. Parsed and re-serialised JSON will not match.
  • Split the header on "," and each part on the first "=". There can be more than one v1: for 24 hours after you rotate the secret, the header carries one v1 for the new secret and one for the old. Accept the request if any of them matches.
  • Reject the request if t is more than 300 seconds away from your clock (replay window).
  • Compare signatures in constant time.
Send yourself a signed test request (bash + openssl)
SECRET='whsec_PASTE_YOUR_SECRET'
URL='https://example.com/webhooks/reply2mail'
BODY='{"id":"evt_test_1","type":"webhook.test","created_at":"2026-10-08T12:00:00Z","api_version":"v1","data":{"endpoint_id":"wh_12"}}'
T=$(date +%s)
SIG=$(printf '%s' "$T.$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.*= //')
curl -X POST "$URL" \
  -H 'Content-Type: application/json' \
  -H 'User-Agent: reply2mail-webhooks/1.0' \
  -H 'Reply2mail-Event-Id: evt_test_1' \
  -H 'Reply2mail-Event-Type: webhook.test' \
  -H 'Reply2mail-Delivery-Attempt: 1' \
  -H "Reply2mail-Signature: t=$T,v1=$SIG" \
  --data-binary "$BODY"
Node.js (Express)
const crypto = require('node:crypto')
const express = require('express')

const SECRET = process.env.R2M_WEBHOOK_SECRET // the whole whsec_… string
const TOLERANCE_S = 300

function verify(rawBody, header) {
  let t = null
  const sigs = []
  for (const part of (header || '').split(',')) {
    const i = part.indexOf('=')
    if (i === -1) continue
    const key = part.slice(0, i).trim()
    const value = part.slice(i + 1).trim()
    if (key === 't') t = value
    else if (key === 'v1') sigs.push(value)
  }
  if (!t || !/^\d+$/.test(t) || sigs.length === 0) return false
  if (Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCE_S) return false
  const expected = crypto.createHmac('sha256', SECRET)
    .update(t + '.', 'utf8')
    .update(rawBody)
    .digest()
  return sigs.some((s) => /^[0-9a-f]{64}$/i.test(s) &&
    crypto.timingSafeEqual(Buffer.from(s, 'hex'), expected))
}

const app = express()
// express.raw keeps the body as bytes: no express.json() in front of this route
app.post('/webhooks/reply2mail', express.raw({ type: 'application/json' }), (req, res) => {
  if (!Buffer.isBuffer(req.body) || !verify(req.body, req.get('Reply2mail-Signature'))) {
    return res.sendStatus(400)
  }
  const eventId = req.get('Reply2mail-Event-Id')
  // skip if eventId was already handled; otherwise store it and do the work later
  const event = JSON.parse(req.body.toString('utf8'))
  res.sendStatus(204)
})

app.listen(3000)
Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, abort, request

SECRET = os.environ["R2M_WEBHOOK_SECRET"].encode("utf-8")  # the whole whsec_… string
TOLERANCE_S = 300
app = Flask(__name__)

def verify(raw, header):
    t, sigs = None, []
    for part in (header or "").split(","):
        key, sep, value = part.strip().partition("=")
        if not sep:
            continue
        if key == "t":
            t = value
        elif key == "v1":
            sigs.append(value)
    if not t or not (t.isascii() and t.isdigit()) or not sigs:
        return False
    if abs(time.time() - int(t)) > TOLERANCE_S:
        return False
    expected = hmac.new(SECRET, t.encode() + b"." + raw, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected.encode(), s.lower().encode()) for s in sigs)

@app.post("/webhooks/reply2mail")
def reply2mail_webhook():
    raw = request.get_data()  # raw bytes: verify before parsing JSON
    if not verify(raw, request.headers.get("Reply2mail-Signature")):
        abort(400)
    event_id = request.headers["Reply2mail-Event-Id"]
    # skip if event_id was already handled; otherwise store it and do the work later
    event = request.get_json()
    return "", 204

Delivery and retries:

  • A 2xx answer within 10 seconds counts as delivered. Anything else, including a 3xx redirect, is a failure.
  • A failed delivery is retried, up to 6 attempts in all. The retries come 1 minute, 10 minutes, 1 hour, 6 hours and 16 hours after the attempt before (about 23 hours from first to last).
  • Events can arrive more than once and out of order. Dedupe on Reply2mail-Event-Id. Answer quickly: store the event and do the work afterwards.
  • After 20 failed attempts in a row spanning at least 24 hours, the endpoint is turned off automatically. You get a notification and can turn it on again in the dashboard.
  • The dashboard shows a delivery log for each endpoint: status code, latency and attempt.
  • The OpenAPI document (https://api.reply2mail.app/v1/openapi.json) describes the events in its webhooks section (OpenAPI 3.1).

Event payloads and headers in the API reference →

What a key or app changed

Anything a key or connected app creates or changes is labelled with its name in Rules, Memory, Agent settings and trackers, and in mails reply2mail sends you (tracker digest, event reminders). In the dashboard, the page of each key or app lists the changes it made and its activity log.

Sensitive-mail filter for ChatGPT

For connections made from ChatGPT, reply2mail leaves out emails it classifies as login codes, password resets and security codes, and the result says why they were left out. API keys and Claude connections are not filtered. The classification can miss an email, so the filter is not a guarantee.

Limits

Calls from connected products do not use your monthly mail credits. These limits protect your mailbox and the service; the current values for your account are shown under Integrations.

LimitFree planPaid plans
Active keys and connected apps110
Calls per minute, per key or connection2060
Mark read / unread / flag per minute, per key or connection1020
Calls per day, all keys and connections together2005,000
Calls running at the same time, per key or connection22
Request size256 KB256 KB
Response size64 KB64 KB
New keys per hour55

A call over a limit returns an error that states the limit and, where it applies, when you can try again (the Retry-After header on REST).

Key expiry and connection lifetime

  • API keys expire after the period you choose: 90 days by default, 180 days at most.
  • The dashboard marks a key in amber when it has 7 days or less left. Rotate it before it expires: rotating gives you a new key, and the old one keeps working for 24 hours (or until its own expiry, if sooner) so you can update your config.
  • A connection made by signing in from Claude or ChatGPT stays valid while it is used at least once every 30 days.

Before you connect: what to know

  • A product with mail:read can read every email in all mail accounts connected to reply2mail, including password-reset links and one-time login codes from other services. The only exception is the ChatGPT filter above. If you would not give a product your mailbox password, do not connect it.
  • Emails can contain instructions. Someone else’s email can try to tell the connected AI what to do. reply2mail marks email content as untrusted for the AI, but cannot guarantee the AI ignores it.
  • What the connected product does with data it has read (storing, sharing, sending it through its own tools, training) is governed by that product’s terms and is outside reply2mail’s control.
  • Treat an API key like a password. Do not put it in shared code or public places.

Pausing and revoking

Under Integrations you can pause, revoke or disconnect any key or connected app. It stops working on its next request. Removing reply2mail inside Claude or ChatGPT stops that product from using the connection; to be sure it is also ended on reply2mail’s side, revoke it in the dashboard too. If you think a key has leaked, revoke it immediately.

See also: Developers (REST API) · REST API reference · Privacy Policy · Terms of Service