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’ll 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 doesn’t 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:
Provider routing reads profile.email, not to.email.
Put each recipient’s address in profile.email so the job delivers to it.
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 doesn’t deduplicate.
The same user_id ingested twice becomes two messages, so deduplicate 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.
Running it again returns 400 BulkJobDuplicateInvocationError. To reach more recipients, create 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.
Stop polling on either one. A job that ends in ERROR stays there.

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