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.

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.

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.