Last updated 2026-05-28
WhatsApp channel
The WhatsApp channel delivers messages via Twilio's WhatsApp Business
pipeline. It reuses the same Twilio Client as
SMS — only the wire-level
formatting differs: both addresses are prefixed with
whatsapp: so Twilio routes through the WhatsApp pipeline
instead of SMS.
When you'd use this
Where WhatsApp is the dominant messaging app (LatAm, India, Indonesia, much of EMEA) and your users have opted in via a pre-approved template flow on the Twilio side.
Configuration
-e NOTIFY_WHATSAPP_PROVIDER=twilio \-e NOTIFY_WHATSAPP_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \-e NOTIFY_WHATSAPP_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \-e NOTIFY_WHATSAPP_FROM=+15555550000
Pass the From number without the whatsapp:
prefix — the provider adds it for you. Same Twilio credentials can
drive both the SMS and WhatsApp channels; the channel kind
disambiguates in the registry.
Library wiring
import ( "github.com/elloloop/notify" "github.com/elloloop/notify/channels/twilio")
client := twilio.NewClient(twilio.ClientConfig{ AccountSID: "ACxxx", AuthToken: "xxx", From: "+15555550000", // bare E.164; whatsapp: prefix added by provider})
registry := notify.NewProviderRegistry()registry.Register(twilio.NewWhatsApp(client))// You can register the SMS provider against the same client too:registry.Register(twilio.NewSMS(client))Sending a WhatsApp message
The producer supplies the recipient as a bare E.164 number — same as SMS — and the channel decides the routing.
curl -sX POST http://notify:8081/elloloop.notify.v1.NotificationInternalService/Notify \ -H 'Content-Type: application/json' \ -H "X-Notify-Internal-Token: $NOTIFY_INTERNAL_TOKEN" \ -d '{ "tenantId": "acme", "notificationId": "checkin-2026-05-27", "userIds": ["user-alice"], "channels": ["DELIVERY_CHANNEL_WHATSAPP"], "title": "Order shipped", "body": "Your order #4017 is on its way.", "addresses": { "user-alice": { "byChannel": { "whatsapp": "+15555550199" } } } }'
The provider builds the Twilio request with the
whatsapp: prefix automatically:
form := url.Values{}form.Set("To", ensureWhatsAppPrefix(to)) // "whatsapp:+15555550199"
fromKey, fromVal := p.client.pickFrom()if fromKey == "From" { fromVal = ensureWhatsAppPrefix(fromVal) // "whatsapp:+15555550000"}form.Set(fromKey, fromVal)form.Set("Body", pickBody(msg)) ensureWhatsAppPrefix is idempotent — calling it on
"whatsapp:+15555550000" returns the same string.
Callers that have already prefixed their config (legacy code paths,
custom Sender wrappers) won't get a double prefix.
Validation
The provider strips the whatsapp: prefix for validation
and runs the same E.164 structural check used by SMS. Empty,
non-E.164, or length-out-of-range addresses short-circuit to
StatusFailed without burning a Twilio request.
Body composition
Same as SMS: Body wins, falling back to Title.
Twilio's WhatsApp pipeline accepts plain text for in-session messages
and approved template references for out-of-session — at the proto
level notify does not model templates today; pass the rendered text
in Body.
Messaging Service caveat
Messaging Services do not take the whatsapp: prefix —
the prefix only applies to bare From numbers. The
provider handles this:
fromKey, fromVal := p.client.pickFrom()if fromKey == "From" { fromVal = ensureWhatsAppPrefix(fromVal)}// fromKey == "MessagingServiceSid" → no prefix appliedReceipts
Same shape as SMS: success returns the Twilio SID as
ProviderMessageID, failure surfaces the error.