Messaging

Broadcast batches

One template, many recipients, one API call. Runs on a dedicated queue so broadcasts cannot starve transactional sends.

Create a batch

POST/v1/batches
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 enqueued

What the worker does

  1. Pulls the MessageBatch by id.
  2. Pages through recipients 100 at a time, resuming from enqueued_count on retry.
  3. For each recipient, checks consent. Opted-out recipients get a failed Message row with error_code: contact_opted_out — no send is ever attempted.
  4. Allowed recipients get a fresh accepted Message row and a broadcast.send job on the outbound.broadcast queue.
The same 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

POST/v1/batches/{id}/pause
POST/v1/batches/{id}/resume
POST/v1/batches/{id}/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

GET/v1/batches/{id}

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.

GET/v1/batches/{id}/failures

The failure export returns failed per-recipient rows for CSV or support workflows.