Introduction
Agent frameworks
Each guide connects one agent framework to the hosted mails.ai MCP server and has the agent send a first email to our test address.
Sending from your own domain
Until you add a domain of your own, your agents’ mail goes out from mails.ai’s own sending domain. Adding a domain needs an active paid plan: without one, POST /v1/domains answers 402 with the code feature_not_enabled and an upgrade_url. Where custom domains are not available at all, every domain route answers 400 with the same code.
Which DNS records do I add?
The ones in the domain’s dns_records, which POST /v1/domains returns and GET /v1/domains/{id} shows again. Each record has a type, the host to create, the exact value to copy, its purpose, and whether it is required. The records are made for your domain, so copy them from the response rather than from an example.
- DKIM is always required. mails.ai signs your mail with it, and DMARC passes on that signature.
- SPF, when it is listed, is optional: DMARC passes on the DKIM signature. If your domain already has an SPF record, add to it rather than replacing it: replacing it breaks the other mail your domain sends.
- DMARC, when it is listed, is optional too.
- A return-path record, when it is listed, is required, and a link-tracking record is optional.
Only required records hold up verification; the optional ones improve deliverability or reporting.
How does verification work?
A new domain is pending. POST /v1/domains/{id}/verify checks every record now and marks each one verified or not. Until every required record matches, the domain is verifying, and fail_reason names each required record that is missing or has the wrong value.
You do not have to keep calling it. About every 15 minutes, mails.ai re-checks domains that are still pending or verifying, and verifies each one as soon as its required records resolve. That covers domains added in the last 30 days, and every domain of a workspace with an active paid subscription, or one the Billing page marks Payment failed.
After 20 calls to POST /v1/domains/{id}/verify in a row that did not pass, the domain is failed. That is not final: on the same terms, a failed domain is still re-checked automatically, less often the longer it fails (at most a day apart), and verifies by itself once its records resolve. A call still checks at once, and the count starts over on success or after a day without a call.
Which agents use the domain?
Agents whose address is on the verified domain send under its own identity. Create one with POST /v1/agents and domain set to the verified domain; a domain that is not verified for your workspace answers 400 domain_not_verified.
What if a record disappears later?
Once a day, mails.ai re-checks every verified domain. If a required record has gone, the domain moves back to verifying, and sends from its agents are refused with 422 domain_not_verified, naming what is missing, until it verifies again.
Bounces and suppression
A bounce marks the message bounced and emits message.bounced with the recipient, the bounce_type (Permanent or Undetermined) and, when the receiving server gave one, its diagnostic_code. A Transient bounce, a soft one after which the mail may still be delivered, is not counted as a bounce: the message is marked delayed and emits message.delayed, which reaches only an endpoint that lists it by name. With no delivery confirmed within 5 days of sending, it becomes bounced.
Only a Permanent bounce suppresses the address, and only when it was a recipient of that message. That suppression covers every workspace on mails.ai, because the mailbox does not exist. suppression.added reports it with a hash of the address rather than the address itself.
What gets an address suppressed?
- A permanent bounce, for every workspace.
- A spam complaint, for every workspace too: a complaint hurts the sending reputation that customers share.
- An unsubscribe through the unsubscribe link in your mail, for your workspace only. The person can still hear from other mails.ai customers they signed up with.
What happens when I send to a suppressed address?
If an address in to, cc or bcc is suppressed, the whole send is refused with 422 recipient_suppressed, which names the reason and its scope (and param the field), and nothing is sent. GET /v1/suppression with ?address= tells you an address’s status before you send.
Can I send to a suppressed address anyway?
After a complaint or an unsubscribe, yes. POST /v1/suppression/allow records an attestation (at least 20 characters) that the person wants your mail, and your workspace can send to them again; DELETE /v1/suppression/allow/{id} withdraws it. A permanent bounce cannot be overridden, and there is no route that deletes a suppression.
Complaints and sending reputation
A spam complaint marks the message complained, emits message.complained, and suppresses the address for every workspace.
How is reputation scored?
Each agent has a reputation score from 0 to 1, taken from its last 30 days of live sending: bounces and complaints pull it down, and replies lift it. Test sends do not count. GET /v1/reputation returns the workspace figures, or one agent’s with agent_id.
When is an agent paused?
Once an agent has made at least 20 live sends in 30 days, it is paused when more than 5% of them bounced or more than 0.3% drew a complaint. It resumes by itself when the rates drop below 3% and 0.2%.
The same rates are checked across the whole workspace, over 30 days (from 50 sends) and over the last 24 hours (from 20 sends). A workspace that crosses them has all of its agents paused, and they resume by themselves once its rates recover.
A paused agent’s sends are refused with 422 agent_paused, which gives the reason. You can resume an agent you paused yourself with PATCH /v1/agents/{id} and "status": "active". For any other pause, that call answers 403 agent_suspended: a pause for the agent’s own rates or the workspace’s lifts by itself once they recover, support can lift it sooner, and only support can lift a pause from abuse prevention.
Prompt-injection scores
Every inbound message within your plan’s limits gets an injection_score from 0 to 1: how strongly its subject and body read as an attempt to steer your agent, such as overriding its instructions, taking over its role, getting it to send data out, or making it call a tool for the sender. The kinds that were flagged are listed in injection_categories.
The score is on the inbound event (message.received, reply.received or message.received.unauthenticated), its webhook delivery and the received message itself (injection_score and quarantined, both null for mail that was never scanned).
What does quarantined mean?
At 0.95 or above, the event is marked quarantined: true and the received message’s parse_status is quarantined. The message is still delivered, with its event and webhook: it is never dropped, so a mistaken score cannot lose mail. Have your agent skip quarantined mail rather than act on it.
Is every message scanned?
Every inbound message is scored, on every plan, replies included. The one exception is mail kept while the workspace was over a limit (see Limits), which is never scanned. classify_inbound, a paid-plan setting on the agent, only adds intent and entity extraction on top.
Mail from a forged sender is scanned too, and also carries the category auth_fail and a score of at least 0.4. It arrives as message.received.unauthenticated: its From domain publishes a DMARC policy of quarantine or reject, and no DKIM signature aligned with that domain verifies. SPF is not checked.
If the scan cannot run, the message is still delivered, with a score of 0.5 and the category scan_unavailable, so a careful agent can hold it.
Cold email
mails.ai carries transactional, recipient-initiated mail only. Every send is classified before it goes out, and a message judged to be cold outreach, bulk mail or marketing is refused with 422 cold_email_prohibited. Nothing is sent, and the error message names the verdict and its confidence.
What passes, and what does not?
The refusal fires when a message pitches a product or service to someone who did not ask for it, or reads as bulk or marketing mail. A first message to someone who has never written to you is judged more strictly. Mail that passes is a reply to a message the recipient sent you, or a confirmation, receipt, notification, reminder or verification addressed to someone who just took an action.
Can I check a message without sending it?
Send it with a test key. It is classified the same way and nothing is transmitted; a cold verdict comes back as classifier_warning on the response instead of a refusal.
What if the verdict is wrong?
POST /v1/messages/appeal with the same agent, subject and body asks for a second, independent review under the same policy. overturned clears exactly that content for 24 hours, upheld means the refusal stands, and not_appealable means the block is fixed policy that no review can change. Each workspace can appeal 10 times in 24 hours.
Re-sending content that was just refused gets the same refusal without a new review for an hour, unless an appeal has cleared it.
Test keys and the sandbox
A test key (mk_test_…) runs the whole API without sending real mail. A send is stored and fires message.sent, with its event and webhook, but nothing is transmitted and nothing is billed. Test sends also:
- do not count toward your plan’s send caps or your reputation, and are never refused by the per-key burst limit;
- skip the suppression check, so you can test-send to any address;
- get a cold verdict back as
classifier_warninginstead of a refusal.
Some requests need live mode. A test key, or a dashboard session sending X-Mails-Mode: test, gets 403 live_mode_required when it trashes or deletes a thread that holds live mail, changes, sends or deletes a draft a live key made, reschedules or cancels a live scheduled send, or runs an agent’s forwarding test, which sends a real email. It also gets it when it revokes a live key, or changes, pauses or archives an agent that has a live key tied to it, live mail (sent, scheduled or received) or a draft a live key made; do it with a live key or from the dashboard.
How do I test receiving?
Send to reply@test.mails.ai. Nothing is transmitted: moments later a reply arrives at the sending agent through the real inbound path, so you get reply.received with the same scan, threading and webhook as real mail. It also works with a live key, where the send still counts toward your send caps. The reply to a live send counts toward your inbound caps; the reply to a test-key send counts toward none. It answers at most 3 times in one thread: a 4th message to it there gets no reply.
reply@test.mails.ai on its own to test.What happens with another address at test.mails.ai?
It is refused, because reply@test.mails.ai is the only address there. A send, reply, forward or draft send that names any other test.mails.ai address answers 422 unknown_test_address, with a live key or a test key, and nothing is sent. param names the field that held it (to, cc or bcc). In a batch, the item that names one is refused with that error in its result. A scheduled send that comes due with one fails the same way.
{
"error": {
"type": "invalid_request_error",
"code": "unknown_test_address",
"message": "x@test.mails.ai (to) does not exist, so nothing was sent. The one test address is reply@test.mails.ai, which answers what you send it.",
"param": "to"
}
}To test first-contact mail, an unauthenticated sender or attachments, use POST /v1/test/inbound. It takes a test key only (a live key gets 400 test_mode_required), runs the real scan, threads the message and emits the same event and webhook as real inbound, and is never billed. Set spf and dkim to fail for an unauthenticated sender, or send a whole email as raw_base64 (up to 3 MB) to test attachments.
Webhook delivery and retries
Each delivery is a POST of the event as JSON. X-Mails-Signature carries t=<unix>,v1=<hex>: an HMAC-SHA256 of <t>.<raw body>, keyed with your endpoint’s signing secret. Verify it against the raw body before parsing, and reject a timestamp more than 300 seconds old, as the SDKs’ verify functions do.
What counts as delivered?
A 2xx answer within 5 seconds. Any other status, a timeout or a connection error is a failed attempt. Up to 3 redirects are followed, and each address is checked the same way as the first.
How are failures retried?
Each delivery gets 3 attempts: the first right away, the second at least 30 seconds after the first fails, and the third at least 2 minutes after the second. After the third there are no more automatic attempts. GET /v1/webhooks/{id}/deliveries shows every attempt, and POST /v1/webhook-deliveries/{id}/replay sends one again once your endpoint is back.
A retry can bring an event you already handled, so dedupe on X-Mails-Event-Id.
Which events does * include?
Every event except thread.created, thread.updated, thread.deleted and message.delayed. An endpoint receives those only when it lists them by name in event_types.
How do I rotate the signing secret?
POST /v1/webhooks/{id}/rotate-secret returns a new secret, once. For the next 24 hours each delivery carries two v1 signatures, one from each secret, so accept the header when any v1 matches. Rotating again within those 24 hours retires the oldest secret.
Idempotency
Sending a message, a batch, a reply, a forward or a draft takes an optional Idempotency-Key header. Give each new request its own key, and reuse the key when you retry that request.
- Repeating a request that succeeded returns the stored response with
Idempotent-Replay: true, and nothing is sent again. Keys are kept for 24 hours, per workspace. - The same key with a different body or route answers
400invalid_idempotency_key. - The same key while the first request is still running answers
409duplicate_resource: retry shortly. - A request that failed before the mail went out, and before a scheduled send was stored, keeps nothing on its key, so retrying it with the same key runs it again. So does a batch that stored no message and sent no mail, though it answered
201. One that failed after the mail was accepted for delivery keeps its error: a retry gets that error back withIdempotent-Replay: true, and the mail is not sent twice. One that failed after a scheduled send was stored keeps a201answer naming that message instead: a retry gets that answer withIdempotent-Replay: true, nothing is scheduled twice, and the retry raises the message’smessage.scheduledevent if the failed request did not.
Limits
Plan send caps, payload sizes and the codes for going over them are on Limits. These limits apply as well:
- Burst. Each live API key can make 100 requests per 10 seconds to the sending routes: send, batch, reply, forward, sending a draft, and appeals. Past that it gets
429rate_limit_exceededwithRetry-After, and the responses carryRateLimit-*andX-RateLimit-*headers. - New recipients. A workspace can reach only so many distinct new recipients per 24 hours: at first 25 on Free, 100 on Pro and 250 on Scale. The allowance grows with the workspace’s age, to 4 times as many after 7 days, 12 times after 30 days and 40 times after 90 days, but never above the plan’s daily send cap. Past it, a send to a new address answers
429new_workspace_fanout_exceeded; mailing an address you already mailed in the last 24 hours is never limited. - Received mail has caps of its own.
| Plan | Received per hour | Per day | Per month |
|---|---|---|---|
| Free | 120 | 1,000 | 3,000 |
| Pro | 2,000 | 20,000 | 50,000 |
| Scale | 10,000 | 100,000 | 500,000 |
Mail over the monthly cap, or that arrives while the subscription is canceled or paused, is stored but not scanned, and raises message.received.over_limit, which carries no subject or body. Mail over the hourly or daily cap is not stored: up to 20 an hour are recorded without their content and raise the same event, and the rest are dropped. * includes that event.
AI tools
Can an agent read these docs?
Every page here is also plain Markdown: add .md to its URL, or use Copy page at the top of the page. /docs/llms.txt lists every page as a Markdown link, so an agent can read the docs without the HTML.
Which tools can an agent use?
The MCP server gives Claude, Cursor, VS Code and other AI clients email tools, and the agent skills are instructions an agent loads when it works with mails.ai.
Go deeper
The reasoning behind the API surface — architecture, patterns, and the security model — lives on the blog and glossary.
Next steps
Was this page helpful?