# Attachments

Attach files to a send by adding an `attachments` array to the `POST /v1/email/messages` payload. Each entry carries the file's bytes base64-encoded in `content`, plus a `filename`. The same array works on a [batch](/docs/guides/email/sending-bulk) item. Full request and response schemas are in the [API reference](/docs/api/reference/create-email-message).

## A send with one attachment

```bash
curl -X POST https://us1.platform.bird.com/v1/email/messages \
  -H "Authorization: Bearer bk_us1_..." \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@yourdomain.com",
    "to": ["delivered@messagebird.dev"],
    "subject": "Your invoice",
    "html": "<p>Thanks for your order — your invoice is attached.</p>",
    "attachments": [
      {
        "filename": "invoice.pdf",
        "content": "JVBERi0xLjcKJ...",
        "content_type": "application/pdf"
      }
    ]
  }'
```

`content` is the raw file bytes, base64-encoded. `content_type` is optional — when you omit it, Bird infers the MIME type from the `filename` extension. Everything else about the send works exactly as in [sending email](/docs/guides/email/sending-email): the `202`, the async model, tags, and metadata are unchanged by the presence of attachments.

## The attachment fields

| Field          | Type   | Required | Notes                                                                                            |
| :------------- | :----- | :------- | :----------------------------------------------------------------------------------------------- |
| `filename`     | string | yes      | 1–255 characters; shown to the recipient. No line breaks or control characters.                  |
| `content`      | string | yes      | Base64-encoded file bytes.                                                                       |
| `content_type` | string | no       | MIME type; inferred from the filename extension when omitted.                                    |
| `content_id`   | string | no       | 1–128 chars, `[A-Za-z0-9._-]`. Set it to render the file inline (see below) instead of attached. |

An email carries up to 20 attachments (`attachments` is capped at 20 items). The `path` field — give Bird a URL and it fetches the file for you — is a preview feature and currently unavailable; supply `content` instead.

## Inline images

To embed an image in the HTML body rather than attach it, give the attachment a `content_id` and reference it from the markup with a `cid:` URL:

```json
{
  "html": "<p>Welcome aboard!</p><img src=\"cid:welcome-banner\"/>",
  "attachments": [
    {
      "filename": "banner.png",
      "content": "iVBORw0KGgoAAAANS...",
      "content_type": "image/png",
      "content_id": "welcome-banner"
    }
  ]
}
```

The `content_id` is the join between the `cid:` reference and the attachment. Each inline image needs a unique `content_id` within the send — a duplicate is rejected with a `422`. An attachment with no `content_id` is delivered as a regular file attachment.

## Size budget

Bird rejects a send whose **estimated generated message size** exceeds **20 MB** with a `413`. The estimate is the HTML body plus the text body plus every attachment measured _after_ base64 encoding — encoding inflates raw bytes by roughly 4/3, so a 15 MB file costs about 20 MB against the budget. As a rule of thumb, keep total raw attachment content at or below **15 MB** to leave headroom for encoding and MIME wrapping.

That cap is what Bird accepts; it is not what every mailbox accepts. Downstream limits vary by provider and by tenant policy — Gmail and Outlook.com document 25 MB, Exchange Online defaults to 35 MB but admins can lower it, and on-prem Exchange often defaults to 10 MB. A message near Bird's 20 MB cap can be accepted by Bird and still bounce at the recipient's server, so size attachments for the inboxes you actually send to.

## Blocked file types

Executable and script attachments are rejected at validation time with a `422`, matched on either the `content_type` or the `filename` extension. The list is deliberately narrow — common mail-borne executable and script formats, not a virus scanner — and covers types like `.exe`, `.dll`, `.msi`, `.bat`, `.cmd`, `.scr`, `.jar`, `.js`, `.vbs`, `.ps1`, `.sh`, `.hta`, and `.lnk`, along with their MIME equivalents (`application/x-msdownload`, `application/java-archive`, `text/javascript`, and similar). Zip the file or host it behind a link if you need to send one of these.

## In a batch

Each item in a [batch send](/docs/guides/email/sending-bulk) can carry its own `attachments`, with the same field contract and the same per-message 20 MB cap. One extra limit applies at the batch level: the serialized JSON request body for the whole batch has a hard 20 MB cap. Because attachments are base64-encoded inside that body, a batch of attachment-heavy messages reaches the body cap quickly — split large attachment sends across several batches, or send them individually.

## Reading and downloading attachments

API reads never echo attachment bytes. `GET /v1/email/messages/{id}` returns an `attachments` array of metadata only — each entry has the attachment `id`, `filename`, `content_type`, `size` (decoded bytes), and `inline`:

```json
{
  "attachments": [
    {
      "id": "ea_019c...",
      "filename": "invoice.pdf",
      "content_type": "application/pdf",
      "size": 215432,
      "inline": false
    }
  ]
}
```

To get the raw bytes back, call `GET /v1/email/messages/{message_id}/attachments/{attachment_id}` ([reference](/docs/api/reference/get-email-message-attachment)). It streams the file with its original content type and a `Content-Disposition` filename. Two conditions gate it:

- **Content storage must be on** for the workspace (it is on by default; see the [async model](/docs/guides/email/sending-email#the-async-model-what-202-means)). With storage off, there is nothing to download.
- **Attachments are retained for 30 days** after the send. After that the download returns `410 Gone`.

A `404` means the message has no stored content or no attachment with that ID; a `425 Too Early` means the attachment is still being stored and the request can be retried in a moment.

## Next steps

- [Sending email](/docs/guides/email/sending-email) — the rest of the send payload: recipients, content, tags, and the async model
- [Bulk sending](/docs/guides/email/sending-bulk) — batches and where attachments fit across many messages
- [API reference: create a message](/docs/api/reference/create-email-message) — the full request schema, including `attachments`
- [API reference: download an attachment](/docs/api/reference/get-email-message-attachment) — the retrieval endpoint and its status codes