Send a Batch

Send up to 100 emails in one call with POST /v1/emails/batch

POST /v1/emails/batch sends up to 100 emails in one request. Each email is independent: its own sender, recipients, subject and content, with the same fields as POST /v1/emails.

Requires API 0.22.0 or later. The SDKs have a batch method too: see With the SDKs.

Request

The body is an object with one field, emails: an array of 1 to 100 emails.

curl -X POST https://api.tratto.email/v1/emails/batch \
  -H "Authorization: Bearer tratto_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2e0a-batch-2026-10-20" \
  -d '{
    "emails": [
      {
        "from": "hello@yourdomain.com",
        "to": "alice@example.com",
        "subject": "Your receipt",
        "text": "Thanks for your order, Alice."
      },
      {
        "from": "hello@yourdomain.com",
        "to": ["bob@example.com"],
        "cc": ["billing@example.com"],
        "subject": "Your receipt",
        "templateId": "tmpl_abc123",
        "variables": { "firstName": "Bob" }
      }
    ]
  }'
import { randomUUID } from 'node:crypto';

const response = await fetch('https://api.tratto.email/v1/emails/batch', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer tratto_live_...',
    'Content-Type': 'application/json',
    'Idempotency-Key': randomUUID(),
  },
  body: JSON.stringify({
    emails: [
      { from: 'hello@yourdomain.com', to: 'alice@example.com', subject: 'Your receipt', text: 'Thanks, Alice.' },
      { from: 'hello@yourdomain.com', to: 'bob@example.com', subject: 'Your receipt', text: 'Thanks, Bob.' },
    ],
  }),
});
const { data } = await response.json();
console.log(`${data.accepted} queued, ${data.failed} failed`);

Rules for each email of the batch:

  • It accepts every field of POST /v1/emails except scheduledAt. To schedule, send the email on its own with POST /v1/emails.
  • attachments is not accepted: it fails with 422.
  • An unknown field fails with 422: both the body and each email are checked strictly. POST /v1/emails does the same with API 0.24.0 or later, see Unknown fields.
  • At most 50 distinct recipients across to, cc and bcc, as on a single send. See Recipients.

The request body can be at most 5 MiB. Once rendered, each email can be at most 900 KB and the whole batch at most 9 MiB.

Validation is all or nothing. If one email is invalid, the whole batch is rejected and nothing is written or charged. details[].path names the email by its position in the array, starting from 0:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed.",
    "docs": "https://docs.tratto.email/en/docs/error-codes",
    "details": [{ "path": "/emails/3/to", "message": "An email can have at most 50 recipients across to, cc and bcc (got 51). Split the send, or use a campaign for a larger audience." }]
  }
}

An unverified sender domain answers 403 FORBIDDEN, and the message starts with the position of the email, as in emails[3]: Domain 'mail.acme.com' is not verified for this tenant.

Response

A batch that passes validation answers 202 Accepted, always with this shape:

{
  "data": {
    "batchId": "batch_V1StGXR8Z5jdHi6BmyT2a",
    "livemode": true,
    "accepted": 2,
    "failed": 1,
    "emails": [
      { "id": "email_abc123xyz", "status": "queued", "to": ["alice@example.com"] },
      {
        "status": "failed",
        "to": ["bob@example.com"],
        "error": {
          "code": "SERVICE_UNAVAILABLE",
          "message": "Not sent and not charged: it could not be handed to the delivery queue. Send it again in a new batch, with a new Idempotency-Key."
        }
      },
      { "id": "email_def456uvw", "status": "queued", "to": ["carol@example.com"] }
    ]
  }
}
  • emails has one entry per email of the request, in the same order.
  • accepted counts the emails queued for delivery; failed counts the ones not sent and is always present, 0 on a full success.
  • A queued email has an id: read its status with GET /v1/emails/{id}, as for any other email. A failed email has no id and will not be sent.
  • A failed email's error.message says whether it was charged. When the quota could not be given back, the message says it may still count against the monthly quota. Nothing was sent in either case.
  • The 202 has no Retry-After header, even when some emails failed.

Retrying the failed emails

Send only the failed emails again, in a new batch with a new Idempotency-Key. Repeating the same request with the same key returns the original response, failures included, and sends nothing (see Idempotency).

If no email of the batch could be queued, the API answers 503 SERVICE_UNAVAILABLE instead of a 202: see Errors.

Idempotency

One Idempotency-Key covers the whole batch, for 24 hours. A replay with the same key and the same body returns the same response, a partial one included, and sends nothing. The same key with a different body answers 409 IDEMPOTENCY_CONFLICT; a request with a key whose first request is still running answers 409 IDEMPOTENCY_IN_PROGRESS. See Idempotency.

Quota

Each email counts against the monthly quota as one email per distinct recipient in its to, cc and bcc, ignoring case, exactly as on a single send. Recipients are deduplicated within one email, never across emails: the same address in two emails of the batch counts as two emails.

The batch reserves the sum of its emails at once, all or nothing. If the account has fewer emails left than the batch has distinct recipients in total, the API answers 429 QUOTA_EXCEEDED and nothing is written or sent:

{
  "error": {
    "code": "QUOTA_EXCEEDED",
    "message": "This batch has 120 recipients, more than the emails left on the free plan (3000 per month). Each distinct address in to, cc and bcc counts as one email. The quota is shared by every workspace of the account that owns this one.",
    "docs": "https://docs.tratto.email/en/docs/error-codes",
    "suggestion": "Send to fewer recipients, or have the workspace owner upgrade the plan at https://app.tratto.email/settings/billing."
  }
}

In test mode the daily cap of 100 test emails counts the sum of the batch in the same way.

Rate limits

A batch counts as one request on the limit of 100 requests per second per API key. On top of that, each key can make at most 10 batch requests per second; over it the API answers 429 RATE_LIMITED with Retry-After: 1 and the message Rate limit exceeded: 10 batch requests/second per API key. See Rate Limits.

Errors

StatusCodeWhenWritten or charged
422VALIDATION_ERROR0 or more than 100 emails; an invalid email; more than 50 recipients in one email; scheduledAt, attachments or an unknown field; a reserved header; no sender; an email over 900 KB or a batch over 9 MiB once renderedNothing
403FORBIDDENThe sender domain of an email is not verified (emails[i]: …). Checked only with a live keyNothing
413PAYLOAD_TOO_LARGERequest body over 5 MiBNothing
429QUOTA_EXCEEDEDFewer emails left than the batch has recipients in totalNothing
429RATE_LIMITEDMore than 10 batches per second per key, or another rate limit. Retry after Retry-After when present, otherwise after a short backoffNothing
409IDEMPOTENCY_CONFLICTSame Idempotency-Key, different bodyNothing
409IDEMPOTENCY_IN_PROGRESSSame Idempotency-Key while the first request is still runningNothing
503SERVICE_UNAVAILABLEToo many batches in progress, or no email could be queued. Comes with Retry-After: 1Nothing sent; nothing charged when too many batches are in progress, otherwise the message says whether it was charged
500INTERNAL_ERRORThe outcome of storing the emails is unknownNothing sent; may be charged

After a 503, retry the same request: the same Idempotency-Key can be reused, because no email of the batch was sent.

After a 500, no email of the batch was sent, but the batch may count against the monthly quota. Send it again: the same Idempotency-Key can be reused.

With the SDKs

Each SDK sends the batch with one call. The call returns the 202 response, also when some emails failed: check failed and the status of each email. Errors (422, 429, 503, 500 and the others in Errors) are raised as exceptions, and on a 422 the exception's details names the email by its position, as in /emails/3/to. The SDKs do not retry: follow the rules above, the same key after a 503 or 500, a new key for the failed emails.

Node.js

emails.sendBatch(emails, idempotencyKey?) requires @tratto/email 1.6.0 or later. See the Node.js SDK.

import { randomUUID } from 'node:crypto';
import { Tratto } from '@tratto/email';

const tratto = new Tratto(process.env.TRATTO_API_KEY!);

const batch = await tratto.emails.sendBatch(
  [
    { from: 'hello@yourdomain.com', to: 'alice@example.com', subject: 'Your receipt', text: 'Thanks, Alice.' },
    { from: 'hello@yourdomain.com', to: 'bob@example.com', subject: 'Your receipt', text: 'Thanks, Bob.' },
  ],
  randomUUID(),
);

console.log(`${batch.accepted} queued, ${batch.failed} failed`);
for (const e of batch.emails) {
  if (e.status === 'failed') console.error(e.to, e.error.code, e.error.message);
}

Python

emails.send_batch(emails, *, idempotency_key=None) requires tratto-email 2.1.0 or later. The key goes on the call: an item with its own idempotency_key raises ValueError. See the Python SDK.

import uuid
from tratto import SendEmailOptions, Tratto

client = Tratto('tratto_live_...')

result = client.emails.send_batch(
    [
        SendEmailOptions(from_='hello@yourdomain.com', to='alice@example.com', subject='Your receipt', text='Thanks, Alice.'),
        SendEmailOptions(from_='hello@yourdomain.com', to='bob@example.com', subject='Your receipt', text='Thanks, Bob.'),
    ],
    idempotency_key=str(uuid.uuid4()),
)

print(result['accepted'], result['failed'])
for item in result['emails']:  # same order as the request
    if item['status'] == 'failed':
        print(item['to'], item['error']['code'])

PHP

emails->sendBatch(array $emails, ?string $idempotencyKey = null) requires tratto/email 1.1.0 or later. See the PHP SDK.

$emails = [
    ['from' => 'hello@yourdomain.com', 'to' => ['alice@example.com'], 'subject' => 'Your receipt', 'text' => 'Thanks, Alice.'],
    ['from' => 'hello@yourdomain.com', 'to' => ['bob@example.com'], 'subject' => 'Your receipt', 'text' => 'Thanks, Bob.'],
];
$batch = $tratto->emails->sendBatch($emails, 'receipts-2026-10-21');

// Resend only the failed emails, in a new batch with a new key.
$failed = [];
foreach ($batch['emails'] as $i => $result) {
    if ($result['status'] === 'failed') {
        $failed[] = $emails[$i];
    }
}
if ($failed !== []) {
    $tratto->emails->sendBatch($failed, 'receipts-2026-10-21-retry-1');
}

Next Steps


Edit this page on GitHub

Last updated on