API reference

Batches

Broadcast one template to a list of recipients over a dedicated queue.

Create a batch

POST/v1/batches202 Accepted

Request body

FieldTypeNotes
No fields.

Response

FieldTypeNotes
idrequiredstring
objectrequired"message_batch"
statusrequired"pending" | "enqueuing" | "sending" | "paused" | "completed" | "failed" | "cancelled"
environmentrequired"sandbox" | "production"
namerequiredstring | null
template_namerequiredstring | null
recipient_countrequiredinteger
range 0..9007199254740991
pacingrequiredobject | null
Accept-time forecast of how this batch will be paced against WhatsApp’s daily NEW-recipient allowance for the sending number. Returned on the create response. Null means "no estimate", never "no pacing" — either the forecast could not be made (the sending number could not be resolved, or its allowance is not a finite number to divide by, or the number has no finite cap at all), or you are reading the batch back later: the forecast is an accept-time snapshot that Tyxter neither stores nor recomputes, so retrieve and list always report null here. For live progress read enqueued_count / sent_count, and for the per-message reason read status_reason on the batch’s messages.
enqueued_countrequiredinteger
range 0..9007199254740991
sent_countrequiredinteger
range 0..9007199254740991
failed_countrequiredinteger
range 0..9007199254740991
recipient_guard_skipped_countinteger
Recipients skipped before dispatch with recipient_duplicate_message or recipient_frequency_capped. Included in failed_count; inspect each reason through the failure export. Present on servers with recipient guards.
range 0..9007199254740991
error_messagerequiredstring | null
trace_idrequiredstring
scheduled_forrequiredstring<ISO-8601> | null
started_atrequiredstring<ISO-8601> | null
completed_atrequiredstring<ISO-8601> | null
created_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updated_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$

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.

recipient_guard_skipped_count is included in failed_count. The batch can complete with every recipient skipped. Export failures for each reason and recovery instruction.

An inline recipient's to follows the same rule as a single send's recipient.id: 8–15 ASCII digits with an optional leading + and only ASCII spaces (U+0020), parentheses, dots, or hyphens as separators. A malformed to returns 400 invalid_phone_e164 with param: recipients.N.to for the first offending recipient, and the message lists up to five malformed indexes. No batch is accepted.

Before a batch is accepted, Tyxter resolves the approved template version. Inline recipient variables can satisfy modeled BODY values in the approved version's format: exact numeric keys for POSITIONAL or exact parameter-name keys for NAMED. Shared template.header_media can satisfy a modeled media header. A required text-HEADER or dynamic URL-button parameter has no batch request source, so it returns 400 template_param_mismatch with param: template. The audience and audience_id shapes have no per-recipient variable source in either format and likewise cannot supply BODY, text-HEADER, or dynamic URL-button parameters, although shared template.header_media remains available for a modeled media header. A mismatch returns 400 template_param_mismatch with a corrective param such as recipients.0.variables, template.header_media, or template. No batch was accepted, billed, queued, or sent to Meta. A later approval change or unmodeled Meta behavior can still fail an accepted recipient asynchronously with the same code.

The create response carries a pacing forecast when Tyxter can make one: how many recipients are expected to wait for WhatsApp’s daily NEW-recipient allowance on the sending number, and roughly how many daily windows the batch will span. A batch spanning more than one window is normal and not an error — the waiting recipients stay queued and send by themselves as allowance frees up. Both numbers are estimates: they move as other traffic on the same number consumes allowance, and recipients already inside an open conversation window cost nothing against it.

Retrieve a batch

GET/v1/batches/{id}

Response

FieldTypeNotes
idrequiredstring
objectrequired"message_batch"
statusrequired"pending" | "enqueuing" | "sending" | "paused" | "completed" | "failed" | "cancelled"
environmentrequired"sandbox" | "production"
namerequiredstring | null
template_namerequiredstring | null
recipient_countrequiredinteger
range 0..9007199254740991
pacingrequiredobject | null
Accept-time forecast of how this batch will be paced against WhatsApp’s daily NEW-recipient allowance for the sending number. Returned on the create response. Null means "no estimate", never "no pacing" — either the forecast could not be made (the sending number could not be resolved, or its allowance is not a finite number to divide by, or the number has no finite cap at all), or you are reading the batch back later: the forecast is an accept-time snapshot that Tyxter neither stores nor recomputes, so retrieve and list always report null here. For live progress read enqueued_count / sent_count, and for the per-message reason read status_reason on the batch’s messages.
enqueued_countrequiredinteger
range 0..9007199254740991
sent_countrequiredinteger
range 0..9007199254740991
failed_countrequiredinteger
range 0..9007199254740991
recipient_guard_skipped_countinteger
Recipients skipped before dispatch with recipient_duplicate_message or recipient_frequency_capped. Included in failed_count; inspect each reason through the failure export. Present on servers with recipient guards.
range 0..9007199254740991
error_messagerequiredstring | null
trace_idrequiredstring
scheduled_forrequiredstring<ISO-8601> | null
started_atrequiredstring<ISO-8601> | null
completed_atrequiredstring<ISO-8601> | null
created_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updated_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$

pacing is always null here. The forecast is an accept-time snapshot that Tyxter neither stores nor recomputes, so read it from the create response. For live progress use enqueued_count and sent_count; for the per-message reason use status_reason on the batch’s messages.

List batches

GET/v1/batches

Response

FieldTypeNotes
objectrequired"list"
datarequiredobject[]
has_morerequiredboolean
next_cursorrequiredstring | null

Control a batch

POST/v1/batches/{id}/pause
POST/v1/batches/{id}/resume
POST/v1/batches/{id}/cancel

Response

FieldTypeNotes
idrequiredstring
objectrequired"message_batch"
statusrequired"pending" | "enqueuing" | "sending" | "paused" | "completed" | "failed" | "cancelled"
environmentrequired"sandbox" | "production"
namerequiredstring | null
template_namerequiredstring | null
recipient_countrequiredinteger
range 0..9007199254740991
pacingrequiredobject | null
Accept-time forecast of how this batch will be paced against WhatsApp’s daily NEW-recipient allowance for the sending number. Returned on the create response. Null means "no estimate", never "no pacing" — either the forecast could not be made (the sending number could not be resolved, or its allowance is not a finite number to divide by, or the number has no finite cap at all), or you are reading the batch back later: the forecast is an accept-time snapshot that Tyxter neither stores nor recomputes, so retrieve and list always report null here. For live progress read enqueued_count / sent_count, and for the per-message reason read status_reason on the batch’s messages.
enqueued_countrequiredinteger
range 0..9007199254740991
sent_countrequiredinteger
range 0..9007199254740991
failed_countrequiredinteger
range 0..9007199254740991
recipient_guard_skipped_countinteger
Recipients skipped before dispatch with recipient_duplicate_message or recipient_frequency_capped. Included in failed_count; inspect each reason through the failure export. Present on servers with recipient guards.
range 0..9007199254740991
error_messagerequiredstring | null
trace_idrequiredstring
scheduled_forrequiredstring<ISO-8601> | null
started_atrequiredstring<ISO-8601> | null
completed_atrequiredstring<ISO-8601> | null
created_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
updated_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$

Export failures

GET/v1/batches/{id}/failures

Response

FieldTypeNotes
objectrequired"message_batch_failure_export"
batch_idrequiredstring
generated_atrequiredstring<ISO-8601>
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
rowsrequiredobject[]