Documentation
Getting started

Quickstart — send your first email in one call

5 min read

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…  sent

A 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.

Got 422 cold_email_prohibited? mails.ai carries transactional and conversational mail only — cold outreach is refused at the API. Iterate without transmitting anything using an mk_test_ key, and if you believe a refusal is wrong, POST /v1/messages/appeal gets 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.