---
title: "Attachments"
description: "Attach files and inline images to email messages, follow size and encoding limits, and download stored attachments through the API."
canonical: "https://bird.com/docs/guides/email/attachments"
---

# Attachments

Attach files to a send by adding an `attachments` array to the `POST /v1/email/messages` payload. Each entry has 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

**TypeScript**

```typescript
await bird.email.send({
  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",
    },
  ],
});
```

Examples: [TypeScript](/docs/guides/email/attachments.ts.md) · [Python](/docs/guides/email/attachments.py.md) · [Go](/docs/guides/email/attachments.go.md) · [PHP](/docs/guides/email/attachments.php.md) · [CLI](/docs/guides/email/attachments.cli.md) · [cURL](/docs/guides/email/attachments.curl.md)

The CLI reads the file and base64-encodes it for you; over the API you supply the encoded bytes yourself. The Go SDK takes the raw bytes and encodes them on the wire.

`content` is the raw file bytes, base64-encoded. `content_type` is optional: when you omit it, we infer the MIME type from the `filename` extension, falling back to `application/octet-stream` for an extension we don't recognize. 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 to 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 to 128 characters, `[A-Za-z0-9._-]`. Set it to render the file inline instead of attached. |

An email can have up to 20 attachments (`attachments` is capped at 20 items).

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

We reject 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 already accounts for the whole 20 MB budget on its own. As a rule of thumb, keep total raw attachment content well under **15 MB** so the bodies and the MIME wrapping still fit.

Receiving servers can apply lower size limits. A message that Bird accepts can still bounce if the recipient's server rejects its size. Choose attachment sizes for the mailbox providers and organizations you send to.

For received messages, see [Inbound message size](/docs/guides/email/receiving-email#inbound-message-size).

## Blocked file types

Executable and script attachments are rejected at validation time with a `422`, based on either `content_type` or the filename extension. Blocked extensions include `.exe`, `.dll`, `.msi`, `.bat`, `.cmd`, `.scr`, `.jar`, `.js`, `.vbs`, `.ps1`, `.sh`, `.hta`, and `.lnk`. Equivalent MIME types such as `application/x-msdownload`, `application/java-archive`, and `text/javascript` are also blocked. This validation is not a virus scanner. To distribute a blocked file, host it behind a link.

## In a batch

Each item in a [batch send](/docs/guides/email/sending-bulk) can have its own `attachments`, with the same field contract and the same per-message 20 MB budget. The batch's serialized request body has its own cap on top, which base64-encoded attachments consume quickly; see [batch sending](/docs/guides/email/sending-bulk#attachments-and-the-body-cap) for the batch-level limit and how to split around it.

## Reading and downloading attachments

API reads never echo attachment bytes. `GET /v1/email/messages/{message_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 own content type and a `Content-Disposition` header naming the filename. Two conditions gate it:

- **Content storage must be enabled** for the workspace. With storage disabled, nothing is stored to download. See [what a 202 means](/docs/guides/email/sending-email#what-a-202-means).
- **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, including recipients, content, tags, and the async model
- [Batch 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

## Related resources

- [Getting started with email](/learn/email/getting-started-with-email) (video)
- [Email](/email-api) (product)
- [Build your first integration](/learn/paths/integration) (course)
- [Send your first email](/docs/get-started/send-your-first-email) (docs)

[Get an implementation brief](/learn/workspace?topic=email)
