Skip to main content
Send one notification to thousands of recipients you supply yourself, with a job you can abandon before anything goes out. Looping over Send API calls has no undo. A bulk job does. You build it across as many ingestion calls as you need, and nothing sends until you run it. An ingestion that fails halfway has delivered nothing, so abandon that job and build a fresh one.

What you will build

Prerequisites

  • Your deduplicated recipient list, with an email or phone for each

Run a bulk job

Create the job with the notification and any shared data, ingest recipients into it, then run it once and poll for progress. Ingestion is open-ended and the job does not expire while you fill it. Running is a one-way door.
1

Create the job

defines the job with an event and any global data that applies to everyone. The required event is either a or a custom event ID mapped to a notification.
Returns 201 Created with the job ID you use for every call that follows:
You can also pass brand, locale, and override. To send something other than the notification attached to the event, add template. event stays required.
Make a retry safe.
Send an Idempotency-Key header and a retry replays the original result. A repeated create returns the first jobId instead of opening a second job.
2

Ingest your recipients

adds recipients to the job. Identify each one in to, and put their personal variables in data, which merges into the job’s global message.data.
Returns 200 OK, where total is the running count for the whole job rather than the current call:
Email jobs need each address in profile.email.
Provider routing ignores to.email, so a job built without profile.email runs clean and delivers nothing.
Each recipient accepts four fields:
  • to.user_id, who they are
  • profile, inline contact data
  • data, per-recipient variables
  • preferences, per-recipient overrides
Call this endpoint as often as you need, keeping each batch to around 1000 recipients. Larger batches can fail with a 502 instead of a validation error, so split big lists across several calls.
Ingestion does not deduplicate.
The same user_id sent twice becomes two entries and two messages, so clean your list before you ingest it.
3

Run the job

Once everyone is in, triggers the send. Courier fans out and delivers to each recipient.
Returns 202 Accepted with an empty body, and the job starts processing in the background.
A job runs once.
A second attempt returns 400 BulkJobDuplicateInvocationError, so reaching more recipients means creating a new job.
4

Track progress

returns the job’s counts and overall status.
  • received: recipients ingested.
  • enqueued: messages that reached the delivery pipeline.
  • failures: errors hit while processing.
  • status: CREATED, PROCESSING, COMPLETED, or ERROR.
COMPLETED and ERROR are both terminal.
Watch for either, or a failed job leaves you polling forever.

Verify

1

Confirm the counts line up

Poll the job until it reaches COMPLETED, then check that enqueued matches received and failures is zero.
2

Spot-check a recipient

Page through the job’s users with client.bulk.listUsers. Leave cursor off the first request, then keep passing paging.cursor back while paging.more is true. Each entry carries a recipient, a status of PENDING, ENQUEUED, or ERROR, and a messageId once enqueued.
3

Trace one message end to end

A messageId is the same ID a regular send returns, so look one up in and confirm it delivered.
To skip polling, with an outbound webhook and handle message:updated, which fires on every status change for every message the job produced.

Troubleshooting