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. - Mandatory Tyxter guards apply to marketing in sandbox and production. Identical template name, language, parameters and media content remain blocked until an inbound message after the previous attempt; there is no timed duplicate expiry within retained message history. Reuploading identical bytes under a new asset ID does not change content. A read receipt is not engagement. Different content on a healthy sender is allowed. A seven-day sender/recipient cooldown applies only when phone or template quality is YELLOW/RED or a policy warning occurred within seven days. An inbound message after the latest marketing attempt exempts duplication and that cooldown, even after the 24-hour service window closes. General opt-outs and Meta marketing stop preferences take priority; only a Meta marketing resume clears that provider preference. Meta 131049 requires at least 24 hours before retrying even after engagement; two consecutive 131026 refusals or a latest 130472 holdout require new inbound engagement. Utility and authentication are outside these marketing guards; general consent still applies. HTTP 400 recipient_duplicate_message or recipient_frequency_capped includes error.details.guard_reason, error.details.next_allowed_at and error.feedback. next_allowed_at is null for pending sends or recovery requiring engagement or consent; never schedule a retry from null. Missing or unreadable policy, unclassifiable pending templates, or new marketing media without verified hashes returns 503 recipient_cadence_policy_unavailable. Legacy media equality without a retained hash remains unknown and is logged; no migration invents historic media equality. Queued templates are checked again before dispatch. Batch failure exports expose guard_reason and next_allowed_at when the failure receipt is retained. Recorded provider rejections count as attempts. Guard skips create no provider attempt or message-processing charge and do not renew the cooldown. Customers cannot disable or shorten these protections. They do not guarantee delivery or improved sender quality. Protected recipients appear in
recipient_guard_skipped_countwithinfailed_count. Export failures to inspect each reason and recovery instruction. - 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 (it replaces any batch_id you pass in the batch metadata) so you can query GET /v1/messages for the full picture.
The failure export returns failed per-recipient rows for CSV or support workflows.