---
title: "Quickstart"
description: "Sign up with a magic link, get an API key, create an inbox, send an email and receive replies by polling, webhook and Server-Sent Events."
canonical: "https://machinemail.dev/docs/getting-started/quickstart"
---

# Quickstart

> Sign up with a magic link, get an API key, create an inbox, send an email and receive replies by polling, webhook and Server-Sent Events.

Source: https://machinemail.dev/docs/getting-started/quickstart

This walks through the whole loop with curl, TypeScript (`fetch`) and Python (`requests`). It takes about five minutes.

## 1. Sign up and get an API key

Go to [machinemail.dev/signup](/content/signup/index.html), enter your email address and, optionally, an organization name. MachineMail emails you a sign-in link. There are no passwords.

Each mailbox can have one MachineMail account. Gmail addresses with the dots moved (or on `googlemail.com`) and `+` aliases such as `you+test@example.com` count as the same mailbox as `you@example.com`. If your mailbox already has an account, sign in with the address you first used.

Open the link within 15 minutes. It works once. On your first sign-in MachineMail creates your organization, its default pod, its [default inbox](/content/docs/concepts/organizations-and-pods#the-default-inbox/index.html) (`{slug}@machinemail.dev`) and a first API key.

Right after you land, the dashboard shows your first key in a **Welcome. Save your key** dialog. This is the only time the full key is shown. Choose **Copy**, store the key, then choose **I've saved it**. If you lose it, revoke it and create another under [API keys](/content/app/api-keys/index.html). You can create more keys there at any time. [Create a key for an agent](/content/docs/guides/api-key-for-an-agent/index.html) shows how.

Keep the key in an environment variable:

```bash title="Terminal"
export MACHINEMAIL_API_KEY=mm_live_...
```

> **Warning:** An API key is shown only once and only its hash is stored. A key created at the top level can do everything in your organization. For agents, create narrower keys scoped to a pod or an inbox, with a permission list. See [API keys](/content/docs/concepts/api-keys/index.html).

Check that the key works:

```bash title="Terminal"
curl https://api.machinemail.dev/v0/organizations \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

## 2. Create an inbox

Your organization already has its default inbox. Create one per agent: an inbox is an email address. Pick a `username`, or leave it out and MachineMail generates one. Without a `domain`, the inbox is created on the shared `machinemail.dev` domain.

**curl**

```bash title="Terminal"
curl https://api.machinemail.dev/v0/inboxes \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"username": "support-agent", "display_name": "Support agent"}'
```

**TypeScript**

```ts title="create-inbox.ts"
const API = 'https://api.machinemail.dev/v0';
const headers = {
  Authorization: `Bearer ${process.env.MACHINEMAIL_API_KEY}`,
  'Content-Type': 'application/json',
};

const res = await fetch(`${API}/inboxes`, {
  method: 'POST',
  headers,
  body: JSON.stringify({ username: 'support-agent', display_name: 'Support agent' }),
});
const inbox = await res.json();
console.log(inbox.inbox_id); // support-agent@machinemail.dev
```

**Python**

```python title="create_inbox.py"
import os
import requests

API = "https://api.machinemail.dev/v0"
headers = {"Authorization": f"Bearer {os.environ['MACHINEMAIL_API_KEY']}"}

res = requests.post(
    f"{API}/inboxes",
    headers=headers,
    json={"username": "support-agent", "display_name": "Support agent"},
)
res.raise_for_status()
inbox = res.json()
print(inbox["inbox_id"])  # support-agent@machinemail.dev
```

```json title="Response (201)"
{
  "inbox_id": "support-agent@machinemail.dev",
  "organization_id": "org_01m4fy8h140000efvagszh6rpx",
  "pod_id": "pod_01m4fy8h150000w4xty1ehfnqa",
  "email": "support-agent@machinemail.dev",
  "display_name": "Support agent",
  "status": "active",
  "ephemeral": false,
  "created_by": "api",
  "created_at": "2026-10-09T09:00:15.835Z",
  "updated_at": "2026-10-09T09:00:15.835Z"
}
```

The `inbox_id` is the address. Use it in every inbox path. Addresses on the shared domain are first come, first served: if `support-agent` is taken you get `409 conflict`, so choose something more specific. `GET /v0/inboxes/availability?username=support-agent` tells you before you try.

> **Note:** Want an address that cleans up after itself? Add `"ttl_seconds": 3600` and the inbox, with its messages, is deleted an hour after it was created or last received mail, whichever is later. See [Inboxes](/content/docs/concepts/inboxes/index.html).

## 3. Send an email

Send to an address you can read, so you can reply in the next step.

**curl**

```bash title="Terminal"
curl https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/send \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["you@example.com"],
    "subject": "Hello from an agent",
    "text": "Reply to this email and I will see it."
  }'
```

**TypeScript**

```ts title="send.ts"
const inboxId = 'support-agent@machinemail.dev';

const res = await fetch(`${API}/inboxes/${inboxId}/messages/send`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    to: ['you@example.com'],
    subject: 'Hello from an agent',
    text: 'Reply to this email and I will see it.',
  }),
});
const sent = await res.json();
console.log(sent.message_id, sent.thread_id);
```

**Python**

```python title="send.py"
inbox_id = "support-agent@machinemail.dev"

res = requests.post(
    f"{API}/inboxes/{inbox_id}/messages/send",
    headers=headers,
    json={
        "to": ["you@example.com"],
        "subject": "Hello from an agent",
        "text": "Reply to this email and I will see it.",
    },
)
res.raise_for_status()
sent = res.json()
print(sent["message_id"], sent["thread_id"])
```

```json title="Response (201)"
{
  "message_id": "msg_01m4fy8h340000kzb2kp66c161",
  "thread_id": "thd_01m4fy8h340001pp6c74x4688h",
  "timestamp": "2026-10-09T09:00:15.844Z"
}
```

The email goes out from `Support agent <support-agent@machinemail.dev>`. You can also send `html`, `cc`, `bcc`, `reply_to`, `attachments` and custom `headers`. See [Sending](/content/docs/concepts/sending/index.html).

## 4. Receive the reply

Reply to the email from your own mail client. MachineMail threads the reply onto the message you sent, computes a trust report and emits a `message.received` event. Pick one of three ways to find out about it.

### Option A: poll

The simplest option. List messages newer than a timestamp, then fetch the full message, which includes the body and the trust report.

**curl**

```bash title="Terminal"
curl "https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages?labels=INBOX&after=2026-10-09T09:00:00Z" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"

curl https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/msg_01m4fy8han0000y3ge3m73vw3w \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

**TypeScript**

```ts title="poll.ts"
const since = new Date().toISOString();

async function waitForReply(): Promise<any> {
  for (;;) {
    const url = `${API}/inboxes/${inboxId}/messages?labels=INBOX&after=${encodeURIComponent(since)}`;
    const { messages } = await (await fetch(url, { headers })).json();
    if (messages.length) {
      const id = messages[0].message_id;
      return (await fetch(`${API}/inboxes/${inboxId}/messages/${id}`, { headers })).json();
    }
    await new Promise((r) => setTimeout(r, 5000));
  }
}

const message = await waitForReply();
console.log(message.trust.level, message.trust.content.text);
```

**Python**

```python title="poll.py"
import time
from datetime import datetime, timezone

since = datetime.now(timezone.utc).isoformat()

def wait_for_reply():
    while True:
        res = requests.get(
            f"{API}/inboxes/{inbox_id}/messages",
            headers=headers,
            params={"labels": "INBOX", "after": since},
        )
        messages = res.json()["messages"]
        if messages:
            msg_id = messages[0]["message_id"]
            return requests.get(f"{API}/inboxes/{inbox_id}/messages/{msg_id}", headers=headers).json()
        time.sleep(5)

message = wait_for_reply()
print(message["trust"]["level"], message["trust"]["content"]["text"])
```

The `labels=INBOX` filter skips your own sent messages, which carry `SENT` instead. `after` compares against the message timestamp, which for inbound mail comes from the sender's `Date` header, so leave a minute of margin for clock skew. Lists are newest first and leave out spam and quarantined mail unless you ask for it with `include_spam` or `include_quarantine`.

The full message looks like this (trimmed):

```json title="GET /v0/inboxes/{inbox_id}/messages/{message_id}"
{
  "message_id": "msg_01m4fy8han0000y3ge3m73vw3w",
  "inbox_id": "support-agent@machinemail.dev",
  "thread_id": "thd_01m4fy8h340001pp6c74x4688h",
  "labels": ["INBOX", "UNREAD"],
  "from": "you@example.com",
  "to": ["support-agent@machinemail.dev"],
  "subject": "Re: Hello from an agent",
  "text": "Thanks, got it.\n\nOn Fri, Oct 9, 2026 at 9:00 AM Support agent wrote:\n> Reply to this email and I will see it.\n",
  "extracted_text": "Thanks, got it.",
  "direction": "inbound",
  "trust_level": "verified",
  "trust": {
    "level": "verified",
    "score": 0.97,
    "authentication": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "aligned": true },
    "sender": {
      "address": "you@example.com",
      "domain": "example.com",
      "prior_correspondence": true,
      "on_allow_list": false,
      "on_block_list": false,
      "free_mail_provider": false
    },
    "content": {
      "text": "Thanks, got it.",
      "segments": [
        { "kind": "body", "text": "Thanks, got it." },
        { "kind": "quote", "text": "On Fri, Oct 9, 2026 at 9:00 AM Support agent wrote:\n> Reply to this email and I will see it." }
      ],
      "hidden_text_found": false,
      "zero_width_found": false,
      "tracking_pixels": 0
    },
    "links": [],
    "attachments": [],
    "classification": {
      "is_spam": 0.02,
      "is_phishing": 0.02,
      "contains_instructions_to_agent": 0.02,
      "requests_credentials_or_payment": 0.02,
      "urgency": 0.03,
      "category": "conversation",
      "language": "en",
      "backend": "heuristic"
    },
    "reasons": ["Verified: DMARC passed and aligned, and you have written to this address before"],
    "computed_at": "2026-10-09T09:02:41.512Z"
  }
}
```

Give your model `trust.content.text`, not `text` or `html`. It is the new part of the message with quoted history, signatures, hidden text and zero-width characters removed. [The trust report](/content/docs/trust/trust-report/index.html) explains every field.

When you have handled a message, mark it read by removing the `UNREAD` label:

```bash title="Terminal"
curl -X PATCH https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/msg_01m4fy8han0000y3ge3m73vw3w \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"remove_labels": ["UNREAD"]}'
```

### Option B: webhook

Register an HTTPS endpoint and MachineMail POSTs every matching event to it, signed, with retries.

**curl**

```bash title="Terminal"
curl https://api.machinemail.dev/v0/webhooks \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/machinemail",
    "event_types": ["message.received"],
    "inbox_ids": ["support-agent@machinemail.dev"]
  }'
```

**TypeScript**

```ts title="webhook.ts"
const res = await fetch(`${API}/webhooks`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    url: 'https://example.com/hooks/machinemail',
    event_types: ['message.received'],
    inbox_ids: [inboxId],
  }),
});
const webhook = await res.json();
// Store webhook.secret: you need it to verify signatures.
```

**Python**

```python title="webhook.py"
res = requests.post(
    f"{API}/webhooks",
    headers=headers,
    json={
        "url": "https://example.com/hooks/machinemail",
        "event_types": ["message.received"],
        "inbox_ids": [inbox_id],
    },
)
webhook = res.json()
# Store webhook["secret"]: you need it to verify signatures.
```

```json title="Response (201)"
{
  "webhook_id": "whk_01m4fy8heb0000jbck9me3gqxb",
  "url": "https://example.com/hooks/machinemail",
  "event_types": ["message.received"],
  "inbox_ids": ["support-agent@machinemail.dev"],
  "secret": "whsec_hbANq3v94BDGbt1lDkzB8Y0tXJNVOSo9",
  "enabled": true,
  "status": "active",
  "created_at": "2026-10-09T09:00:16.203Z",
  "updated_at": "2026-10-09T09:00:16.203Z"
}
```

Each delivery is a JSON `POST`:

```http title="Webhook request"
POST /hooks/machinemail HTTP/1.1
Content-Type: application/json
User-Agent: MachineMail-Webhooks/1.0
X-MachineMail-Signature: t=1791536561,v1=5f0c1e...
X-MachineMail-Event: message.received
X-MachineMail-Event-Id: evt_01m4fy8hba0000449dcj0gmkc5
X-MachineMail-Delivery-Id: dlv_01m4fy8hbb0000a7s1wq0c6m5e

{
  "type": "event",
  "event_id": "evt_01m4fy8hba0000449dcj0gmkc5",
  "event_type": "message.received",
  "timestamp": "2026-10-09T09:02:41.530Z",
  "message": { "message_id": "msg_01m4fy8han0000y3ge3m73vw3w", "trust": { "level": "verified" } },
  "thread": { "thread_id": "thd_01m4fy8h340001pp6c74x4688h", "message_count": 2 },
  "data": { "message": { "...": "..." }, "thread": { "...": "..." } }
}
```

The `message` and `thread` objects appear twice: at the top level, as AgentMail sends them, and inside `data`. Verify `X-MachineMail-Signature` before you trust the body and return a `2xx` quickly. [Verify webhook signatures](/content/docs/guides/verify-webhooks/index.html) has the code in TypeScript, Python and Go.

To try it without waiting for real mail, send a test event: `POST /v0/webhooks/{webhook_id}/test`.

### Option C: Server-Sent Events

Open a long-lived stream and read events as they happen. Good for scripts and workers that cannot receive inbound HTTP.

**curl**

```bash title="Terminal"
curl -N "https://api.machinemail.dev/v0/events/stream?inbox_id=support-agent@machinemail.dev&event_types=message.received" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

**TypeScript**

```ts title="stream.ts"
const url = `${API}/events/stream?inbox_id=${encodeURIComponent(inboxId)}&event_types=message.received`;
const res = await fetch(url, { headers: { Authorization: headers.Authorization } });
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();

let buffer = '';
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;
  let end;
  while ((end = buffer.indexOf('\n\n')) !== -1) {
    const frame = buffer.slice(0, end);
    buffer = buffer.slice(end + 2);
    const data = frame.split('\n').find((l) => l.startsWith('data: '));
    if (!data) continue; // keep-alive comment
    const event = JSON.parse(data.slice(6));
    if (event.type === 'event') console.log(event.event_type, event.message?.subject);
  }
}
```

**Python**

```python title="stream.py"
import json

with requests.get(
    f"{API}/events/stream",
    headers=headers,
    params={"inbox_id": inbox_id, "event_types": "message.received"},
    stream=True,
    timeout=(10, None),
) as res:
    for line in res.iter_lines(decode_unicode=True):
        if line and line.startswith("data: "):
            event = json.loads(line[6:])
            if event.get("type") == "event":
                print(event["event_type"], event["message"]["subject"])
```

```text title="Stream output"
event: ready
data: {"type":"subscribed"}

id: evt_01m4fy8hba0000449dcj0gmkc5
event: message.received
data: {"type":"event","event_id":"evt_01m4fy8hba0000449dcj0gmkc5","event_type":"message.received","message":{...},"thread":{...},"data":{...}}

: ping
```

The stream needs a key with the `metrics_read` permission (unrestricted keys have it). It sends a `: ping` comment every 15 seconds when idle. Reconnect with the `Last-Event-ID` header to catch up on what you missed. [Realtime events](/content/docs/guides/realtime-events/index.html) covers reconnection and the WebSocket alternative.

## 5. Reply in the thread

Reply to the inbound message. MachineMail addresses it to the sender (or their `Reply-To`), sets `Re:` on the subject and adds `In-Reply-To` and `References`, so it lands in the same thread on both sides.

**curl**

```bash title="Terminal"
curl https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/msg_01m4fy8han0000y3ge3m73vw3w/reply \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Got it, thanks."}'
```

**TypeScript**

```ts title="reply.ts"
await fetch(`${API}/inboxes/${inboxId}/messages/${message.message_id}/reply`, {
  method: 'POST',
  headers,
  body: JSON.stringify({ text: 'Got it, thanks.' }),
});
```

**Python**

```python title="reply.py"
requests.post(
    f"{API}/inboxes/{inbox_id}/messages/{message['message_id']}/reply",
    headers=headers,
    json={"text": "Got it, thanks."},
).raise_for_status()
```

## Testing without a mail client

`POST /inbound/raw` delivers a raw RFC 5322 message straight into one of your inboxes, which is handy in tests and CI. It needs a key with the `inbound_raw` permission. Full-access keys do not include it: tick **inbound_raw** when you create the key in the dashboard (or create it with `{"permissions": {"*": true, "inbound_raw": true}}` from a key that already has it). Keep it to test and CI keys.

```bash title="Terminal"
curl https://api.machinemail.dev/inbound/raw \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: message/rfc822" \
  -H "X-Recipient: support-agent@machinemail.dev" \
  --data-binary $'From: Ada <ada@example.com>\r\nTo: support-agent@machinemail.dev\r\nSubject: Test\r\n\r\nYour verification code is 482913.\r\n'
```

Injected messages get `received_via: "raw"`. Any `Authentication-Results` headers in them are ignored: authentication is always `none`, with the reason `Injected via API (/inbound/raw); authentication results not verified`, so injected mail is never `verified` or `known`.

## Next

- [Give an agent an inbox](/content/docs/guides/give-an-agent-an-inbox/index.html): The full receive, check, act and reply loop.
- [OTP codes and sign-ups](/content/docs/guides/otp-and-signups/index.html): Throwaway inboxes for verification codes and magic links.
- [Using trust in agents](/content/docs/trust/using-trust-in-agents/index.html): Rules for what an agent should and should not act on.
- [Connect over MCP](/content/docs/agents/mcp/index.html): Use the same key from Claude Code, Cursor and other MCP clients.
