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 item. Full request and response schemas are in the API reference.
A send with one attachment
Code example
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: 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:
Code example
{
"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 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:
Code example
{
"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). 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). 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 — the rest of the send payload: recipients, content, tags, and the async model
- Bulk sending — batches and where attachments fit across many messages
- API reference: create a message — the full request schema, including attachments
- API reference: download an attachment — the retrieval endpoint and its status codes