> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ruber.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Sending

> POST /v1/messages — send mail from an address your organization owns, with attachments, threading, and the limits that apply.

```
POST https://app.ruber.me/api/v1/messages
```

**Required permission:** Sending → Write (`messages:send`)

Sending is a separate permission from everything else. `messages:write` lets a
key file and star mail that already exists; it does **not** let it send. Mail
sent here leaves under your domain's reputation and spends your plan's daily
allowance, so granting it is a deliberate choice.

## Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://app.ruber.me/api/v1/messages \
    -H "Authorization: Bearer rk_your_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "from": "you@yourdomain.com",
      "to": ["customer@example.com"],
      "subject": "Your order has shipped",
      "text": "It is on its way — tracking is attached.",
      "html": "<p>It is on its way — tracking is attached.</p>"
    }'
  ```

  ```js Node theme={null}
  const response = await fetch("https://app.ruber.me/api/v1/messages", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.RUBER_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      from: "you@yourdomain.com",
      to: ["customer@example.com"],
      subject: "Your order has shipped",
      text: "It is on its way.",
    }),
  });
  ```

  ```python Python theme={null}
  import os, requests

  requests.post(
      "https://app.ruber.me/api/v1/messages",
      headers={"Authorization": f"Bearer {os.environ['RUBER_API_KEY']}"},
      json={
          "from": "you@yourdomain.com",
          "to": ["customer@example.com"],
          "subject": "Your order has shipped",
          "text": "It is on its way.",
      },
  )
  ```
</CodeGroup>

| Field         | Type            |                                                                |
| ------------- | --------------- | -------------------------------------------------------------- |
| `from`        | string          | **required.** A mailbox your organization owns.                |
| `to`          | array or string | **required.** Up to 25 recipients.                             |
| `cc`          | array or string | Up to 25.                                                      |
| `bcc`         | array or string | Up to 25.                                                      |
| `subject`     | string          | Up to 200 characters.                                          |
| `text`        | string          | Plain-text body, up to 100,000 characters.                     |
| `html`        | string          | HTML body, up to 400,000 characters. Sanitised before sending. |
| `in_reply_to` | string          | The `message_id` you are replying to.                          |
| `references`  | array           | Message IDs of the thread.                                     |
| `attachments` | array           | Up to 10, 8 MB total. See below.                               |

Address fields accept either an array or a comma-separated string. Both are
common in the wild and neither is wrong.

<Note>
  **`from` must be a mailbox, not an alias.** Aliases are sendable in the web app
  but are not accepted here yet — an alias is refused rather than quietly sent as
  its owner. Use an address from [`GET /v1/mailboxes`](/api/mailboxes).

  Private mailboxes cannot send through the API at all: the message would have to
  be encrypted with a key only the owner's browser holds.
</Note>

### Attachments

Each entry is `filename`, base64 `content`, and an optional `content_type`
(defaults to `application/octet-stream`).

```json theme={null}
{
  "from": "you@yourdomain.com",
  "to": ["customer@example.com"],
  "subject": "Your invoice",
  "text": "Attached.",
  "attachments": [
    {
      "filename": "invoice.pdf",
      "content_type": "application/pdf",
      "content": "JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9..."
    }
  ]
}
```

At most **10 attachments** totalling **8 MB** decoded. The ceiling leaves
headroom under SES's \~10 MB message limit once MIME encoding is added — base64
inflates content by roughly a third, and that inflation happens after this
check.

## Response

`202 Accepted`. The message has been handed to the mail server, which is not
the same as delivered — remote servers accept and bounce on their own schedule.

```json theme={null}
{
  "accepted": ["customer@example.com"],
  "rejected": [],
  "message_id": "<2f8c1e40-9d7a-4b3e-8c11-0a2f6b7d9e10@yourdomain.com>"
}
```

A recipient in `rejected` was refused by the mail server outright — usually a
malformed address. Recipients in `accepted` were taken for delivery.

A copy is filed in the Sent folder automatically. If that filing fails the send
still reports success: the message has already gone, and reporting failure would
invite you to send it twice.

## Limits

Sending passes several ceilings, and they fail differently:

| Limit                  | Scope            | Error            |
| ---------------------- | ---------------- | ---------------- |
| 600 requests/minute    | per API key      | `rate_limited`   |
| 50 messages/hour       | per mailbox      | `rate_limited`   |
| 5 messages/10 seconds  | per mailbox      | `rate_limited`   |
| Plan's daily allowance | per organization | `quota_exceeded` |

The per-mailbox limits are the ones that decide whether a leaked key can burn
your domain's reputation before you notice, so an API key does not get a more
generous version of them than the web app has. The daily allowance is the
entitlement your plan sells, and it is shared: messages sent from the dashboard
and through the API draw on the same counter.

`rate_limited` carries a `Retry-After` header. `quota_exceeded` does not — the
allowance resets at midnight UTC.

## Errors

| Code               | HTTP  | Cause                                                                                                            |
| ------------------ | ----- | ---------------------------------------------------------------------------------------------------------------- |
| `invalid_request`  | `400` | Malformed JSON, a bad address, no recipients, attachments over 8 MB, or `from` is an alias or a Private mailbox. |
| `unauthorized`     | `401` | Missing, malformed, unknown or revoked key.                                                                      |
| `quota_exceeded`   | `402` | Your plan's daily sends are used up.                                                                             |
| `forbidden`        | `403` | The key lacks `messages:send`.                                                                                   |
| `not_found`        | `404` | `from` is not a mailbox on this account.                                                                         |
| `rate_limited`     | `429` | One of the sending limits above. See `Retry-After`.                                                              |
| `mail_unavailable` | `502` | The mail server refused the message. **Nothing was sent.**                                                       |

See [Errors](/api/introduction#errors) for the full response shape.
