# 1. Sign up and get an API key

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

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

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

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

```bash
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
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
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
```

Response (201)

```json
{
  "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.

```bash
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
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
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"])
```

Response (201)

```json
{
  "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.

```bash
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
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
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`.

**Note**  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
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.

```bash
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
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
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.
```

Response (201)

```json
{
  "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"
}
```

Every delivery is a JSON `POST`:

Webhook request

```http
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.

```bash
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
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
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"])
```

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

```bash
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
await fetch(`${API}/inboxes/${inboxId}/messages/${message.message_id}/reply`, {
  method: 'POST',
  headers,
  body: JSON.stringify({ text: 'Got it, thanks.' }),
});
```

```python
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).

```bash
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`.
