Mailfully logo

Attachments

Attach files to a send with base64 content, embed inline images with cid, and stay inside the 40 MiB message ceiling.

Any send can carry an attachments array. Each entry is a file: a name, the bytes as base64, and optionally a MIME type and a Content-ID. A message with at least one attachment is assembled as a full MIME message rather than the plain subject-and-body form.

The attachment object

FieldTypeRequiredBehavior
filenamestringyesThe name the recipient sees.
contentstringyesThe file's bytes, base64-encoded. Bare base64 only, with no data: URI prefix.
contentTypestringnoMIME type, for example application/pdf. Omit it and the type is derived from filename.
cidstringnoContent-ID. Setting it makes the part inline rather than a listed attachment. See Inline images.

contentType and cid are camelCase, unlike the snake_case fields elsewhere in the send body. Unknown keys are ignored, so content_type is not read and the type comes from filename.

Attach a file

curl -X POST https://api.mailfully.com/v1/emails \
  -H "Authorization: Bearer mf_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "billing@mail.acme.com",
    "to": "you@yourcompany.com",
    "subject": "Invoice 4212",
    "text": "Invoice 4212 is attached.",
    "attachments": [
      {
        "filename": "invoice-4212.pdf",
        "content": "JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9UeXBlL...",
        "contentType": "application/pdf"
      }
    ]
  }'

The response is the standard 202 acceptance. Attachments are not echoed back, the same way html and text are not.

In the SDK, attachments is typed unknown[] and sent as-is, so TypeScript will not catch a misspelled filename. The API will: a bad entry comes back as a 422 naming it by index.

Inline images

Give an attachment a cid and it becomes part of the message body rather than a file listed at the bottom. Reference it from your HTML with cid: and the same value:

{
  "from": "news@mail.acme.com",
  "to": "you@yourcompany.com",
  "subject": "This month at Acme",
  "html": "<p>Hello.</p><img src=\"cid:header-logo\" alt=\"Acme\">",
  "attachments": [
    {
      "filename": "logo.png",
      "content": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAA...",
      "contentType": "image/png",
      "cid": "header-logo"
    }
  ]
}

Pick a cid that is unique within the message. Whether the image renders is up to the receiving client, and many hold images until the reader allows them, so keep the alt text meaningful and never put content only in an image.

Size limit

The ceiling is 40 MiB (41,943,040 bytes) of base64, totaled across every attachment on the message. Base64 inflates bytes by about a third, so that budget is roughly 30 MiB of original file.

The limit is per message, not per file. A batch of 100 gets 40 MiB each, not 40 MiB between them.

Going over returns a 422 on the send, with param set to attachments.

Validation

Attachments are validated when the request is received. A malformed attachment returns a 422 with the standard error envelope, and param names the offending entry by index. Nothing is silently dropped.

Rejectedparam
filename missing, not a string, or emptyattachments.<i>.filename
content missing, not a string, or emptyattachments.<i>.content
content carries a data: URI prefixattachments.<i>.content
content is not base64attachments.<i>.content
Total base64 across all attachments exceeds 41,943,040 bytesattachments

So a bad content on the first attachment reports as attachments.0.content.

Line-wrapped base64 and the URL-safe base64 alphabet (- and _) are both accepted.

Attachments elsewhere

  • Batch: POST /v1/emails/batch takes the same object per entry, with the same rules.
  • Templates: a template supplies html and text; attachments stay on the send. Templates cannot carry them.
  • Test mode: attachments are sent as in live mode. See Test mode for what changes.
  • Scheduled sends: attachments are validated when you schedule the send, not when it dispatches. See Scheduled sends.