Developers · REST API

The AI Emaily API. Your inbox, programmable.

One versioned REST API over your unified inbox — Gmail, Outlook/Microsoft 365, and any IMAP provider — plus the AI layer on top: semantic search, voice-matched drafting, the Living Brief, and the autonomous agent. Scoped API keys, signed webhooks, undo on every send.

base URL https://api.aiemaily.com/v1

Overview

One API for mail, search, AI, and the agent

REST + JSON, versioned in the URL (/v1), documented by an OpenAPI 3.1 spec that is the contract's source of truth. Everything the AI Emaily app can do on your behalf, your code can do too — inside the same safety rails.

Mail as data

Threads, messages, drafts, contacts, rules, and calendar across every provider — normalized into one schema, cursor-paginated, mirror-consistent.

The AI layer

Semantic search, RAG answers over your mailbox, voice-matched drafting grounded in Personal Context, and the Living Brief as structured JSON.

Push, not poll

Signed webhooks for new mail, sends, agent actions, and briefs — HMAC-SHA256 with rotation, retries with backoff.

Safety by construction

Scoped keys, confirm-gated sends with undo windows, daily send caps, plan-aware quotas, and a complete audit log.

Building for an AI assistant instead of code? The same capabilities are exposed as MCP tools at https://mcp.aiemaily.com — see the MCP server documentation.

Quickstart

First call in under a minute

Create a key, list your unread threads, then search by meaning. That's the whole on-ramp.

terminal
# 1. Create an API key at app.aiemaily.com → Settings → Developer
export AIEMAILY_API_KEY="aiem_live_..."

# 2. Your first call — what's unread in the inbox?
curl "https://api.aiemaily.com/v1/threads?state=inbox&unread=true&limit=5" \
  -H "Authorization: Bearer $AIEMAILY_API_KEY"

# 3. Search by meaning, not keywords
curl -X POST https://api.aiemaily.com/v1/search \
  -H "Authorization: Bearer $AIEMAILY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "the invoice Marta chased last month", "mode": "semantic"}'

Authentication

Scoped API keys

Bearer authentication with keys you create, scope, and revoke at Settings → Developer. Keys are shown once and stored only as a hash.

every request
Authorization: Bearer aiem_live_3kd92mf8a1...
  • Key format: aiem_live_… for production, aiem_test_… for test keys. Prefixes are safe to log; full keys never are.
  • Scopes are fixed at creation. Create one narrowly-scoped key per integration so revoking one doesn't break the rest.
  • Revocation is immediate — keys are checked on every request. Rotation: create the new key, deploy it, revoke the old one.
  • OAuth 2.1 + PKCE with dynamic client registration is live on the MCP server for AI clients acting on your behalf. Three-legged OAuth for third-party REST apps is not yet public — contact us if you're building one.

Scopes

All permission scopes for the AI Emaily API
ScopeGrants
mail:readRead threads, messages, and attachments
mail:writeArchive, label, snooze, mark read/unread, star
mail:sendSend an existing draft (confirm-gated) and cancel within the undo window
drafts:readList and read drafts
drafts:writeCreate and edit drafts
search:readKeyword, semantic, and hybrid search
contacts:readList contacts and relationship data
contacts:writeUpdate contacts — VIP flags, notes
context:readRead client profiles and typed variables
context:writeCreate and update client profiles and variables
brief:readRead the Living Brief
ai:invokeRun AI operations (ask-inbox, AI drafting) — spends plan credits
agent:readRead the agent action log (what Copilot/Autopilot did)
agent:runTrigger an agent pass over the inbox, within your authority settings
calendar:readRead calendar events from connected accounts
calendar:writeCreate and delete calendar events
webhooks:manageCreate, rotate, and delete webhook endpoints
usage:readRead quota and credit balances

Conventions

Pagination, idempotency, errors

Learn these three patterns and every endpoint behaves predictably.

Cursor pagination

List endpoints return { data, has_more, next_cursor }. Pass cursor to get the next page. Cursors are opaque — don't parse them.

pagination
GET /v1/threads?limit=25
→ { "data": [...25 threads...], "has_more": true, "next_cursor": "eyJvZmZzZXQiOjI1fQ" }

GET /v1/threads?limit=25&cursor=eyJvZmZzZXQiOjI1fQ
→ { "data": [...], "has_more": false }

Idempotency

Send an Idempotency-Key header (any unique string, e.g. a UUID) on mutating requests. Retries with the same key return the original result instead of repeating the action. Required on send endpoints; recommended everywhere else.

Error envelope

Every error is a stable machine-readable code plus a human message and a request_id you can give support. The full code table is below.

error · 400
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_params",
    "message": "Sending requires confirm: true — sends are never implicit.",
    "param": "confirm",
    "request_id": "req_8f31ka92"
  }
}

Endpoint reference

Every endpoint

Grouped by resource, each with parameters and a real request/response pair. Search by path, scope, or what it does.

Accounts

The mailboxes connected to the AI Emaily account. Read-only over the API — connecting a mailbox happens in-app (OAuth with the provider).

GET/accountsmail:readList connected mailboxes.
response
{
  "data": [
    { "id": "acc_71b2", "provider": "gmail", "email": "[email protected]", "status": "active" },
    { "id": "acc_98x1", "provider": "outlook", "email": "[email protected]", "status": "active" }
  ],
  "has_more": false
}

Threads

Conversations across every connected mailbox. State changes mirror back to Gmail/Outlook/IMAP so all clients stay consistent.

GET/threadsmail:readList threads, filtered and cursor-paginated.
Parameters for GET /threads
ParameterInTypeDescription
statequeryinbox | archived | snoozed | trash | sentThread state. Default inbox.
tabqueryprimary | social | promotions | updatesCategory tab filter.
labelquerystringOnly threads carrying this label.
account_idquerystringRestrict to one mailbox.
unreadquerybooleanOnly unread threads.
limitquerynumber (1–100)Page size. Default 25.
cursorquerystringCursor from a previous page.
request
GET /v1/threads?state=inbox&tab=primary&unread=true&limit=10
response
{
  "data": [
    {
      "id": "thr_9f2ka81",
      "account_id": "acc_71b2",
      "subject": "Contract renewal — need your sign-off by Friday",
      "from": { "name": "Dana Whitfield", "email": "[email protected]" },
      "snippet": "Hi — legal cleared the redlines. Can you confirm the...",
      "labels": ["important", "client/acme"],
      "unread": true,
      "message_count": 4,
      "updated_at": "2026-07-10T14:22:00Z"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJvZmZzZXQiOjEwfQ"
}
GET/threads/{id}mail:readGet one thread with its message list.
Parameters for GET /threads/{id}
ParameterInTypeDescription
id*pathstringThread id.
response
{
  "id": "thr_9f2ka81",
  "subject": "Contract renewal — need your sign-off by Friday",
  "participants": ["[email protected]", "[email protected]"],
  "labels": ["important", "client/acme"],
  "state": "inbox",
  "messages": [{ "id": "msg_77xk1", "from": { "email": "[email protected]" }, "date": "2026-07-10T14:22:00Z", "snippet": "Hi — legal cleared..." }]
}
PATCH/threads/{id}mail:writeMutate thread state — read, star, archive, labels, snooze.

All mutations are reversible and mirror to the provider. Combine multiple changes in one call.

Parameters for PATCH /threads/{id}
ParameterInTypeDescription
readbodybooleanMark read/unread.
starredbodybooleanStar / unstar.
statebodyinbox | archived | trash | spamMove the thread.
add_labels / remove_labelsbodystring[]Label changes. New labels are created on first use.
snooze_untilbodyISO 8601 datetime | nullSnooze; null unsnoozes.
request
{ "read": true, "add_labels": ["client/acme"], "state": "archived" }
response
{ "id": "thr_9f2ka81", "state": "archived", "labels": ["important", "client/acme"], "unread": false }
GET/threads/{id}/messagesmail:readList full messages in a thread.
Parameters for GET /threads/{id}/messages
ParameterInTypeDescription
include_quotedquerybooleanInclude quoted reply history in bodies. Default false.
response
{
  "data": [
    {
      "id": "msg_77xk1",
      "from": { "name": "Dana Whitfield", "email": "[email protected]" },
      "to": [{ "email": "[email protected]" }],
      "date": "2026-07-10T14:22:00Z",
      "body_text": "Hi — legal cleared the redlines. Can you confirm the renewal terms by Friday? — Dana",
      "attachments": [{ "id": "att_3m1", "name": "renewal-v4.pdf", "size": 182044, "mime": "application/pdf" }]
    }
  ],
  "has_more": false
}

Messages & attachments

Individual messages, decrypted bodies, and attachment downloads. Bodies are stored encrypted; decryption happens server-side per request.

GET/messages/{id}mail:readGet one message with metadata and text body.
response
{
  "id": "msg_77xk1",
  "thread_id": "thr_9f2ka81",
  "from": { "name": "Dana Whitfield", "email": "[email protected]" },
  "date": "2026-07-10T14:22:00Z",
  "body_text": "Hi — legal cleared the redlines...",
  "headers": { "message_id": "<[email protected]>" }
}
GET/messages/{id}/bodymail:readStream the full sanitized HTML body.

Returns text/html. Tracking pixels are stripped and links sandboxed, exactly as in the app.

response
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

<!doctype html><html>…sanitized message body…</html>
GET/messages/{id}/attachments/{att_id}mail:readDownload an attachment (streamed).
response
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="renewal-v4.pdf"

<binary>

Drafts & sending

The recommended flow is draft-then-send: iterate on a draft, then commit with an explicit send call. Idempotency-Key is required on sends.

GET/draftsdrafts:readList drafts.
response
{
  "data": [{ "id": "drf_8s31x", "to": ["[email protected]"], "subject": "Re: Contract renewal", "status": "draft", "updated_at": "2026-07-10T14:50:00Z" }],
  "has_more": false
}
POST/draftsdrafts:writeCreate a draft — plain, or AI-written in the user's voice.

With use_ai: true, AI Emaily writes the body from instructions using the user's Personal Context (spends 1 AI credit).

Parameters for POST /drafts
ParameterInTypeDescription
account_idbodystringSending mailbox. Default: primary account.
reply_to_thread_idbodystringCompose as a reply to this thread.
to / cc / bccbodystring[]Recipients. Inherited when replying.
subjectbodystringSubject. Inherited when replying.
bodybodystringPlain text or simple HTML body.
use_aibodybooleanHave AI Emaily write the body. Default false.
instructionsbodystringWhat the AI draft should say (with use_ai).
request
{
  "reply_to_thread_id": "thr_9f2ka81",
  "use_ai": true,
  "instructions": "Confirm the renewal at the agreed $24k/year, ask for the signature link, warm tone."
}
response
{
  "id": "drf_8s31x",
  "to": ["[email protected]"],
  "subject": "Re: Contract renewal — need your sign-off by Friday",
  "body": "Hi Dana — confirmed on our side at $24,000/year as agreed. Send over the signature link and I'll turn it around today. Best, Alex",
  "status": "draft",
  "credits_spent": 1
}
PATCH/drafts/{id}drafts:writeEdit a draft.
request
{ "body": "Hi Dana — confirmed at $24,000/year. Please send the signature link. Best, Alex" }
response
{ "id": "drf_8s31x", "status": "draft", "updated_at": "2026-07-10T14:58:00Z" }
DELETE/drafts/{id}drafts:writeDelete a draft.
response
{ "id": "drf_8s31x", "deleted": true }
POST/drafts/{id}/sendmail:sendSend a draft — explicit, capped, undoable.

Requires confirm: true and an Idempotency-Key header. Counts against the plan's daily send cap. Returns an undo handle valid for the undo window.

Parameters for POST /drafts/{id}/send
ParameterInTypeDescription
confirm*bodybooleanMust be true.
undo_window_sbodynumber (0–30)Server-side undo hold. Default 10.
scheduled_atbodyISO 8601 datetimeSchedule instead of sending now.
request
POST /v1/drafts/drf_8s31x/send
Idempotency-Key: 5f3e9c1a-renewal-reply

{ "confirm": true }
response
{
  "send_id": "snd_1vv92",
  "status": "scheduled",
  "undo_until": "2026-07-10T15:04:10Z"
}
POST/sends/{id}/cancelmail:sendCancel a send inside its undo window.

Atomic: succeeds only before dispatch. After the window, returns 409 undo_window_elapsed and the message stays sent.

response
{ "send_id": "snd_1vv92", "status": "undone", "draft_id": "drf_8s31x" }
POST/messages/sendmail:sendCompose and send in one call (scripts only).

For trusted automations. Same caps, idempotency requirement, and undo semantics as draft sends. Not exposed as an MCP tool — agents must use the draft-then-send flow.

request
POST /v1/messages/send
Idempotency-Key: 91c2ab-receipt-fwd

{
  "account_id": "acc_71b2",
  "to": ["[email protected]"],
  "subject": "June receipts",
  "body": "Attached digest of June receipts.",
  "undo_window_s": 10
}
response
{ "send_id": "snd_8a2m1", "status": "scheduled", "undo_until": "2026-07-10T15:10:10Z" }

Contacts

People, enriched with relationship signals from your mail history.

GET/contactscontacts:readList or search contacts.
Parameters for GET /contacts
ParameterInTypeDescription
qquerystringName, email, or domain filter.
vipquerybooleanOnly VIPs.
response
{
  "data": [{ "id": "cnt_11a", "name": "Dana Whitfield", "email": "[email protected]", "vip": true, "last_contact": "2026-07-10", "threads": 42 }],
  "has_more": false
}
PATCH/contacts/{id}contacts:writeUpdate a contact (VIP flag, notes).
request
{ "vip": true, "notes": "VP Ops at Acme — renewal owner." }
response
{ "id": "cnt_11a", "vip": true }

Rules

Inbox rules (filing, labeling) and agent rules (per-sender authority). Conditions and actions are validated against the same allowlist the app uses.

GET/rulesmail:readList rules.
response
{
  "data": [
    {
      "id": "rul_5m2",
      "kind": "inbox",
      "conditions": [{ "field": "from_domain", "op": "equals", "value": "acme.com" }],
      "actions": [{ "type": "addLabel", "value": "client/acme" }],
      "priority": 10
    }
  ]
}
POST/rulesmail:writeCreate a rule.
request
{
  "kind": "inbox",
  "conditions": [{ "field": "subject", "op": "contains", "value": "invoice" }],
  "actions": [{ "type": "addLabel", "value": "finance" }, { "type": "setCategory", "value": "updates" }]
}
response
{ "id": "rul_9k1", "kind": "inbox", "priority": 20 }
DELETE/rules/{id}mail:writeDelete a rule.
response
{ "id": "rul_9k1", "deleted": true }

Personal Context

The Context & Variables Engine: per-client profiles keyed to email domains, plus typed variables with per-client overrides. What makes AI drafts factually right.

GET/context/profilescontext:readList client/domain profiles.
response
{
  "data": [{ "id": "ctx_acme", "domain": "acme.com", "name": "Acme Corp", "updated_at": "2026-07-10T15:00:00Z" }],
  "has_more": false
}
GET/context/profiles/{id}context:readGet one profile with its variables.
response
{
  "id": "ctx_acme",
  "domain": "acme.com",
  "summary": "Enterprise client since 2024. Renewal cycle: August. Main contact Dana Whitfield (VP Ops).",
  "variables": { "client.name": "Acme Corp", "pricing.renewal": "$24,000/year", "tone": "warm, concise" }
}
PATCH/context/profiles/{id}context:writeUpdate profile variables or append a note.
request
{ "set": { "pricing.renewal": "$24,000/year" }, "append_note": "Renewal confirmed 2026-07-10 at $24k/yr." }
response
{ "id": "ctx_acme", "updated_keys": ["pricing.renewal"] }
GET/context/variablescontext:readList global typed variables.
response
{ "data": [{ "key": "pricing.pro", "value": "$20/month", "type": "string" }] }
GET/context/filescontext:readList the reference files attached to Personal Context.

Documents you've added to the context brain (rate cards, playbooks, boilerplate) that ground AI drafting. Returns metadata; bodies are fetched by id.

response
{
  "data": [{ "id": "ctf_5a2", "name": "2026-rate-card.pdf", "bytes": 84213, "profile_id": "ctx_acme", "created_at": "2026-07-01T10:00:00Z" }],
  "has_more": false
}

Brief

The Living Brief as structured data — read the latest, or generate a fresh one on demand. Generation is credit-metered (BYOK plans uncapped) and reports credits_spent.

GET/brief/latestbrief:readGet the latest Living Brief.
response
{
  "date": "2026-07-10",
  "needs_reply": [{ "thread_id": "thr_9f2ka81", "from": "[email protected]", "why": "Sign-off requested by Friday" }],
  "commitments": [{ "thread_id": "thr_5h2m6", "promise": "Send Q3 proposal to Linh", "due": "2026-07-11" }],
  "calendar": [{ "title": "Acme renewal call", "start": "2026-07-10T16:00:00Z" }]
}
POST/brief/generateai:invokeGenerate a fresh brief now (spends AI credits).
response
{ "date": "2026-07-10", "status": "ready", "credits_spent": 3 }

Calendar

Google Calendar events synced through connected accounts.

GET/calendar/eventscalendar:readList events in a window.
Parameters for GET /calendar/events
ParameterInTypeDescription
from / to*queryISO 8601 datetimeWindow bounds.
response
{ "data": [{ "id": "evt_3k1", "title": "Acme renewal call", "start": "2026-07-10T16:00:00Z", "end": "2026-07-10T16:30:00Z" }] }
POST/calendar/eventscalendar:writeCreate an event.
request
{ "title": "Contract review", "start": "2026-07-14T15:00:00Z", "end": "2026-07-14T15:30:00Z", "attendees": ["[email protected]"] }
response
{ "id": "evt_9m4", "status": "confirmed" }
DELETE/calendar/events/{id}calendar:writeDelete an event.
response
{ "id": "evt_9m4", "deleted": true }

Webhooks

Push instead of poll: register HTTPS endpoints and receive signed events. Retries with exponential backoff for 3 days, then the endpoint auto-disables (you get an email).

GET/webhookswebhooks:manageList webhook endpoints.
response
{ "data": [{ "id": "whk_2d1", "url": "https://yourco.com/hooks/aiemaily", "events": ["message.received"], "status": "active" }] }
POST/webhookswebhooks:manageRegister an endpoint. The signing secret is returned once.
request
{ "url": "https://yourco.com/hooks/aiemaily", "events": ["message.received", "send.completed"] }
response
{
  "id": "whk_2d1",
  "url": "https://yourco.com/hooks/aiemaily",
  "events": ["message.received", "send.completed"],
  "secret": "whsec_9a81c2...  // shown once — store it now",
  "status": "active"
}
POST/webhooks/{id}/rotate-secretwebhooks:manageRotate the signing secret (old secret stays valid 24h).
response
{ "id": "whk_2d1", "secret": "whsec_new...", "previous_valid_until": "2026-07-11T15:00:00Z" }
DELETE/webhooks/{id}webhooks:manageDelete an endpoint.
response
{ "id": "whk_2d1", "deleted": true }

Usage

Quota and credit visibility, so integrations can pace themselves instead of hitting 429s.

GET/usageusage:readCurrent period consumption and limits.
response
{
  "plan": "autopilot",
  "requests": { "used": 1240, "limit": 20000, "resets": "2026-07-11T00:00:00Z" },
  "sends": { "used": 12, "limit": 1000 },
  "ai_credits": { "used": 46, "limit": 1000, "byok": false }
}

Webhooks

Events, signing, and verification

Register an HTTPS endpoint, choose events, verify signatures. Deliveries retry with exponential backoff for 3 days; a persistently failing endpoint auto-disables and you're emailed.

Event catalog

message.receivedA new message lands in any connected mailbox (post-triage, so tags are populated).
payload
{
  "event": "message.received",
  "created_at": "2026-07-10T14:22:03Z",
  "data": {
    "thread_id": "thr_9f2ka81",
    "message_id": "msg_77xk1",
    "account_id": "acc_71b2",
    "from": { "email": "[email protected]" },
    "subject": "Contract renewal — need your sign-off by Friday",
    "tags": ["important", "client"]
  }
}
thread.updatedThread state changes: archived, labeled, snoozed, read-state.
payload
{ "event": "thread.updated", "data": { "thread_id": "thr_9f2ka81", "changes": { "state": "archived" } } }
send.completedA send left the outbox successfully (after the undo window).
payload
{ "event": "send.completed", "data": { "send_id": "snd_1vv92", "thread_id": "thr_9f2ka81", "message_id": "msg_81aa2" } }
send.failedA send failed after retries (provider rejection, auth failure).
payload
{ "event": "send.failed", "data": { "send_id": "snd_1vv92", "reason": "provider_rejected", "detail": "550 mailbox unavailable" } }
draft.createdThe agent drafted a reply and is holding it for review (Copilot).
payload
{ "event": "draft.created", "data": { "draft_id": "drf_8s31x", "thread_id": "thr_9f2ka81", "confidence": 0.93 } }
agent.actionAny autonomous agent action: triaged, queued, sent, escalated.
payload
{ "event": "agent.action", "data": { "kind": "queued", "thread_id": "thr_2231a", "confidence": 0.95, "undo_until": "2026-07-10T15:09:00Z" } }
brief.readyThe daily Living Brief finished generating.
payload
{ "event": "brief.ready", "data": { "date": "2026-07-10", "needs_reply_count": 4 } }

Verify every delivery

Each delivery carries X-Aiemaily-Signature: t=<unix-ts>,v1=<hmac> — an HMAC-SHA256 of ${t}.${rawBody} with your endpoint secret. Verify the signature, reject stale timestamps, and always compare in constant time.

verify.ts · Node
import crypto from "node:crypto";

// X-Aiemaily-Signature: t=1760107330,v1=5257a869e7...
export function verifyAiemailySignature(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  const ts = Number(parts.t);
  // Reject stale deliveries (replay protection)
  if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  try {
    return crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(parts.v1, "hex"));
  } catch {
    return false;
  }
}

SDKs

Official SDKs, generated from the spec

TypeScript and Python clients with typed requests, typed errors, automatic retries with backoff, and pagination helpers. Or generate your own from the OpenAPI document.

@aiemaily/sdk · npm
npm install @aiemaily/sdk

import { Aiemaily } from "@aiemaily/sdk";

const aiemaily = new Aiemaily({ apiKey: process.env.AIEMAILY_API_KEY });

// List what needs attention
const { data: threads } = await aiemaily.threads.list({ state: "inbox", unread: true });

// Semantic search
const { data: hits } = await aiemaily.search({ query: "invoices from June", mode: "semantic" });

// Draft in the user's voice, then send explicitly
const draft = await aiemaily.drafts.create({
  replyToThreadId: hits[0].threadId,
  useAi: true,
  instructions: "Confirm receipt, promise payment by Friday",
});
const send = await aiemaily.drafts.send(draft.id, { confirm: true });

// Errors are typed: err.code === "rate_limited", err.retryAfter, err.requestId

Errors

Error codes

Stable codes, shared with the MCP server. Match on error.code, never on message text.

Error codes for the AI Emaily API
CodeHTTPMeaningWhat to do
unauthorized401Missing, expired, or revoked token / API key.Re-run the OAuth flow in your client, or issue a new API key in Settings → Developer.
forbidden_scope403The token lacks the scope this tool or endpoint requires.Reconnect and grant the scope on the consent screen. Tools you lack scopes for are hidden from the tool list.
invalid_params400Parameters failed validation (missing field, wrong type, confirm not true on a send).The error message names the failing param. Check types against the tool/endpoint reference.
not_found404Thread, draft, or contact id doesn't exist or isn't yours.Ids are user-scoped — re-fetch with list_inbox or search_email.
rate_limited429Per-key, per-user, or per-IP request rate exceeded.Honor Retry-After. Call get_usage to see remaining quota before batches.
quota_exceeded429A daily quota is exhausted — the request or send cap for the plan.Caps reset daily (UTC). Check get_usage; contact support for higher limits.
insufficient_credits402AI credits for the period are used up (metered plans).Buy a top-up, wait for the reset, or switch to BYOK (uncapped).
plan_required402The feature needs a higher plan — e.g. MCP requires Autopilot or Team.Upgrade at aiemaily.com/pricing, or ask your team owner to add a seat.
internal_error500Something failed on our side.Safe to retry once. Every response carries a request_id — include it when contacting support.

Rate limits & plans

Quotas by plan

Daily request and send caps set by plan, plus a per-minute burst ceiling enforced per key (120/min), per user (300/min), and per IP (300/min). Every response carries X-RateLimit-Limit / -Remaining / -Reset; 429s include Retry-After.

API rate limits by AI Emaily plan
PlanAPI accessRequests / daySends / dayWebhook endpoints
Free
Pro · $20/mo✓ core scopes5,0002002
Autopilot · $40/mo✓ all scopes + MCP20,0001,00010
Team · from $25/seat✓ org keys + MCPPooled per seatPooled20
  • AI endpoints (/ai/*, /brief/generate, use_ai drafting, semantic search) additionally spend plan AI credits. BYOK plans are uncapped.
  • New keys run at reduced send caps for their first 24 hours (abuse protection).
  • Need more? Talk to us about higher limits.

FAQ

Frequently asked questions

What is the AI Emaily API?

A versioned REST API (api.aiemaily.com/v1) that exposes your unified inbox — Gmail, Outlook/Microsoft 365, and any IMAP provider — plus AI Emaily's AI layer (semantic search, voice-matched drafting, the Living Brief, the autonomous agent) to your own scripts, backends, and automation tools.

How do I get an API key?

Settings → Developer → API keys in the AI Emaily app (Pro plan or higher). Keys are scope-fixed at creation, shown once, and stored only as a hash. Use one narrowly-scoped key per integration.

Which plans include API access?

Pro ($20/mo) includes the REST API with core scopes. Autopilot ($40/mo) adds agent scopes and the MCP server. Team (from $25/seat) adds org keys and pooled quotas. The Free plan has no API access.

Is there an OpenAPI spec?

Yes — the machine-readable OpenAPI 3.1 document lives at api.aiemaily.com/v1/openapi.json and is the source of truth for the whole contract. The TypeScript and Python SDKs are generated from it, and you can generate clients for any other language.

How do webhooks stay secure?

Every delivery is signed: the X-Aiemaily-Signature header carries a timestamp and an HMAC-SHA256 of the payload using your endpoint's secret. Verify the signature and reject stale timestamps (older than 5 minutes) to prevent replay. Secrets rotate with a 24-hour dual-validity window.

What happens if I exceed a rate limit?

You get a 429 with a Retry-After header. A per-minute burst ceiling (rate_limited) applies per key, per user, and per IP; daily request and send caps (quota_exceeded) are set by plan. Check GET /v1/usage (or the X-RateLimit headers on any response) to pace batch jobs.

Can the API bypass the app's safety settings?

No. The API resolves to the same ownership-scoped enforcement core as the app and the MCP server: sends require explicit confirmation and count against daily caps, agent runs respect your authority modes and sender allowlists, and every mutation is audited.

Is my email content used to train models?

Never. Zero-retention agreements with model providers and no training on user mail — the same privacy policy that governs the app covers every API and MCP call.

Versioning & changelog

A contract you can build on

Major version in the URL (/v1). Additive changes — new endpoints, new optional fields — ship continuously and never break clients. Breaking changes get /v2 and at least 6 months of dual-running deprecation notice, announced here and by email to every active key owner.

v1.0.0

2026 — GAcurrent
  • Initial public release: 21 tools across read, search & AI, organize, draft & send, and context & insight.
  • Streamable-HTTP JSON-RPC at https://mcp.aiemaily.com, negotiating MCP protocol 2025-11-25; tools carry annotations (readOnly / destructive / idempotent hints).
  • OAuth 2.1 with PKCE and dynamic client registration — the consent screen shows a verified/unverified badge and the redirect host, and gates privileged scopes; API-key bearer (aiem_live_…) for headless clients.
  • ChatGPT-compatible search and fetch tools, so deep research and company-knowledge connectors work out of the box.
  • Two-step confirm-gated sending with server-side undo window; untrusted-content fencing on all email-derived output.
  • Per-key, per-user, and per-IP rate limits, daily request/send caps, and full audit logging.

v0.9.0

2026 — private beta
  • Design-partner beta over the v1 REST API.
  • Added get_attachment_text and list_agent_actions based on beta feedback.
  • Consent screen rewritten in product language (scope → plain English).

Build on your inbox.

Create a key, make a call, ship something. API access starts on Pro; agents get the MCP server on Autopilot.