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:

channels/twilio/whatsapp.go
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 applied

Receipts

Same shape as SMS: success returns the Twilio SID as ProviderMessageID, failure surfaces the error.

Related