Knowledge base
Email API
Send one message or a hundred per call, now or later, and retry without sending twice.
Details
- POST /emails sends one message and POST /emails/batch up to 100 independent ones. A batch is never all or nothing: a bad address on item 7 fails item 7, the rest still go, and the answer reports every item on its own.
- The body comes from exactly one source: html and or text, a stored template by id or slug (pin its version when somebody else owns the copy), or an existing draft. Up to 10 tags ride along and are echoed back on every read.
- scheduledAt holds a message for up to 365 days, as an ISO 8601 instant or a duration such as PT1H. cancellableForSeconds gives an immediate send an undo window of up to 900 seconds. Either can be cancelled, and a scheduled send rescheduled, until it goes.
- Every send carries an Idempotency-Key that is claimed before anything is dispatched, so a retry after a timeout returns the first result with Idempotency-Replayed: true. The same key with a different body is refused as idempotency_key_reuse.
- Each send has a msg_ id from the moment it is accepted. GET /emails/{id} reads it, /events gives the trail for each recipient and /tracking the opens and clicks.
- Add translate and the message is delivered in the language of the recipient. It is translated when the request is accepted, so a scheduled send carries the wording you approved, and a translation that cannot be produced refuses the send rather than falling back to the original.
- Attachments: up to 20 files, inline ones capped at 5 MB in total. A larger file is sent by naming a workspace file by its id, and travels as a download link.
- What is missing: there is no test mode yet, so every key delivers for real, and the send row has no bounced state. A bounce is labelled on the thread and raised as an email.bounced webhook, while GET /emails still reads sent.
- The suppression list, Blocked addresses in Settings, is on the API as well. GET /suppressions reads it with a search and a reason filter, POST /suppressions blocks an address by hand, and DELETE /suppressions/{id} allows one again, except a hard bounce, which stays. The SDK and the MCP server do the same, and each change fires the suppression.added or suppression.removed webhook.