API Reference
API Reference
GitHub
Email Sending

Attachments

Send emails with file attachments

Attach files to emails by providing base64-encoded content in the attachments array. Attachments work the same way on /emails/send and /emails/send-template, on both the console API and the machine API.

From the console

The Send Email page has an attachment picker that works in both Raw and Template mode. Files are read in the browser, base64-encoded, and sent with the message - there is no staging upload, so abandoning the form leaves nothing behind on the server.

The picker enforces the same limits the server does, reading them from GET /api/v1/emails/limits rather than hardcoding them, so tuning the config below changes what the form accepts without a rebuild.

Template mode

A template can carry its own stored attachments (managed on the template’s detail page, or under /api/v1/templates/:id/attachments). Those are appended by the server on top of whatever the form or the request sends, and both sets count toward the limits below.

Limits

LimitConfig keyDefault
Per attachmentsending.max_attachment_size10 MiB
Total per emailsending.max_total_attachment_size25 MiB
Count per emailnot configurable10

Exceeding a size limit is a 400 naming the offending file:

{
    "error": "attachment \"report.pdf\" exceeds maximum size of 10485760 bytes"
}

The HTTP body limit is derived from these

Base64 inflates a file by 4/3, so the request body cap on the send routes is computed from sending.max_total_attachment_size at startup, not set independently. It applies to the routes that carry attachments only - every other API route is capped at 8 MiB, whatever the attachment limits say.

A request larger than that cap is rejected by the HTTP layer before any handler runs, and that response is plain text (Request Entity Too Large), not the usual JSON error shape. If you are building a client, treat a 413 as a size failure without trying to parse it.

Query the effective limits instead of assuming the defaults:

curl http://localhost:3000/api/v1/emails/limits \
  -H "Authorization: Bearer myk_..."
{
    "limits": {
        "max_recipients": 50,
        "max_attachments": 10,
        "max_attachment_size": 10485760,
        "max_total_attachment_size": 26214400
    }
}

The same endpoint is on the machine API at /api/v1/emails/limits under emails:read.

The shape

Each entry has three fields, and only two of them matter:

FieldNotes
filenameRequired. An entry without one is refused
contentThe file, base64 with standard padding
content_typeOptional — defaults to application/octet-stream

Leaving content_type out is safe but rarely what you want: application/octet-stream tells the recipient’s client nothing, so a PDF arrives as an anonymous blob to be downloaded rather than something the client can preview inline. Send the real type when you know it.

filename reaches the recipient twice, in the part’s Content-Type name parameter and in its Content-Disposition. Both are encoded properly, so a name with spaces or non-ASCII characters is safe to send as-is.

Sending one

curl -X POST http://localhost:3000/api/v1/emails/send \
  -H "Authorization: Bearer myk_..." \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
        --arg pdf "$(base64 < report.pdf | tr -d '\n')" \
        '{from: "sender@example.com",
          to: ["recipient@example.com"],
          subject: "Your report",
          html: "<p>The March report is attached.</p>",
          attachments: [{filename: "report.pdf", content: $pdf, content_type: "application/pdf"}]}')"

Building the body with jq rather than string interpolation is deliberate. Base64 output wraps at 76 columns on some platforms and not others, and an embedded newline breaks the JSON — tr -d '\n' and a real JSON encoder remove both problems at once.

Add further files by extending the array. Every entry counts toward all three limits above, and so do a template’s own stored attachments.

When it is refused

Validation runs over the whole set before anything is written, and the message names the file:

MessageCause
attachment filename is requiredAn entry with no filename
attachment "report.pdf" has invalid base64 contentNot decodable — usually a stray newline or missing padding
attachment "report.pdf" exceeds maximum size of 10485760 bytesOver the per-file limit
total attachment size exceeds maximum of 26214400 bytesThe set is over the combined limit

Sizes are measured on the decoded bytes, not on the base64 you sent, so a 9 MB file is a 9 MB attachment even though it travels as roughly 12 MB of text.

Generating a client

The Go client’s Attachment type mirrors the shape above; the Python and Ruby clients take it as a plain dict or hash. See API Keys - clients . For any other language, generate from the OpenAPI document, which carries the shape above so you do not transcribe field names by hand.