Broadcast batches
One template, many recipients, one API call. Runs on a dedicated queue so broadcasts cannot starve transactional sends.
Create a batch
curl https://api.tyxter.com/v1/batches \
-H "authorization: Bearer $TYXTER_API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "April promo",
"from": "pn_...",
"template": { "name": "promo_april", "language": "pt_BR" },
"recipients": [
{ "to": "+5511999999001", "variables": { "1": "Diego" } },
{ "to": "+5511999999002", "variables": { "1": "Ana" } }
]
}'Returns 202 with a message_batch.
The approved template version, not a batch field, chooses its parameter format. Inline recipients may use exact POSITIONAL numeric keys or exact NAMED BODY parameter names through the existing variables map. Every recipient is checked before the batch is written; a missing, extra, or wrong-format key returns 400 template_param_mismatch at recipients.<index>.variables. Inline batches cannot supply text-HEADER or dynamic URL button values, so those templates synchronously return param: template.
{
"name": "Named order updates",
"from": "pn_...",
"template": { "name": "order_tracking_update", "language": "pt_BR" },
"recipients": [
{ "to": "+5511999999001", "variables": { "customer_name": "Diego", "order_id": "ORD-1" } },
{ "to": "+5511999999002", "variables": { "customer_name": "Ana", "order_id": "ORD-2" } }
]
}Target a saved audience
Use audience_id instead of inline recipients when you want to reuse a saved contact list.
curl https://api.tyxter.com/v1/batches \
-H "authorization: Bearer $TYXTER_API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "April promo",
"from": "pn_...",
"template": { "name": "promo_april", "language": "pt_BR" },
"audience_id": "aud_..."
}'Audience membership is snapshotted when the batch is created. Existing batches are not affected by later audience edits.
An audience has no per-recipient template-variable source. Any approved template requiringPOSITIONAL or NAMED BODY variables, text-HEADER values, or dynamic URL button values is refused synchronously with template_param_mismatch and param: template; Tyxter does not interpolate contact fields.
Lifecycle
pending → just accepted, nothing enqueued yet
enqueuing → worker is walking the recipient list
sending → every recipient has been enqueued; sends are in flight
paused → enqueue cursor is preserved until resume
completed → every send terminal (sent / failed / opted_out)
failed → the batch itself failed before enqueuing could complete
cancelled → manual cancel; no more recipients are enqueuedWhat the worker does
- Pulls the
MessageBatchby id. - Pages through
recipients100 at a time, resuming fromenqueued_counton retry. - For each recipient, checks consent. Opted-out recipients get a
failedMessagerow witherror_code: contact_opted_out— no send is ever attempted. - Allowed recipients get a fresh
acceptedMessagerow and abroadcast.sendjob on theoutbound.broadcastqueue.
SendOutboundMessageUseCase processes both transactional and broadcast jobs. The separation is purely queue-level, which is enough: a broadcast of 50k messages can't delay a time-sensitive OTP because they never share a worker.Pause, resume, cancel
Pause stops future enqueue pages while preserving enqueued_count. Resume requests the enqueue worker again and continues from that cursor. Cancel is terminal for the batch enqueue worker.
Scheduling
Pass scheduled_for (ISO-8601) to delay the batch. The outbox publisher respects available_at, so the job simply won't be picked up until that time.
Tracking progress
Poll the batch row for enqueued_count, sent_count, and failed_count. Individual per-recipient messages carry the batch id in their metadata.batch_id so you can query GET /v1/messages for the full picture.
The failure export returns failed per-recipient rows for CSV or support workflows.