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/mcpThe 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:
export R2M_TOKEN=PASTE_YOUR_KEY_HERE$env:R2M_TOKEN = 'PASTE_YOUR_KEY_HERE'API keys use their own host:
https://api.reply2mail.app/mcpClaude 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):
{
"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):
{
"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_inboxWithin /v1, changes are additive only: new tools, new optional parameters, new result fields. Clients should ignore result fields they do not know.
Permissions
Each key or connection has its own permissions, grouped as follows. You can change them at any time under Integrations.
| Group | What it allows | How it runs |
|---|---|---|
| mail:read | Search, list and count mail; read a message, thread or attachment text; folders; accounts; reply and email previews (nothing is stored or sent); daily brief | Read only |
| calendar:read | List events, find conflicts, list proposed events | Read only |
| trackers:read | List trackers and read one | Read only |
| mail:organize | Mark a message read or unread, star it | Runs immediately, one message at a time (default); can be set to ask first |
| mail:reply | Send a reply to a message | Waits for your approval (default); on paid plans can run without approval for one key or app |
| mail:send | Send a new email, forward an email | Waits for your approval (default); on paid plans can run without approval for one key or app |
| mail:manage | Archive, move to Trash, move or label one message | Waits for your approval (default); on paid plans can run without approval for one key or app |
| mail:bulk | Archive, move to Trash, label or mark read several messages; mark all unread mail read; restore recently trashed mail | Waits for your approval (default); on paid plans can run without approval for one key or app |
| mail:folders | Create or delete a folder | Waits for your approval (default); on paid plans can run without approval for one key or app |
| calendar:write | Create, propose, update, confirm and delete events; update or delete several events | Waits for your approval (default); on paid plans can run without approval for one key or app |
| trackers:write | Create a tracker, link messages or events to it, add a note, set it to open or waiting | Runs immediately (default); can be set to ask first |
| trackers:remove | Resolve or delete trackers, one or several | Waits for your approval (default); on paid plans can run without approval for one key or app |
| memory:read | Read what reply2mail remembers | Off by default; read only when turned on |
| memory:write | Add, edit or remove memory items | Off by default; when turned on, waits for your approval (can run without approval for one key or app on paid plans) |
| rules:read | List your rules | Off by default; read only when turned on |
| rules:write | Add, turn on, turn off, remove or replace rules | Off by default; when turned on, waits for your approval (can run without approval for one key or app on paid plans) |
| profile:read | Read your persona, signature, brand voice, language and reply templates | Off by default; read only when turned on |
| profile:write | Change your persona, signature, brand voice or language; add a reply template | Off by default; when turned on, waits for your approval (can run without approval for one key or app on paid plans) |
| account:read | Plan and credits, usage, setup status, what reply2mail’s own assistant did | Off 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:
| Setting | Default | You can |
|---|---|---|
| Recipients | A 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 limit | 10 an hour and 50 a day per key or app. Sends beyond that become approval requests. | Change the numbers |
| How long | 30 days, then the group returns to approval | Choose 24 hours, 7 days, or until you turn it off |
| API keys that send | Its lifetime is shortened to 30 days | Keep the original lifetime |
| Mail accounts | You 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:
| Event | When it is sent |
|---|---|
| message.received | New 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.decided | An 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.test | Sent 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}.
{"id":"evt_3kTq…","type":"message.received","created_at":"2026-10-08T12:00:00Z","api_version":"v1","data":{"account_id":12,"message_id":"m1_…"}}{"id":"evt_…","type":"approval.decided","created_at":"…","api_version":"v1","data":{"approval_id":"apr_…","state":"executed","account_id":12}}{"id":"evt_…","type":"webhook.test","created_at":"…","api_version":"v1","data":{"endpoint_id":"wh_12"}}The request:
| Header | Value |
|---|---|
| (method) | POST |
| Content-Type | application/json |
| User-Agent | reply2mail-webhooks/1.0 |
| Reply2mail-Event-Id | Unique ID of the event. Use it to drop duplicates. |
| Reply2mail-Event-Type | message.received, approval.decided or webhook.test |
| Reply2mail-Delivery-Attempt | 1 to 6 |
| Reply2mail-Signature | t=<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.
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"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)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 "", 204Delivery 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).
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.
| Limit | Free plan | Paid plans |
|---|---|---|
| Active keys and connected apps | 1 | 10 |
| Calls per minute, per key or connection | 20 | 60 |
| Mark read / unread / flag per minute, per key or connection | 10 | 20 |
| Calls per day, all keys and connections together | 200 | 5,000 |
| Calls running at the same time, per key or connection | 2 | 2 |
| Request size | 256 KB | 256 KB |
| Response size | 64 KB | 64 KB |
| New keys per hour | 5 | 5 |
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