# Sending a message

Every message is sent from an inbox. MachineMail builds the MIME message, sends it, stores a copy with the `SENT` label, threads it and emits `message.sent`.

## Sending a message

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 '{\n        "to": ["ada@example.com"],\n        "subject": "Hello",\n        "text": "Sent by an agent.",\n        "html": "<p>Sent by an agent.</p>"\n      }'

### send.ts

const inbox = encodeURIComponent('support-agent@machinemail.dev');
    const res = await fetch(`https://api.machinemail.dev/v0/inboxes/${inbox}/messages/send`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.MACHINEMAIL_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        to: ['ada@example.com'],
        subject: 'Hello',
        text: 'Sent by an agent.',
        html: '<p>Sent by an agent.</p>',
      }),
    });
    const { message_id, thread_id } = await res.json();

### send.py

import os, requests

sent = requests.post(
        "https://api.machinemail.dev/v0/inboxes/support-agent@machinemail.dev/messages/send",
        headers={"Authorization": f"Bearer {os.environ['MACHINEMAIL_API_KEY']}"},
        json={
            "to": ["ada@example.com"],
            "subject": "Hello",
            "text": "Sent by an agent.",
            "html": "<p>Sent by an agent.</p>",
        },
    ).json()

### Response

{
      "message_id": "msg_01m4fy8h340000kzb2kp66c161",
      "thread_id": "thd_01m4fy8h340001pp6c74x4688h",
      "timestamp": "2026-10-09T09:00:15.844Z"
    }

The response comes back with `201` once the message has been handed off for delivery. Whether it reached the recipient’s server arrives later as a `message.delivered`, `message.bounced` or `message.complained` event, and as `delivery_status` on the stored message.

## Request fields

| Field | Notes |
| --- | --- |
| `to`, `cc`, `bcc` | A string or an array of addresses. `Name <addr@example.com>` is accepted. At least one recipient is required across the three, and at most 50, each address counted once. |
| `subject` | Defaults to empty. |
| `text` | Plain-text body. |
| `html` | HTML body. Send both `text` and `html` for the best rendering everywhere. |
| `reply_to` | A string or array. Sets the `Reply-To` header so replies go elsewhere. |
| `headers` | Extra headers as a string-to-string map, such as `{ "X-Ticket-Id": "4821" }`. |
| `attachments` | See [Attachments](/content/docs/concepts/sending#attachments/index.html). |
| `labels` | Labels for the stored copy, in addition to `SENT`. |

## From address and display name

The `From` address is always the inbox’s address. You cannot send as another address. If the inbox has a `display_name`, it is used as the sender name:

From: Support agent <support-agent@machinemail.dev>

Change it with `PATCH /v0/inboxes/{inbox_id}` and `display_name`. To send from your own domain, create the inbox on a verified custom domain.

## Threading

A new send starts a new thread unless you pass `In-Reply-To` or `References` in headers that match a message in the inbox. The easier way to continue a conversation is to use the reply endpoints, which set the headers for you.

| Endpoint | Recipients | Subject |
| --- | --- | --- |
| `POST /v0/inboxes/{inbox_id}/messages/{message_id}/reply` | The original’s `Reply-To`, else its sender. For a message you sent, its original recipients. Override with `to`. | `Re:` added unless present. |
| `POST .../{message_id}/reply-all` | Sender plus original `to` and `cc`, minus this inbox. | Same. |
| `POST .../{message_id}/forward` | `to`, `cc` or `bcc` (at least one required). | `Fwd:` added; the original is quoted below your `text`. |

Replies accept `text`, `html`, `to`, `cc`, `bcc`, `reply_to`, `labels`, `attachments` and `headers`.

## Attachments

Each attachment is an object with either base64 `content` or a `url` that MachineMail fetches for you:

{
      "attachments": [
        { "filename": "invoice.pdf", "content_type": "application/pdf", "content": "JVBERi0xLjcK..." },
        { "url": "https://files.example.com/report.csv" },
        { "filename": "logo.png", "content_type": "image/png", "content": "iVBORw0KGgo...", "content_disposition": "inline", "content_id": "logo" }
      ]
    }

| Field | Notes |
| --- | --- |
| `content` | Base64-encoded bytes. |
| `url` | Public `http` or `https` URL. Fetched once at send time: at most 10 MB, 20 second timeout, no redirects, private addresses refused. `filename` and `content_type` default from the URL and response. |
| `filename`, `content_type` | Recommended with `content`. |
| `content_disposition`, `content_id` | For inline images referenced as `cid:logo` in your HTML. |

A URL that cannot be fetched fails the whole send with a `validation` error naming the attachment, for example `attachments[1].url`.

## When sending is refused

| Status | Cause |
| --- | --- |
| `400 validation` | No recipients, more than 50 recipients (5 from an unclaimed sandbox inbox), an invalid address, or an attachment that could not be loaded. |
| `403 forbidden` | The inbox is paused, a recipient is suppressed, a recipient is blocked by a send list, or the key lacks message_send (or message_reply for replies). |
| `404 not_found` | The inbox is not in the key’s scope. |
| `429 rate_limited` | An unclaimed agent’s sandbox inbox has used its sends or its different recipients for the past 24 hours (Retry-After says when the next send fits). |

All recipients are checked before anything is sent; if one is refused, none receive the message.

## Limits

- One message can have at most 50 recipients across `to`, `cc` and `bcc`, each address counted once. This applies to every way of sending: new messages, replies, reply-all, forwards and drafts. To reach more people, send separate messages.
- Sandbox inboxes of agents that registered themselves and have not been claimed have caps on messages and different recipients per 24 hours and on recipients per message. Claimed agents and inboxes created with your API keys have no per-inbox send cap.
- Attachments fetched by URL are capped at 10 MB each, and all attachments of a message at 10 MB together.
- Scheduled sending goes through drafts with `send_at`.

## Deliverability

- Send from a custom domain with SPF and DKIM verified for anything customer-facing. The shared machinemail.dev domain is fine for agents talking to services and to each other.
- Include a text body even when you send html.
- Watch bounce and complaint rates at /v0/metrics/rates. Hard bounces and complaints are suppressed automatically.
