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#
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#
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 door | What happens |
|---|---|
| Invalid or local-format number | Skipped and reported |
| Duplicate recipient | Skipped and reported |
| Opted out (promotional) | Skipped and reported |
| Missing template variable | Skipped and reported |
| Over the daily SMS allowance | The whole broadcast is refused — half a campaign is worse than none |
Category defaults to promotional
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.
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.
{ "+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.
SMS_PROMO_SENDERS={"infobip":"AD-RentGari"}
SMS_SENDER_RULES={"+971":"AD-"} # the default