Quickstart — send your first email in one call
This is the five-minute path from nothing to a delivered email and a structured reply event. Every call here is plain HTTPS against https://api.mails.ai with an Authorization: Bearerheader — no SDK to install, no glue code, and no setup step before the first send. If you prefer a wrapper, the TypeScript SDK and MCP server are live on npm today and wrap exactly this REST surface; the API below is the interface today and the foundation underneath every SDK.
1. Get an API key
Sign in at app.mails.ai (magic link) and mint a key under API keys. Minting is self-serve — no request, no waiting. Keys look like mk_live_… (or mk_test_…for the test mode) and are shown exactly once at creation — store it in an environment variable:
export MAILS_API_KEY="mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Every request authenticates with that key in the standard bearer header. A request with no key gets a structured 401 telling you exactly what is missing — see Authentication for scopes, key modes, and rotation.
2. Send your first email
POST /v1/messages sends one message, and it is the whole setup: if your workspace has no agent yet, this call creates one as it sends. Give it a from, a recipient, a subject, and a text or HTML body — the same request you would write for any email API:
curl https://api.mails.ai/v1/messages \
-H "Authorization: Bearer $MAILS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "sarah@yourapp.com",
"to": "user@example.com",
"subject": "Welcome aboard",
"body_text": "Thanks for signing up — reply any time and I will read it."
}'The same call in TypeScript, using nothing but fetch:
const res = await fetch("https://api.mails.ai/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MAILS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "sarah@yourapp.com",
to: "user@example.com",
subject: "Welcome aboard",
body_text: "Thanks for signing up — reply any time and I will read it.",
}),
});
const message = await res.json();
console.log(message.id, message.status); // msg_01J9… sentA 201 returns the sent message, and cost_usd is what the send cost on your plan:
{
"id": "msg_01J9X2K7Q8ABCDEF...",
"agent_id": "agt_01J9X2K7Q8WERTY...",
"thread_id": "thrd_01J9X2K7Q8...",
"to": ["user@example.com"],
"subject": "Welcome aboard",
"classifier_score": 0.03,
"status": "sent",
"cost_usd": 0.0001,
"created_at": "2026-08-05T17:25:11.004Z"
}Two things to know about that call. from is a naming hint, not a spoofable identity: its local part names the agent that gets created (here sarah), and the mail actually sends from sarah@<your-workspace>.mails.ai until you verify a custom domain. And if the workspace already has exactly one agent, the call simply uses it; with several, pass "agent" to say which.
Got422 cold_email_prohibited? mails.ai carries transactional and conversational mail only — cold outreach is refused at the API. Iterate without transmitting anything using anmk_test_key, and if you believe a refusal is wrong,POST /v1/messages/appealgets you an independent second review.
3. Meet your agent
The send you just made was by an agent— the thing mails.ai is actually about. An agent is a named sending identity that owns an address, a reputation score, optional send limits — and an inbox. Mail sent to its inbound address <name>.<workspace>@in.mails.ai (here sarah.acme@in.mails.ai) lands with your agent, parsed and scored; outbound mail carries that address as its Reply-To, so replies route back automatically. That receive-and-reply half is what a plain send API doesn’t do.
You can also create agents explicitly — to pick names deliberately, run several identities, or set per-agent limits. One field; the name becomes the local part of <name>@<workspace>.mails.ai:
curl https://api.mails.ai/v1/agents \
-H "Authorization: Bearer $MAILS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "sarah" }'Rename the auto-created one any time in the dashboard under Agents— the send history moves with it.
4. Receive the reply
When user@example.com replies, the inbound is parsed into a structured event — with injection_score and sender_reputation on every reply, plus intent and entities if you enable classification on the agent. There are two ways to consume it.
Poll the events feed— simplest to start with:
# A genuine reply to one of your sends emits reply.received.
# (message.received is a cold/first-contact inbound, not a reply.)
curl "https://api.mails.ai/v1/events?event_type=reply.received" \
-H "Authorization: Bearer $MAILS_API_KEY"Or register a webhook so events are pushed to your endpoint as they arrive (HMAC-signed, retried with backoff):
curl https://api.mails.ai/v1/webhooks \
-H "Authorization: Bearer $MAILS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/inbound",
"event_types": ["reply.received", "message.received"]
}'For the live tail — a long-lived stream of every event as it happens — see the events & streaming guide. For verifying webhook signatures, see webhooks.
What you just built
A working agent address that sends real email and turns every reply into a structured, scored event your code can branch on — from one API call, with no setup step. From here:
- Authentication— key scopes (
send/read/manage), test vs live mode, rotation. - Concepts— how workspaces, agents, messages, events, and threads relate.
- API reference— every endpoint, request and response shape, with the exact fields.
- Errors— the error envelope and every code you can get back.
Common questions
Do I need to install an SDK?
No. The mails.ai API is plain HTTPS + JSON — everything in this quickstart is curl and fetch. If you'd rather use a wrapper, the TypeScript SDK (@mailsai/sdk), the Python SDK (mailsai), and the MCP server (@mailsai/mcp-server) are live today. All are thin wrappers over this same REST surface, which is — and always will be — the canonical interface.
Why was my send refused with 422 cold_email_prohibited?
mails.ai carries transactional and conversational mail only — cold outreach is refused at the API, not in the fine print. Messages shaped like unsolicited pitches get a 422 naming the verdict and what a passing message looks like. To iterate without transmitting anything, send with an mk_test_ key (or the composer's Send test). If you believe a refusal is wrong, POST /v1/messages/appeal — an independent reviewer re-judges the exact content under the same policy, and an overturned verdict clears it to send for 24 hours.
Where does the email actually send from?
From your agent's address, <agent>@<workspace-slug>.mails.ai — e.g. sarah@acme.mails.ai. Each agent is a named sending identity inside your workspace with its own address, reputation, and optional per-agent send limits. If you never created one, your first send did: the local part of your `from` names it. That form is the sender identity only: to email an agent, write to <agent>.<workspace-slug>@in.mails.ai (e.g. sarah.acme@in.mails.ai) — the workspace subdomain itself does not receive mail. Custom sending domains are live on paid plans: verify DKIM once and your agents send as you@yourdomain.com.