Skip to content

Broadcasts, templates & opt-outs

One message to many people, the same words every time, and a way for anyone to say no — on WhatsApp and SMS alike.

Categories#

Every message carries a category: transactional (the default), otp or promotional. Only promotional messages are affected by opt-outs and sending windows. A receipt or a login code always goes.

Templates#

typescript
await wa.templates.put("order-shipped", "Hi {{name}}, order {{order}} has shipped.")

await wa.outbound.send({
  to: "+923001234567",
  template: "order-shipped",
  vars: { name: "Sara", order: "A-17" },
})

Rendered when the message is accepted. A missing variable is a 400, never a message that goes out saying “Hi {{name}}”. Editing a template never changes a message already queued. Templates work on /v1/send, /v1/sms and broadcasts.

Broadcasts#

typescript
const { broadcast, skipped } = await wa.broadcasts.create({
  name: "October offer",
  channel: "sms",               // or "whatsapp"
  template: "offer",            // or text: "..."
  vars: { code: "OCT10" },      // shared
  recipients: [
    { to: "+923001111111", vars: { name: "Ali" } },
    { to: "+923002222222", vars: { name: "Sana" } },
  ],
  sendAfter: new Date("2026-10-01T09:00:00+05:00"),
})

await wa.broadcasts.get(broadcast.id)    // counts by state
await wa.broadcasts.cancel(broadcast.id) // withdraw what hasn't left

A broadcast is ordinary queued messages that share an id, so pooling, pacing, caps, provider failover, opt-outs and sending windows apply to each one as they would to a single send.

At the doorWhat happens
Invalid or local-format numberSkipped and reported
Duplicate recipientSkipped and reported
Opted out (promotional)Skipped and reported
Missing template variableSkipped and reported
Over the daily SMS allowanceThe whole broadcast is refused — half a campaign is worse than none

Category defaults to promotional

That’s what most broadcasts are, and it’s the category that honours opt-outs and sending windows. Say transactional explicitly if it isn’t.

Opt-outs#

One list per project, across both channels: someone who opts out on WhatsApp doesn’t get the same promotion by SMS instead. A recipient is added when you call the API, or when they reply on WhatsApp with a message that is exactly an opt-out keyword — STOP, UNSUBSCRIBE or STOP ALL by default. “Please don’t stop the delivery” is a customer, not an opt-out.

typescript
await wa.optOuts.add("+923001234567")
await wa.optOuts.remove("+923001234567") // opted back in
await wa.optOuts.list()

Replies by SMS count too, through providers that sign them (Twilio, Telnyx): a STOP reply opts the sender out of every project that texted them through that provider in the last 30 days.

A promotional send to an opted-out recipient is refused with 409. One already queued when they opt out is cancelled when it falls due.

Sending windows#

Some markets allow promotions only during the day. A promotional message outside its destination’s window isn’t refused or failed — it waits, without using a retry, until the window opens. By default the UAE’s 07:00–21:00 applies to +971; set SEND_WINDOWS to change or add windows, and confirm the hours with your provider.

SEND_WINDOWS
{ "+971": "07:00-21:00@+04:00" }

Promotional sender ids#

Some markets register promotional senders separately — the UAE requires an AD- prefix. Give each provider its promotional sender, and promotions go out under it. Where a destination has a rule, only a provider with a matching sender may carry promotions there; with none, the promotion is refused with no_promotional_sender rather than sent under the wrong name.

engine environment
SMS_PROMO_SENDERS={"infobip":"AD-RentGari"}
SMS_SENDER_RULES={"+971":"AD-"}   # the default