PristineSend
Get started
API Reference

Batch send

Send up to 100 transactional emails in a single request. Each item is metered, sent, and logged independently, so one bad item fails on its own while the rest still send. For a single email, use Send email.

Endpoint

POST/api/v1/send/batch
HeaderValue
AuthorizationBearer ps_live_YOUR_API_KEY — required
Content-Typeapplication/json — required

Request body

A JSON object with a single emails field: a non-empty array of 1 to 100 items. An empty array, a non-array, or more than 100 items is rejected as a whole-batch 400 invalid_field (param: "emails").

Each item has the same shape as a single send:

FieldTypeRequiredDescription
tostringrequiredRecipient email address.
subjectstringrequiredEmail subject line.
htmlstringrequiredHTML body of the email.
sender_idstringoptionalPer-item sender id (Settings → Senders). Takes precedence over from; a missing/cross-tenant id is a per-item sender_not_found, an unverified-domain id a per-item sender_not_verified.
fromstringoptionalPer-item sender address on a verified domain. Ignored when sender_id is given. Omit both to use your workspace default sender.
reply_tostring | string[]optionalPer-item Reply-To — a single email address or an array. Precedence: this field, then the resolved sender's default reply_to (Settings → Senders), then none. An invalid address fails that item with invalid_field (param reply_to).
attachmentsobject[]optionalPer-item attachments (up to 20), each inline ({ filename, content, content_type? }) or hosted ({ filename, url, content_type? }). A bad/oversize attachment fails that item with invalid_field (param attachments) or message_too_large — the rest still send. See Send → Attachments.

Example request body:

{
  "emails": [
    {
      "to": "first@example.com",
      "subject": "Your receipt",
      "html": "<p>Thanks for your order.</p>",
      "from": "orders@yourdomain.com",
      "reply_to": "support@yourdomain.com"
    },
    {
      "to": "second@example.com",
      "subject": "Your receipt",
      "html": "<p>Thanks for your order.</p>"
    }
  ]
}

Reply-To. Each item may carry reply_to — a single email or an array of emails. Precedence per item is request reply_to → the resolved sender's default reply_to → none; set a per-sender default under Settings → Senders. An invalid address fails that item with invalid_field (param: "reply_to"), leaving the rest of the batch unaffected.

Response

When the batch is accepted the API returns 207 Multi-Status with a data array (one entry per item, in request order) and a summary. Each entry is either a success or a failure:

FieldDescription
indexThe item's zero-based position in the request emails array.
status"sent", "failed", or "duplicate".
idOn "sent": the email's UUID, usable with GET /api/v1/emails/{id}.
errorOn "failed": an object with code, message, and an optional param.
duplicate_ofOn "duplicate": the index of the earlier byte-identical entry this one collapsed into.
summary{ total, sent, failed, duplicates } counts across the batch.

Example 207 body:

{
  "data": [
    { "index": 0, "status": "sent", "id": "550e8400-e29b-41d4-a716-446655440000" },
    {
      "index": 1,
      "status": "failed",
      "error": {
        "code": "missing_field",
        "message": "Field 'subject' is required.",
        "param": "subject"
      }
    }
  ],
  "summary": { "total": 2, "sent": 1, "failed": 1, "duplicates": 0 }
}

Partial success

Items are independent. A malformed item (missing to/subject/html), or one the provider rejects, fails on its own and does not roll back the others. Every successful item is metered, sent, and logged independently, producing its own delivery and scan rows just like a single send. The failed count is refunded so your meter reflects only real sends.

Whole-batch errors

Some conditions stop the entire batch before any item is sent. These return a single error envelope (not a 207):

StatuscodeWhen
401unauthorizedMissing or invalid API key.
403transactional_pausedTransactional sending is paused for review on this account.
402overage_cap_reachedThe workspace spend cap was reached — the batch meters fail-closed.
429rate_limitedThe per-key rate limit (1000/minute) was exceeded. Sets Retry-After.
503service_unavailableMetering is briefly unavailable — the batch fails closed (no sends). Retryable.
503suppression_unverifiableThe suppression list couldn't be verified for the batch — it fails closed (no sends). Retryable.

Example whole-batch error:

{
  "error": {
    "code": "transactional_paused",
    "message": "Transactional sending is paused for review on this account. Contact support.",
    "request_id": "req_8f3c9a1b2d4e5f6071829304"
  }
}

See Error codes for the canonical envelope and the complete list.

Code examples

"color:#ff7b72">import { PristineSend } "color:#ff7b72">from "pristinesend"

"color:#ff7b72">const ps = "color:#ff7b72">new PristineSend(process.env.PRISTINESEND_API_KEY!)

"color:#ff7b72">const { data, summary } = "color:#ff7b72">await ps.emails.sendBatch({
  emails: [
    { to: "first@example.com", subject: "Your receipt", html: "<p>One</p>", "color:#ff7b72">from: "orders@yourdomain.com", reply_to: "support@yourdomain.com" },
    { to: "second@example.com", subject: "Your receipt", html: "<p>Two</p>" },
  ],
})

console.log(`${summary.sent}/${summary.total} sent`)
for ("color:#ff7b72">const item of data) {
  if (item.status === "failed") console.error(item.index, item.error.code)
}