Skip to navigation

WhatsApp lines

View as Markdown

WhatsApp is a channel on an iMessage line, not a separate kind of number. A line that carries it can send iMessage and WhatsApp from the same phone number — so unlike every other channel, you say which one you mean on each message.

WhatsApp is in beta and enabled per account. Talk to us to request access.

Until it’s enabled, the WhatsApp provisioning endpoints answer 404 rather than 403. That’s deliberate: to an account without access the capability doesn’t exist, which is also what the dashboard shows — the number’s menu has no such item. A 403 would advertise something you can’t reach and can’t ask for.

Add WhatsApp to a line

Two entry points. Buy a new iMessage number with WhatsApp included:

dial number purchase --include-imessage --whatsapp \
--inbound-instruction "You are my receptionist." \
--explicit-programmatic-consent "User modal confirmation"

…or add it to an iMessage number you already hold:

dial number whatsapp pn_123

whatsapp: true is only valid alongside capabilities: ["sms","call","imessage"] — WhatsApp rides on an iMessage line, so there’s no standard-number combination to ask for.

The two paths differ in how long WhatsApp takes:

  • A new number arrives ready. Provisioning with whatsapp: true hands you a number whose WhatsApp is already set up and warmed: whatsapp.status is ready in the response itself. areaCode is not honored for these numbers. When no WhatsApp number is available right now the request is refused with 503 whatsapp_unavailable — nothing was bought or charged, so retrying shortly is safe.
  • Connecting your own number takes hours. WhatsApp is registered on the number (provisioning), and then the number is warmed up (warming_up) for about 6 hours before its first use, and only then becomes ready. Wait for the number.status_changed event that reports ready — with a long timeout — rather than polling tightly. If the warm-up cannot finish, the track ends failed with a reason you can show.

Sending on the channel is rejected until whatsapp.status is ready.

What the number tells you

A WhatsApp-capable line reports two things nothing else does:

{
"id": "pn_123",
"capabilities": ["sms", "call", "imessage", "whatsapp"],
"setupStatus": "ready",
"whatsapp": {
"status": "ready",
"error": null,
"retryAvailableAt": null,
"autoRetryScheduled": false
}
}
  • capabilities gains whatsapp only once the channel actually works — at ready, never during provisioning or warming_up. Before that the line reports ["sms","call","imessage"], because a capability you can see but can’t use is worse than one that shows up late.
  • whatsapp.status is independent of setupStatus. They’re separate tracks on the same line: voice caller-ID can be ready while WhatsApp is failed, and the other way round. One status field could never carry both, so there are two.
  • whatsapp.retryAvailableAt is when the channel will accept another verification attempt, or null. It’s the one failure you can neither fix nor retry around — if it’s set and in the future, a retry is refused until it lifts. Show the time rather than a button that won’t work.
  • whatsapp.autoRetryScheduled tells you whether to wait or to act. A cooling-off period is the one failure that resolves on its own clock, so Dial waits it out and re-runs setup for you — a bounded number of times, after which it stops and leaves the line to you. While this is true, there is nothing to do: say a retry is coming and let it. When it’s false and retryAvailableAt is null, the next attempt is yours to ask for.
  • whatsapp.status: "paused" means Dial has temporarily paused WhatsApp sending on the line. Nothing is wrong with the number. While it lasts, sends with channel: "whatsapp" are refused with 503 whatsapp_unavailable; receiving is unaffected, so whatsapp stays in capabilities. It returns to ready when Dial resumes sending. Both moves arrive as number.status_changed events, so you can stop sending when a line pauses and start again when it resumes instead of retrying into the 503.
  • whatsapp.status: "banned" means WhatsApp withdrew a line that was working. It is not a setup failure — previousStatus is "ready" — and it is permanent: retryAvailableAt is always null, no retry helps, and whatsapp leaves capabilities, so sends on that channel are refused from then on. The number’s other channels are unaffected. Registering WhatsApp on the number again is the only route back. You learn about it through number.status_changed, which is why that event is worth consuming for the life of a number and not only while one is being set up.

Choose the channel

channel is accepted both when sending a message (POST /api/v1/messages) and on a typing indicator (POST /api/v1/typing, or dial typing start):

{ "channel": "imessage" } // the iMessage rail, with its automatic RCS/SMS fallback
{ "channel": "whatsapp" } // the WhatsApp rail
dial message --to +14155550123 --body "Hello over WhatsApp" --from-number pn_123 --channel whatsapp

Omitting channel keeps the line’s own default, so nothing you already send changes: a standard number sends SMS, an iMessage number sends iMessage. You only need channel on a line that carries both rails — and passing it explicitly is a good habit there, because “the default” is the one thing that could change under you.

Two ways it’s rejected, both 400:

  • channel: "whatsapp" on a line whose WhatsApp track isn’t live — “WhatsApp isn’t connected on this number.”
  • channel: "imessage" on a number with no iMessage rail (a standard SMS/call number) — the number can’t do it, so the ask is refused rather than quietly delivered as an SMS.

What WhatsApp does differently

Media goes out one attachment at a time. A WhatsApp send takes the same media / mediaUrls fields as every other channel, but carries at most one attachment — more is rejected with 400 (“A WhatsApp message can carry at most one attachment.”). Supported types match the rest of Send a message: images, audio, video, PDF, vCard, and iCalendar.

dial message --to +14155550123 --from-number pn_123 --channel whatsapp \
--media ./party.jpg --body "See you Saturday"
  • A caption only rides on an image or a video. WhatsApp shows no caption on audio or a document, so a body alongside one is rejected with 400 rather than sent without its text. Send the text as its own message.
  • A lone audio attachment is a voice note by default. Set forceAudioFile: true (--force-audio-file) to deliver it as a regular audio file instead — the same flag as on iMessage numbers.
  • A vCard arrives as a document, not a native contact card.

Inbound attachments are delivered normally. A photo someone sends to your WhatsApp line arrives as a regular media array on the message.received event, hosted by Dial like any other attachment. That includes a shared contact: a contact card someone sends you arrives as a media item with contentType: "text/vcard", and its url serves the .vcf file itself, ready to import into any address book. The message body carries the contact’s display name, so you can show who was shared without parsing the card.

Replies and reactions are native. Reply to a message works on WhatsApp in both forms, in a one-to-one conversation or a group:

dial message reply msg_123 --react "🎉"
dial message reply msg_123 --body "Count me in"
  • A reaction lands on the target message itself, exactly as one added in the app. Any single emoji works, and the six reaction names map to their emoji — 👍 like, ❤️ love, 👎 dislike, 😂 laugh, ‼️ emphasize, ❓ question.
  • A body reply is a native quoted reply — the target is quoted above your text. A reply carries text only; to send media, send a new message into the conversation.
  • The target can be in either direction — a message you received or one you sent — but not a reaction.

Delivery status firms up after the send. A WhatsApp send returns once WhatsApp’s server has the message, so it comes back with deliveryState: "pending" (status: "sent") — or already delivered, when the recipient’s handset acknowledged it before the call returned. Receipts then arrive as message.status_changed events and the state climbs to delivered and read from there.

WhatsApp can refuse a message after taking it. When WhatsApp’s server rejects a message the line already sent — typically a number that can’t start new conversations yet — the outcome depends on when Dial learns it. If the refusal is already known when the send returns, the call fails with 403 whatsapp_limited and the message is recorded with deliveryState: "failed". If it lands later — before any of the recipient’s devices has acknowledged the message — the message moves to deliveryState: "undelivered" with a deliveryError, through the same message.status_changed event. That verdict is not final on this channel: a receipt from the recipient’s phone can still arrive afterwards, and then a further event moves the message to delivered. A message already reported delivered stays delivered.

Reads are reported. Unlike SMS, WhatsApp tells you when a message was read, so readState and readAt are meaningful on this channel. In a group, both delivery and read mean every recipient — on this channel that verdict arrives already tallied for you.

One send at a time per line. A WhatsApp line sends messages one after another. A second send that arrives while the first is in flight is refused, not queued — so you get an error immediately and retry, rather than waiting behind something you can’t see.

Messages from one number are spaced. WhatsApp suspends numbers that behave like software, so consecutive sends on a line are separated by at least a second. A send that arrives inside that gap is rejected with 429 and a Retry-After telling you when to come back — your request is never held open waiting.

Volume and unanswered replies are capped. Each WhatsApp number has a daily cap, a per-recipient cap, and a limit of three unanswered messages to one person. Over any of them, a send is rejected with 429 (rate_limited). See WhatsApp sending limits before you plan a campaign.

WhatsApp can limit a number’s access. While it does, the number can’t start a conversation with someone it hasn’t talked to before: a send or a typing indicator toward them is rejected with 403 (whatsapp_limited) and nothing is sent. Replies to people who have messaged the number still go through. See Limited WhatsApp access.

A WhatsApp text message types before it’s sent. The recipient sees “typing…” for about 200-250 ms per character, up to about 30 seconds however long the message is, and then the message arrives, the way it would from a person on their phone. The same goes for a text reply. The send returns once the message is out, so it takes that much longer: about 7 seconds for a 30-character message.

Typing indicators work inside a group too — address it with groupId instead of toNumber, and every member sees it. See Group conversations.

Next