Attachments

Attach files to transactional email, campaigns, automations and agent inboxes.

GET/POST/api/v1/attachmentsElenca o carica un allegato riusabile: `content` base64 per l'upload immediato, `size` per ottenere una `uploadUrl` firmata (file grandi).(scope: email:send|inbox:send)
GET/DELETE/api/v1/attachments/:idMetadati + URL di download firmata, oppure eliminazione.(scope: email:send|inbox:send)
POST/api/v1/attachments/:id/confirmConferma un upload firmato (allinea dimensione e Content-Type reali).(scope: email:send|inbox:send)

The attachments field is accepted on every email sending path:

PathEndpoint
TransactionalPOST /api/v1/emails, POST /api/v1/messages (channel email)
Marketing (broadcast)POST /api/v1/campaigns
Marketing (automations)Journey message node, attachmentIds field
Agent EmailPOST /api/v1/inboxes/:id/send, POST /api/v1/inboxes/:id/threads/:threadId/reply

The three sources

Each item in attachments declares exactly one source:

ShapeWhen to use it
{ "id": "<uuid>" }A file already uploaded with POST /api/v1/attachments. This is the recommended path: upload once, reuse across as many sends as you like.
{ "filename": "...", "content": "<base64>" }Small files: the content travels in the request body, so the practical limit is the few MB an HTTP body will take.
{ "filename": "...", "url": "https://..." }Files already published at a public https URL: we fetch them (private hosts and loopback are blocked).

Common optional fields: contentType (inferred from the filename when absent) and contentId. With contentId the attachment becomes inline and is referenced from the HTML as cid:<value>:

{
  "html": "<p>Here is the logo: <img src=\"cid:logo\" /></p>",
  "attachments": [{ "filename": "logo.png", "content": "<base64>", "contentId": "logo" }]
}

Limits

LimitValue
Attachments per email20
Single file size10 MB
Total size25 MB

Executable extensions (exe, bat, js, jar, msi, vbs, …) are rejected with 422: provider filters treat them as malware, and the message would land in spam or bounce, hurting the domain's reputation. For those files, send a link instead.

Inline attachment in a send

import { readFileSync } from "node:fs";
 
await sending.emails.send({
  from: "Acme <[email protected]>",
  to: "[email protected]",
  subject: "Your invoice",
  html: "<p>July's invoice is attached.</p>",
  attachments: [
    { filename: "invoice-2026-07.pdf", content: readFileSync("invoice.pdf").toString("base64") },
  ],
  idempotencyKey: "invoice-sara-2026-07",
});

POST /api/v1/attachments · reusable attachment

Two modes, depending on the size of the file.

Direct upload (base64 in the body, small files):

curl -X POST "https://sending.dev/api/v1/attachments" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{ "filename": "pricelist.pdf", "content": "<base64>" }'
# → 201 { "attachment": { "id": "...", "status": "ready", ... } }

Signed upload (large files): ask for an uploadUrl, PUT the file, confirm.

Create the attachment declaring size and type

curl -X POST "https://sending.dev/api/v1/attachments" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{ "filename": "catalogue.pdf", "contentType": "application/pdf", "size": 4194304 }'
# → 201 { "attachment": { "id": "...", "status": "pending" }, "uploadUrl": "https://...", "expiresIn": 900 }

Upload the file to uploadUrl

curl -X PUT "<UPLOAD_URL>" \
  -H "Content-Type: application/pdf" \
  --data-binary @catalogue.pdf

The signed URL pins Content-Type and Content-Length to what you declared in step 1.

Confirm (optional)

curl -X POST "https://sending.dev/api/v1/attachments/<ID>/confirm" \
  -H "Authorization: Bearer <YOUR_API_KEY>"

Confirming reconciles the real size and Content-Type. Skipping it costs you nothing: the same check runs automatically the first time the attachment is used in a send.

The TypeScript SDK wraps the three steps:

const attachment = await sending.attachments.upload({
  filename: "catalogue.pdf",
  data: readFileSync("catalogue.pdf"),
  contentType: "application/pdf",
});
 
await sending.emails.send({
  from: "Acme <[email protected]>",
  to: "[email protected]",
  subject: "The 2026 catalogue",
  html: "<p>Here it is.</p>",
  attachments: [{ id: attachment.id }],
  idempotencyKey: "catalogue-sara-001",
});

Campaigns and automations

On a campaign, attachments are declared at creation and apply to every recipient (the file sits in storage once, it is not duplicated per contact):

curl -X POST "https://sending.dev/api/v1/campaigns" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{ "name": "2026 pricelist", "fromAddress": "[email protected]", "listId": "<UUID>",
        "subject": "The new pricelist", "html": "<p>Attached.</p>",
        "attachments": [{ "id": "<ATTACHMENT_ID>" }] }'

In automations the message node carries attachmentIds (ids of already uploaded attachments only): the DAG stays small, and swapping the file does not require a new version of the journey.

{
  "id": "welcome-mail",
  "type": "message",
  "preferredChannel": "email",
  "fallbackChain": ["email"],
  "templateByChannel": { "email": "<TEMPLATE_ID>" },
  "attachmentIds": ["<ATTACHMENT_ID>"],
  "next": null
}

Management

OperationEndpoint
ListGET /api/v1/attachments
Metadata plus signed download (15 min)GET /api/v1/attachments/:id
DeleteDELETE /api/v1/attachments/:id

The required scope is email:send or inbox:send. GET /api/v1/emails/:id reports the attachments of that send (id, filename, contentType, size).

Deleting an attachment does not stop already queued sends that reference it: they will go out without that file.
Full schema and a "try it" console in the interactive API reference (OpenAPI).