SDK
Send a batch
`emails.sendBatch`: up to 100 messages, results per item.
emails.sendBatch
const result = await openemail.emails.sendBatch(invoices.map(toMessage)) console.log(result.sent, 'sent,', result.failed, 'failed') for (const item of result.items) { if (item.status === 'error') console.error(item.index, item.error.code, item.error.message) else console.log(item.index, item.email.id)}items holds one entry per input, in order, each either ok with its message or error with the envelope that message would have been refused with. Nothing rolls back, so failed > 0 is a list to act on rather than a reason to resend the batch.
One idempotency key covers the batch and the server extends it per item, so a retried batch replays every message rather than collapsing them onto the first.
Parameters: emails.sendBatch
emailsEmailSend[]required- One to 100 messages, serialised as `{ "emails": [...] }` and accepted one at a time in the order given. An empty array, more than 100, or more than 10 items carrying `translate` refuses the whole call with a `validation_error` on `emails`. So do a missing `emails:send` scope, a body that is not an array or `{ emails: [...] }`, and a malformed `Idempotency-Key`, all of them before a single message is sent.
options.idempotencyKeystring- Deduplicates the batch across processes. The client attaches a freshly generated key on every call in any case, so its own retries never double-send, and the server extends whichever key it gets per item as `key/0`, `key/1` and so on, slash-separated with a character your own key may not contain, so one key over a hundred messages cannot collapse them onto the first.
emails[].fromRecipientInputrequired- The sender, as a bare address, `Name <addr@host>` or an object. There is no fallback sender and the key must be allowed this address; a refusal fails that one item, as a `permission_error` with code `from_address_forbidden`.
emails[].toRecipientInput | RecipientInput[]required- At least one recipient, and a lone one is wrapped into an array by the client. At most 50 addresses across `to`, `cc` and `bcc` combined, counted per message rather than across the batch.
emails[].ccRecipientInput | RecipientInput[]- Defaults to none, and counts against the same 50-address total as `to` and `bcc`.
emails[].bccRecipientInput | RecipientInput[]- Defaults to none, and counts against the same 50-address total. `Bcc` is one of the names `headers` may not set, so this is the only way to blind-copy. The header form would undo the per-recipient envelope that keeps the address blind.
emails[].replyToRecipientInput- Where replies go. It is applied after `headers`, so it overwrites a `Reply-To` you also set there rather than adding a second one.
emails[].subjectstring- At most 998 characters, the RFC 5322 line limit, and defaults to an empty string. An empty subject falls through to the template's own when `template` supplies one.
emails[].htmlstring- The HTML part, at most a million characters, and the part recipients see when both bodies are given. One of `html`, `text`, `template` or `draftId` is required, and an item with none of them fails as a `validation_error` on `html`.
emails[].textstring- The plain-text part, at most a million characters. Both may be sent, and every transport on this path builds one body from one string, so `html` wins where there is one.
emails[].headersRecord<string, string>- `X-*`, `List-*`, Reply-To, Precedence, Auto-Submitted, Importance, Priority and Feedback-ID only; anything the transport sets itself (From, To, Bcc, Subject, Message-ID, the DKIM and ARC headers) is refused as `reserved_header` rather than quietly dropped. Values are at most 998 characters and may not carry CR, LF or NUL, because a second line is a second header.
emails[].attachmentsAttachmentInput[]- At most 20 files per message, with inline files totalling 5 MB once decoded, counted per message and not per batch. `content` is base64 on the wire; pass bytes and the client encodes them, which is the one place hand-rolled base64 reliably blows the call stack. A `{ fileId }` entry names a file already in the workspace and is not counted against the inline cap.
emails[].threadIdstring- Reply into an existing thread, at most 256 characters. The transport writes In-Reply-To and References from it, which is what makes the reply land in the conversation rather than beside it.
emails[].draftIdstring- Send a saved draft's content under this envelope, at most 256 characters. The recipients, subject and headers built here are what go on the wire.
emails[].template{ id, version?, props?, slots? }- Render a stored template server-side, by id (`tpl_…`) or slug, with `version` pinning a revision and `props`/`slots` filling it. Resolved once, when the item is accepted, and refused alongside `html`/`text` and alongside `draftId`, since each of those is a second answer to what the message contains.
emails[].scheduledAtDate | string- A `Date`, an ISO-8601 instant, or a duration like `PT1H`; at least a second in the future and at most 365 days out. Items schedule independently, so one batch can hold a hundred different send times.
emails[].cancellableForSecondsnumber- An undo window in seconds on an immediate send, an integer from 0 to 900, defaulting to 0. Anything above 0 is refused alongside `scheduledAt` on the same item, since a scheduled message is already cancellable until it goes.
emails[].trackingTrackingRequest- `opens` and `clicks`, each independently optional and each overriding the setting for this message alone. A switch you omit falls back to the setting of the address the message is sent from, or else to All addresses, which is on unless one of those turned it off.
emails[].tagsRecord<string, string>- At most 10 labels, keys of 1 to 64 characters drawn from `A-Za-z0-9_-` and values up to 256. Echoed back on the message and never interpreted: `emails.list` takes `status`, `from`, `limit` and `cursor` and nothing else, so a tag is something to read off a message you already hold rather than a way to find it.
emails[].translateSendTranslateOptions- Send this item in another language, resolved at accept time so the words that were approved are the words that go out. At most 10 items in one batch may carry it: each spends several model calls and the items run in order, so a larger batch would be killed mid-send. Over that the whole call is refused as `too_many_items` on `emails`, before anything is sent.
Response: BatchResultResource
itemsBatchItemResource[]- One entry per input, in the order you sent them. Nothing rolls back, so this is a record of what happened to each message rather than a report on a transaction. The API answers 207 whether every message was accepted, some were or none was, so the promise resolves either way and the per-item `status` is what to branch on.
sentnumber- How many items were ACCEPTED, which is not the same as how many left. An item can be `ok` and still carry an `email.status` of `failed` or `partial`, because a transport that refuses the message after the row exists is a delivery outcome and not a rejected request.
failednumber- How many entries carry an `error`. `failed > 0` is a list to act on rather than a reason to resend the batch. The accepted messages have already gone.
items[].indexnumber- The position this entry's message held in the array you sent. Carried as a field as well as an order, so code that filters or sorts `items` can still say which input failed.
items[].status'ok' | 'error'- The discriminant of the union: `ok` carries `email`, `error` carries `error`, and no entry carries both.
items[].emailSentEmailResource- The accepted message, on an `ok` entry only, in the same shape a single send returns. It carries no `tracking` key, because engagement is reported later and there is nothing to report at accept time.
items[].email.replayedboolean- True when the derived `Idempotency-Key` matched a send that already existed, so nothing new was sent and this is the original message.
items[].error{ type: string; code: string; message: string; param?: string }- Why this one message was refused, on an `error` entry only. It is the API's error envelope minus `docUrl` and `requestId`: those describe the request, and the request as a whole succeeded.
items[].error.typestring- The category a client may branch on: `validation_error`, `permission_error`, `not_found_error`, `conflict_error` and the rest. The set is frozen and will not grow, unlike `code`.
items[].error.codestring- The specific failure: `from_address_forbidden`, `invalid_email_address`, `too_many_recipients`, `reserved_header`, `message_too_large`, `unknown_parameter`. Open and additive, so treat a code you do not recognise as its `type`.
items[].error.messagestring- One sentence written for a person, naming the offending value where there is one. Not a stable identifier. Switch on `code`.
items[].error.paramstring- The field that was refused, as a dotted path within THAT message: `to.0`, `from`, `attachments`. Absent when the failure names no field, and never prefixed with the batch position, which is what `index` is for.