Developer documentation
Mailient Agent API
Eight endpoints that let an external AI agent work with a Mailient user’s email: read what needs them, read one conversation, draft a reply in their voice, and send a draft they approved.
Mailient stays the security boundary. The agent never receives a Gmail access token, an OAuth refresh token, or any other credential belonging to the user or to Mailient’s integrations. Every Gmail operation happens inside Mailient, under the account the bearer token resolves to.
Base URL
https://mailient.xyz/api/v1HTTPS only. Plain HTTP is redirected before it reaches the application, but a bearer token should never be sent over one — always use the https:// form. The machine-readable contract lives at https://mailient.xyz/api/v1/openapi.json and is public: a connector platform can read it without a credential.
Authentication
One static bearer token per integration. The account owner issues it at Settings → Agent API keys and chooses its scopes there. It is shown once and stored as a hash, so it cannot be recovered afterwards — only replaced.
Authorization: Bearer mak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxuser_id parameter, no email field, no header that names an account. The token resolves to exactly one Mailient user, and that user is the only one the request can reach. A client-supplied identity is not an identity, so we do not accept one.Scopes
| Scope | Grants |
|---|---|
| inbox:read | All six read endpoints. |
| drafts:write | POST /v1/drafts. Saves to Gmail drafts. Cannot send. |
| email:send | POST /v1/send. Off by default on every key — the account owner has to tick it explicitly when issuing the credential. |
Getting started
One request proves the whole chain — credential, subscription, Gmail connection:
curl -s https://mailient.xyz/api/v1/me \
-H "Authorization: Bearer $MAILIENT_KEY"
{
"id": "acct_9f2c…",
"email": "you@example.com",
"name": "You",
"connected_email": "you@example.com",
"gmail_connected": true,
"calendar_connected": true,
"mailbox_status": "ok",
"scopes": ["inbox:read", "drafts:write"]
}gmail_connected is a live answer — Mailient actually reaches the mailbox to produce it, rather than reporting that a connection once existed. When it is false, mailbox_status tells you which problem it is: not_connected (the owner never connected one) or needs_reauth (they did, and it has stopped working). Either way the inbox endpoints will return 409 rather than an empty list — see Empty means empty.
Endpoints
| Method | Endpoint | Scope | Purpose |
|---|---|---|---|
| GET | /v1/me | inbox:read | The Mailient account this key acts as, and whether Gmail is connected. |
| GET | /v1/inbox/priority | inbox:read | Conversations that need the owner, each with a written reason. |
| GET | /v1/inbox/summary | inbox:read | Counts plus Mailient's one-line briefing for the day. |
| GET | /v1/followups | inbox:read | Threads the owner sent that nobody answered. |
| GET | /v1/opportunities | inbox:read | Commercial signals in recent mail, with the phrase that triggered each. |
| GET | /v1/threads/{thread_id} | inbox:read | One conversation in full. Nothing else. |
| POST | /v1/drafts | drafts:write | Write a reply in the owner's voice and save it to Gmail. |
| POST | /v1/send | email:send | Send a draft this API created. Requires Idempotency-Key. |
What the agent asks → what you call
"What emails need my attention?"
GET /v1/inbox/priority?limit=5
"What follow-ups do I owe?"
GET /v1/followups
"Anything commercially important come in?"
GET /v1/opportunities
"Summarise my important email."
GET /v1/inbox/summary (the briefing + counts)
GET /v1/inbox/priority (the items behind them)
"Draft a response to the latest message."
GET /v1/threads/{thread_id} (read what it actually says)
POST /v1/drafts { "thread_id": "...", "instruction": "..." }
"Send that reply."
POST /v1/send { "draft_id": "..." }
Idempotency-Key: <unique per intended send>Reading what matters
curl -s "https://mailient.xyz/api/v1/inbox/priority?limit=3" \
-H "Authorization: Bearer $MAILIENT_KEY"
{
"items": [
{
"thread_id": "18f2a9c4d1e0b7a3",
"message_id": "18f2a9c4d1e0b7a4",
"subject": "Q3 proposal — revised numbers",
"sender": "Priya Shah",
"sender_email": "priya@acme.com",
"reason": "Priya needs the Q3 budget approved before Friday's board call.",
"evidence": ["4th email from her", "deadline is Friday"],
"rank": 1,
"priority": "high",
"needs_action": true,
"received_at": "2026-09-19T14:02:11.000Z",
"gmail_url": "https://mail.google.com/mail/u/0/#all/18f2a9c4d1e0b7a3"
}
],
"count": 1,
"generated_at": "2026-09-20T08:14:03.221Z",
"cached": true,
"message": "1 conversation needs attention, most important first."
}reasonis written by Mailient’s triage model from the message itself — it is the field that lets an agent say why something matters rather than reading out subject lines. rank is the model’s ordering; priorityis a band derived from it (1–3 high, 4–6 medium, 7+ low), not a separate score.
Drafting and sending
curl -s -X POST https://mailient.xyz/api/v1/drafts \
-H "Authorization: Bearer $MAILIENT_KEY" \
-H "Content-Type: application/json" \
-d '{"thread_id":"18f2a9c4d1e0b7a3","instruction":"Confirm Tuesday works and ask for the deck."}'
{
"draft_id": "r-482910385512",
"thread_id": "18f2a9c4d1e0b7a3",
"to": "priya@acme.com",
"subject": "Re: Q3 proposal — revised numbers",
"body": "Hi Priya,\n\nTuesday works…",
"status": "draft",
"voice_match_score": 86,
"gmail_url": "https://mail.google.com/mail/u/0/#drafts/…",
"message": "Draft saved to Gmail. Nothing has been sent."
}
curl -s -X POST https://mailient.xyz/api/v1/send \
-H "Authorization: Bearer $MAILIENT_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4d0f1c7a-1d1e-4e37-9d1a-0b1a2c3d4e5f" \
-d '{"draft_id":"r-482910385512"}'
{ "status": "sent", "message_id": "…", "thread_id": "…",
"to": "priya@acme.com", "subject": "Re: Q3 proposal — revised numbers",
"idempotent_replay": false, "message": "Sent to priya@acme.com." }Permissions and approval
Sending is the only irreversible thing this API does, so it is constrained four ways at once:
- Scope.
email:sendis off by default. A key issued for reading and drafting cannot send, whatever it asks for. - Draft-first. The only thing
/v1/sendaccepts is adraft_idthat/v1/draftsproduced. There is no endpoint that takes a recipient and a body, so an agent cannot email a stranger — and whatever goes out sat in the owner’s Gmail drafts first, where they can read or delete it. - Reply-only addressing.
/v1/draftsresolvestofrom the thread. The caller cannot set it. - Idempotency.
Idempotency-Keyis required on every send.
On top of that, a connector platform normally runs its own approval prompt before a consequential action. That is welcome and expected — but this API does not assume it happened. The four constraints above hold regardless of what the client did.
Idempotency
Send any unique string up to 200 characters as Idempotency-Key, one per intended send. Reuse it for retries of that same send.
- Same key, same body, already finished → the original response is replayed with
idempotent_replay: true. No second email. - Same key, same body, still running →
409 REQUEST_IN_PROGRESS. Retry in a moment. - Same key, different body →
422 IDEMPOTENCY_KEY_REUSED. Replaying would tell you an email went out that never did.
Keys are scoped to your credential and retained for 24 hours.
Errors
Every non-2xx response has the same shape. Branch on error.code, never on error.message — the codes are part of the contract, the prose is not.
{
"error": {
"code": "THREAD_NOT_FOUND",
"message": "The requested thread could not be found.",
"request_id": "req_6f1c…"
}
}| Code | HTTP | Meaning |
|---|---|---|
| MISSING_CREDENTIAL | 401 | No Authorization header. |
| INVALID_CREDENTIAL | 401 | The key is not recognised. |
| CREDENTIAL_EXPIRED | 401 | The key passed its expiry. |
| CREDENTIAL_REVOKED | 401 | The owner revoked it. Ask them for a new one. |
| SUBSCRIPTION_REQUIRED | 402 | The Mailient account has no active subscription. |
| INSUFFICIENT_SCOPE | 403 | The key lacks the scope this operation needs. `required_scope` says which. |
| THREAD_NOT_FOUND | 404 | No such thread in this account. |
| DRAFT_NOT_FOUND | 404 | No such draft in this account. |
| INVALID_REQUEST | 400 | Validation failed. The message names the field. |
| IDEMPOTENCY_KEY_REQUIRED | 400 | POST /v1/send needs an Idempotency-Key header. |
| GMAIL_NOT_CONNECTED | 409 | The owner has not connected a mailbox. |
| GMAIL_REAUTH_REQUIRED | 409 | The mailbox connection needs re-authorising in Mailient. |
| REQUEST_IN_PROGRESS | 409 | An identical send is still in flight. Retry shortly. |
| IDEMPOTENCY_KEY_REUSED | 422 | That key was already used for a different body. |
| RATE_LIMITED | 429 | See `retry_after_seconds`. |
| INTERNAL_ERROR | 500 | Our fault. Quote `request_id`. |
| UPSTREAM_GMAIL_ERROR | 502 | Gmail refused or failed the operation. |
| DRAFT_GENERATION_FAILED | 502 | The writer could not produce a grounded reply. Nothing was saved. |
| AI_UNAVAILABLE | 503 | Mailient's writing service is down. Nothing was saved. |
| CREDENTIAL_STORE_UNAVAILABLE | 503 | Credentials cannot be verified right now. Retry. |
Rate limits
These exist to stop a retry loop turning into thousands of Gmail calls. All but the first are counted per account, not per key, so issuing a second credential does not buy a second allowance.
| Every request, per credential | 120 / minute |
| /v1/inbox/* and /v1/followups | 60 / hour |
| /v1/opportunities | 20 / hour |
| /v1/threads/{id} | 200 / hour |
| /v1/drafts | 40 / hour |
| /v1/send | 20 / hour |
Over a limit returns 429 with retry_after_seconds. The inbox endpoints are backed by a five-minute snapshot cache, so polling them is cheap — the response tells you whether you got a cached snapshot via cached.
Empty means empty
An agent that reports “nothing needs you today” when the truth is “we could not look” is worse than one that fails. So:
- No mailbox connection →
409, never{"items": []}. - Mailient’s triage model unavailable →
/v1/inbox/summaryreturnsdegraded_reasonand null summaries. Say so rather than presenting the fallback labels as analysis. - The writer unavailable →
DRAFT_GENERATION_FAILEDand nothing is saved. Mailient never substitutes a polite template and calls it a draft. /v1/opportunitiesreturnsscan_windowstating exactly what was examined, so an empty list reads as “nothing matched here” rather than “your inbox holds nothing”.
Security model
agent (Muse, or any HTTP client)
│ Authorization: Bearer mak_live_…
▼
Mailient Agent API ── scope · subscription · rate limit · request log
▼
Mailient's own auth + integration layer
▼
Gmail (via the user's existing Mailient connection)- No credential leaves Mailient. Gmail tokens, OAuth refresh tokens and integration secrets are never in a response, an error message or a log line.
- Cross-account access is structural, not filtered. Every Gmail read happens with the calling account’s own credential against its own mailbox, so another account’s thread or draft id is not addressable at all — it returns 404 exactly like an invented one, which also means we never confirm that someone else’s thread exists.
- Keys are stored hashed. A database dump yields no working credential. Revocation is immediate: the next request with a revoked key returns
CREDENTIAL_REVOKED. - Every request is logged — account, endpoint, timestamp, request id, outcome, latency, and for writes the Gmail id that was touched. Never credentials, subjects, bodies or recipient addresses.
- CORS is open, and safe.
Access-Control-Allow-Origin: *with noAllow-Credentials: this API is bearer-only and never reads a cookie, so there is no ambient credential for a cross-site request to abuse. - Personal details are stripped before any model sees them. Drafting runs through Mailient’s existing PII sanitisation, the same as every draft written inside the product.
Versioning
The path carries the version. Within /v1 we may add fields and endpoints; we will not remove a field, change a type, or repurpose an error code. Treat unknown fields as additive and ignore them.
request_id from the response and get in touch.