Notix
Learn

One hundred emails in one call, or one email at nine tomorrow.

The single send endpoint is the right tool for a receipt or a reset link. Two other shapes come up in every product: a set of messages produced by one job, and a message that must go out at a chosen time. Notix handles both with the same email object. A batch is an array of up to 100 of them posted to POST /v1/emails/batch; a scheduled send is any email with a scheduledAt timestamp, which can be moved or cancelled until it goes.

Batch: the same email, one hundred times.

A batch request is a JSON array. Each element is a complete email, with its own recipients, template or body, attachments, headers and, if you want, its own scheduledAt. Nothing is shared between elements, so a digest with 100 personalised bodies and a plain notification to 100 addresses look the same to the API. The one thing a batch cannot do is render a React element on the client: pass HTML or a templateId with variables instead.

The batch total is what the per-email limits cannot see. One hundred emails that each pass the per-email attachment check could still add up to a request no server should accept, so the batch carries its own cap on attachment bytes, measured after base64 decoding, and the body as a whole is capped at 20 MB at the edge.

LimitValueNote
Emails per request100The 101st is refused with 400 BAD_REQUEST before anything is stored.
Request body20 MBApplied at the edge; over it the answer is 413 with no JSON body.
Attachments per email10Same as a single send; base64 in the body.
Attachments across the batch40 MB decodedStops 100 emails that each pass the per-email check from adding up to a request nothing would accept.
Idempotency-KeyUp to 256 charactersOne key for the whole request; a retry with the same key and body returns the same ids without sending again.
Billing0.4 units per emailBatch items are transactional sends, so 2.5 of them make one unit, the same as calling the single endpoint 100 times.

Validation runs on the whole request before anything is stored, so a batch with one bad element is refused as a batch, and the 400 names the field and the limit. A from address on a domain the team has not verified is refused the same way. Past validation, each message is created and queued on its own; a recipient on the suppression list is recorded as suppressed rather than sent, and a row that could not be created is simply absent from the response.

The call

One request, one id per message.

The response is a data array of email ids. Each id behaves exactly like one from the single endpoint: fetch it, watch its events, receive its webhooks.

POST /api/v1/emails/batch
curl -X POST https://app.usenotix.dev/api/v1/emails/batch \
  -H "Authorization: Bearer $NOTIX_API_KEY" \
  -H "Idempotency-Key: digest-2026-09-13" \
  -H "Content-Type: application/json" \
  -d '[
    { "from": "digest@acme.com", "to": "ada@example.com",
      "templateId": "tpl_weekly", "variables": { "name": "Ada" } },
    { "from": "digest@acme.com", "to": "grace@example.com",
      "templateId": "tpl_weekly", "variables": { "name": "Grace" },
      "scheduledAt": "2026-09-14T08:00:00+01:00" }
  ]'

# 200 { "data": [ { "emailId": "em_..." }, { "emailId": "em_..." } ] }
A batch response, two messages accepted
{
  "data": [
    { "emailId": "em_01j7x3k9q2" },
    { "emailId": "em_01j7x3k9q3" }
  ]
}

Scheduled: the same email, later.

Any send, single or inside a batch, accepts a scheduledAt timestamp in ISO 8601 form with a timezone offset. The message is stored with status SCHEDULED and handed to the queue with a delay; at the chosen time it goes out like any other send, through the same domain, suppression and webhook path. A timestamp in the past is treated as now. There is no stated upper bound on how far ahead you can schedule.

While a message is SCHEDULED it is yours to change. PATCH /v1/emails/{id} with a new scheduledAt moves it; POST /v1/emails/{id}/cancel cancels it, records a CANCELLED event and hands back any slot it had taken. Once the message has left the queue both calls answer 400 with the message “Email already processed”, so treat a 400 here as “too late” rather than as a bug.

A scheduled send is not part of today’s traffic. It holds no reservation against your daily or monthly allowance when you create it; the queue worker reserves for it on the day it goes out, and cancelling it costs nothing. The check that you are not already over your limit still runs at the request, so a team that is over today cannot queue tomorrow’s sends as a way round it.

Schedule, move, cancel
# Schedule: any send accepts scheduledAt (ISO 8601 with an offset)
curl -X POST https://app.usenotix.dev/api/v1/emails \
  -H "Authorization: Bearer $NOTIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "from": "billing@acme.com", "to": "ada@example.com",
        "subject": "Your invoice is due tomorrow",
        "html": "<p>Invoice 1042 is due on 15 September.</p>",
        "scheduledAt": "2026-09-14T09:00:00Z" }'
# 200 { "emailId": "em_..." }   status: SCHEDULED

# Move it
curl -X PATCH https://app.usenotix.dev/api/v1/emails/em_... \
  -H "Authorization: Bearer $NOTIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "scheduledAt": "2026-09-14T12:00:00Z" }'

# Cancel it
curl -X POST https://app.usenotix.dev/api/v1/emails/em_.../cancel \
  -H "Authorization: Bearer $NOTIX_API_KEY"

Retries, and what a batch costs.

A batch is the request most likely to be retried by accident: a job runner times out, a deploy restarts the worker, and the same 100 messages are posted again. Put an Idempotency-Key on every batch, named after the thing that produced it, such as the digest issue or the export id. A retry with the same key and body returns the original ids and sends nothing; a retry with a different body is a 409 so you notice the drift. Keys live for 24 hours, which covers a retry loop and not much more.

Billing is per message, not per request. Batch items are transactional sends, and a transactional email is 0.4 of a usage unit, so 100 of them cost 40 units, the same as 100 calls to the single endpoint. A scheduled send costs the same as an immediate one and is counted on the day it goes out. The pricing page has the unit price and what each plan includes.

FAQ

Questions, answered.

How do I send many emails with one API call?
POST an array of up to 100 email objects to /v1/emails/batch. Each object takes the same fields as a single send: from, to, subject or templateId, html or text, variables, attachments, headers and scheduledAt. The response is a data array with one emailId per accepted message. Above 100 messages, send more than one request, each with its own Idempotency-Key.
How do I schedule an email to send later through an API?
Add scheduledAt to any send, single or batch, as an ISO 8601 timestamp with a timezone offset. The message is stored with status SCHEDULED and goes out at that time. Until then, PATCH /v1/emails/{id} with a new scheduledAt moves it and POST /v1/emails/{id}/cancel cancels it. A scheduledAt in the past sends now.
Is a batch all-or-nothing?
No. A request that fails validation, such as a 101st email, a missing from, or a from address on a domain you have not verified, is refused as a whole and nothing is created. Once past validation, each message is created and queued on its own; one that cannot be created is left out of the response rather than failing the rest. If no message at all could be created the answer is a 500. Keep the returned ids next to your own records so you can tell which went.
Do the ids come back in the order I sent the messages?
Not necessarily. Notix groups a batch by from address so each sending domain is validated once, and the ids are returned in the order the messages were created. Use a distinct to address or a custom header per message if you need to map ids back, or send one from address per batch so the order is preserved.
Can I retry a batch safely?
Yes, with an Idempotency-Key header on the request. The key covers the whole batch: a retry with the same key and the same body returns the original ids with a 200 and sends nothing again; the same key with a different body answers 409 NOT_UNIQUE; a retry while the first request is still running also answers 409, so wait a moment and retry. Keys are kept for 24 hours. Use one per logical batch, such as the digest issue or the export run.
Does a scheduled send count against my limit when I schedule it, or when it goes out?
When it goes out. A send scheduled for later reserves nothing on the day you create it; the queue worker takes a slot on the day it actually sends. Your stored limit is still checked at the request, so a team already over its limit is refused straight away. Cancelling a scheduled send never charges you, and cancelling one that already reserved a slot gives the slot back.

The endpoints are in the API reference.

Batch, schedule, update and cancel, with every field and every error code.