RadMail · Developers
v1 API reference
Everything the inbox knows — the can’t-miss lane, ranked search, commitments in both directions, and the work RadMail prepared for you — over a small, stable REST surface. Machine-readable version at /llms.txt.
Authentication
Every request carries a bearer API key. Mint one in about a minute at Settings → API keys — the key’s workspace is bound server-side; a request can never act as another tenant.
Authorization: Bearer tmk_live_...
- Base URL
https://app.radmail.ai/api/v1- Scopes
- read (all GETs) · write (POST /ingest, POST /send, POST /send/{id}/confirm)
- Plans & limits
- Pro and up: full access at 120 requests/min per key. Free and trial workspaces: every read endpoint works on the metered free tier at 100 requests/day per key; write requires Pro+ (403 api_not_entitled_upgrade_to_pro).
- Rate-limit headers
- Every response carries X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset; 429s add Retry-After.
- Errors
- Errors are { ok: false, error } — 401 missing_or_malformed_api_key | invalid_api_key, 403 missing_scope:<scope> | api_not_entitled_upgrade_to_pro, 429 rate_limited.
/right-nowscope: readfree tier OKWhat can't be missed right now — the inbox's own hero lane, ranked by the two-axis importance engine. Each item carries importance (0–100), urgency (0–100), band (low | normal | high | critical) and plain-English reasons. Candidate pool is recent mail (last 90 days, unarchived, not spam); `total` is qualifying rows in that pool.
limit· query- default 10, max 50
offset· query- default 0
Returns: { ok, data: [{ id, receivedAt, from, fromName, subject, classification, importance, urgency, band, reasons[], counterparty, threadId }], pagination }
curl -H "Authorization: Bearer $RADMAIL_API_KEY" "https://app.radmail.ai/api/v1/right-now?limit=10"
/searchscope: readfree tier OKRanked search over your emails — every term must match sender, subject, or body; header matches rank above body-only matches. Hits include a ~160-char body snippet and matchedIn (which fields hit).
q· query · required- search terms (max 200 chars, first 8 whitespace-separated terms used)
from· query- substring filter on sender email or name
after· query- ISO date — only mail received after
before· query- ISO date — only mail received before
limit· query- default 25, max 100
offset· query- default 0
Returns: { ok, data: [{ ...email metadata, snippet, matchedIn[] }], pagination }
curl -H "Authorization: Bearer $RADMAIL_API_KEY" "https://app.radmail.ai/api/v1/search?q=invoice+overdue"
/emailsscope: readfree tier OKList your emails newest-first (metadata only — bodies stay out of list views).
limit· query- default 25, max 100
offset· query- default 0
Returns: { ok, data: [{ id, receivedAt, from, fromName, subject, classification, classificationSource, isSpam, needsOwnerEyes, counterparty, threadId }], pagination }
curl -H "Authorization: Bearer $RADMAIL_API_KEY" "https://app.radmail.ai/api/v1/emails?limit=25"
/emails/{id}scope: readfree tier OKFetch one email by id, including the text body and attachment count.
id· path · required- email UUID from a list/search response
Returns: { ok, email: { ...metadata, to, textBody, attachmentCount, llmClass } } — 404 if not found
curl -H "Authorization: Bearer $RADMAIL_API_KEY" "https://app.radmail.ai/api/v1/emails/<uuid>"
/emails/by-message-idscope: readfree tier OKResolve an RFC 5322 Message-ID to your email's backend id. Lets a client map ANY local thread — including mail the engine didn't surface — to its RadMail id, e.g. to submit a 👍/👎 via POST /feedback. Returns the most recent match when a Message-ID repeats.
messageId· query · required- the RFC 5322 Message-ID header value (max 998 chars, angle brackets included as sent)
Returns: { ok, data: { id, messageId, from, fromName, subject, receivedAt, classification, isSpam, threadId } } — 404 not_found
curl -H "Authorization: Bearer $RADMAIL_API_KEY" "https://app.radmail.ai/api/v1/emails/by-message-id?messageId=%3Cabc%40mail.example.com%3E"
/prepared-draftsscope: readfree tier OKThe answer-ready inbox, as data: your stored AI-prepared reply drafts, newest first, each linked to its source email. Read-only — a draft is NEVER auto-sent; a human approves every reply (the BEC hard-stop stays sacred). Filter to one email with ?emailId=.
emailId· query- email UUID — return only drafts prepared for that email
limit· query- default 25, max 100
offset· query- default 0
Returns: { ok, data: [{ id, emailId, style, model, promptVersion, createdAt, body, from, subject }], pagination }
curl -H "Authorization: Bearer $RADMAIL_API_KEY" "https://app.radmail.ai/api/v1/prepared-drafts?limit=25"
/commitmentsscope: readfree tier OKOpen commitments in both directions — what you owe and what you're owed — extracted from mail and tracked to completion (north star: nothing lost, in either direction).
limit· query- default 25, max 100
offset· query- default 0
Returns: { ok, data: [{ id, direction, party, action, actionType, dueDate, duePhrase, state, confidence, counterpartyEmail }], pagination }
curl -H "Authorization: Bearer $RADMAIL_API_KEY" "https://app.radmail.ai/api/v1/commitments"
/usagescope: readfree tier OKWork-units rollup: how much work RadMail did for your org over a window — prepared reply drafts, lead dossiers, and supporting triage calls, grouped by feature, with honest engine cost.
days· query- window, default 30, max 90
Returns: { ok, windowDays, workUnits, triageCalls, totalCalls, totalCostCents, byFeature[] }
curl -H "Authorization: Bearer $RADMAIL_API_KEY" "https://app.radmail.ai/api/v1/usage?days=30"
/ingestscope: writePro and upPush an email into your workspace from your own infrastructure. Runs the full pipeline — classification, threading, importance, commitments, webhooks — exactly like every other inbound path. The org is bound to your key; the body can never assert a different tenant.
fromEmail· body · required- sender address
subject· body- subject line
rawText· body- plain-text body
rawHtml· body- HTML body
toEmail· body- recipient address
fromName· body- sender display name
receivedAt· body- ISO timestamp, defaults to now
idempotencyKey· body- dedupe key — safe retries return 200 instead of creating a duplicate
headers· body- object of raw headers
attachments· body- array of attachment descriptors
Returns: 201 on create, 200 when deduped by idempotencyKey; { ok, ... }
curl -X POST -H "Authorization: Bearer $RADMAIL_API_KEY" -H "Content-Type: application/json" -d '{"fromEmail":"vendor@example.com","subject":"Invoice #1042","rawText":"Net-30, due Friday."}' "https://app.radmail.ai/api/v1/ingest"/feedbackscope: readfree tier OKTeach the importance engine: a 👍/👎 on one of your emails records the operator's ground-truth label (important | not) that the tuner + scorer consume, so the engine learns what actually matters to you. Read-scope — it's a self-org training signal on your own RLS-scoped mail, not a privileged write. One current vote per email; a repeated vote corrects rather than piles up.
emailId· body · required- email UUID (from any list/search response, or GET /emails/by-message-id)
verdict· body · required- 'up' (👍 important) or 'down' (👎 not) — anything else is 400 invalid_verdict
Returns: { ok, label: 'important' | 'not' } — 404 email_not_found if the id isn't yours
curl -X POST -H "Authorization: Bearer $RADMAIL_API_KEY" -H "Content-Type: application/json" -d '{"emailId":"<uuid>","verdict":"up"}' "https://app.radmail.ai/api/v1/feedback"/sendscope: writePro and upHuman-initiated send with the outbound double-check gate: RadMail runs deterministic checks (recipient-typo distance, placeholder scan, attachment presence, reply-context match) plus an LLM review BEFORE anything leaves via your connected M365 mailbox. Clean sends to normal recipients release immediately (201). Anything questionable — and ALWAYS regulator/government/court/bank recipients or money/banking-change/first-contact content — returns 202 held with a one-time confirmToken: a human re-reads the rendered email and confirms. Feature-flagged (404 send_api_disabled until enabled); the from address must be one of your org's connected M365 mailboxes with send permission.
from· body · required- a CONNECTED M365 mailbox address of your org (the send goes out through it)
to· body · required- recipient address or array of addresses
cc· body- cc address(es)
bcc· body- bcc address(es)
subject· body · required- subject line (an empty subject holds the send)
markdown· body · required- markdown or plain-text body (aliases: body, text) — rendered to safe HTML server-side
inReplyTo· body- Internet Message-ID being replied to — enables the reply-context match check + threading
attachments· body- array of { filename, contentType, contentBase64 } — max 10, 3 MB total decoded
requestedBy· body- who is asking (e.g. 'doug-claude-session') — recorded as send provenance
Returns: 201 { ok, status: 'sent', requestId, sendId } · 202 { ok, status: 'held', requestId, tier, holdReasons[], findings[], renderedHtml, confirmToken, confirmExpiresAt, confirmEndpoint } · 409 mailbox_send_scope_missing when the mailbox was connected read-only (reconnect with send permission)
curl -X POST -H "Authorization: Bearer $RADMAIL_API_KEY" -H "Content-Type: application/json" -d '{"from":"you@yourco.com","to":"kat@vendor.com","subject":"Thursday delivery","markdown":"Confirming Thursday works."}' "https://app.radmail.ai/api/v1/send"/send/{id}scope: readfree tier OKRead one send request — the human re-read surface before confirming a held send: status, tier, hold reasons, every deterministic finding, the LLM review verdict, and the rendered final HTML exactly as it would leave.
id· path · required- send request UUID (from POST /send)
Returns: { ok, request: { id, status, tier, from, to, cc, bcc, subject, renderedHtml, findings[], holdReasons[], llmVerdict, sendId, ... } }
curl -H "Authorization: Bearer $RADMAIL_API_KEY" "https://app.radmail.ai/api/v1/send/<uuid>"
/send/{id}/confirmscope: writePro and upRelease a HELD send after a human re-reads it — the second signature on the double-check gate. Requires the one-time confirmToken from the hold response; the release claim is atomic, so a racing double-confirm can never double-send. Tokens expire after 24 hours (410 confirm_token_expired — re-submit the send).
id· path · required- send request UUID
confirmToken· body · required- the snd_cfm_… token returned when the send was held (shown once, never stored)
confirmedBy· body- who confirmed (recorded on the request)
Returns: { ok, status: 'sent', requestId, sendId } — 403 invalid_confirm_token, 409 already_sent, 410 confirm_token_expired
curl -X POST -H "Authorization: Bearer $RADMAIL_API_KEY" -H "Content-Type: application/json" -d '{"confirmToken":"snd_cfm_...","confirmedBy":"doug"}' "https://app.radmail.ai/api/v1/send/<uuid>/confirm"Using an AI agent instead?
The RadMail MCP server (npm: radmail-mcp, MIT) speaks this API for you — zero-auth sandbox out of the box; set RADMAIL_API_KEY for read-only access to your real inbox.
npx radmail-mcp
Webhooks (signed outbound events) are configured per workspace at Settings → API keys & webhooks. Security posture and the verifiable refusal receipt live at /trust.