WhatsApp Business Calling
WhatsApp Business Calling lets a customer place a one-tap voice call to your business directly from a WhatsApp chat, and lets your business call any contact who has granted call permission. Calls run over WhatsApp’s voice transport — no PSTN, no SIP trunk on the customer side — and are billed per-minute by destination country. This page covers the full lifecycle: enabling calling on a number, the permission state machine, initiating outbound calls, lifecycle webhooks, and billing.Capability matrix
Country availability
Meta does not yet permit WhatsApp Business Calling in every country. Orbit refuses to enable calling on a phone number whose country is on the blocklist; the block is enforced server-side and surfaced in the dashboard. The following countries are blocked at the time of writing:
Calling enables fine on numbers from any other country. Meta updates this list periodically; the dashboard surfaces the live list under Channels → WhatsApp → Calling.
Step 1 — Enable calling on a phone number
Calling is opt-in per WABA phone number. Owner / admin tier roles only.Response
422 with code WHATSAPP_CALLING_COUNTRY_BLOCKED.
To disable:
Migrate an older WABA to calling-aware webhooks
WABAs onboarded before 2026-05-09 do not have thecalls field on their Meta webhook subscription. Calling lifecycle webhooks will not arrive until you re-register. Owner / admin only:
calls is a 200 no-op. Newly-onboarded numbers register the field automatically during Embedded Signup.
Platform-shared calling phone (operator setup)
When a tenant has no calling-enabled WABA of their own, Orbit can originate from a platform-shared demo calling number. This is operator configuration, not a tenant API surface — set these four env vars (mirrored into thedevotel-api-env K8s Secret):
If the PIN (or phone-id / access token) is unset, shared calling registration soft-degrades rather than crashing — calls from tenants without their own calling WABA fail with
WHATSAPP_NO_CALLING_PHONE.
Media relay (rtpengine NG control)
WhatsApp Business Calling media rides a media relay that bridges Meta’s WebRTC (DTLS-SRTP) leg to the plain-RTP softswitch leg. The API programs that relay by sending rtpengine NG-control commands over UDP. Point it at your rtpengine instance with these two operator env vars (mirrored into thedevotel-api-env K8s Secret). Neither is secret.
When
DEVOTEL_WA_CALLING_RTPENGINE_NG_HOST is unset, the relay transmitter is a no-op: the call lifecycle (signalling, billing, webhooks) still completes, but the media leg is never programmed and the call degrades to signalling-only. This lets you provision the media gateway and carrier before the NG endpoint is reachable from the API without breaking the call lifecycle in that window. Set the host once rtpengine is reachable; leave the port unset to accept the 22222 default.
Step 2 — Calling permissions
Outbound calls require an active permission row for the(contact, waba, phone_number_id) triple. Permission states:
Grants come from three sources:
- Template-authorized — the customer replies to a template with category
call_permission_request. Persistent grant (no TTL). - In-app prompt — Meta surfaces the call-permission UI in the WhatsApp client during a chat. Often time-bounded (24h).
- Explicit consent — the customer taps a
wa_call_permission_grant_*button you sent. Persistent grant.
Step 3 — Initiate an outbound call
Owner / admin / developer roles. The call is gated through the full pre-flight chain:- WABA connection lookup (404 if unknown phone_number_id).
- Country blocklist (422
WHATSAPP_CALLING_COUNTRY_BLOCKEDif blocked). - Calling enabled on the number (422
WHATSAPP_CALLING_NOT_ENABLED). - Recipient is a known contact (422
WHATSAPP_CALLING_NO_PERMISSION). - Active call permission for
(contact, waba, phone_number_id)(422WHATSAPP_CALLING_NO_PERMISSION). - Wallet balance covers the worst-case spend (402
WHATSAPP_CALLING_INSUFFICIENT_BALANCE). - Daily cost cap not exceeded (429
WHATSAPP_CALLING_DAILY_CAP_EXCEEDED). - Meta
POST /{phone-number-id}/callsround-trip.
Body fields
Response
Operator actions on inbound calls
When a customer calls your business, you can accept or decline programmatically:/messages/whatsapp ties the same actions to a UI button.
Lifecycle webhooks
Subscribe to these event types via your tenant webhook endpoint. Each event carries thewa_call_log_id you can use to join Orbit’s persisted call row.
Example
whatsapp.call.terminated payload:
Billing
Pre-flight balance check
The platform refuses to initiate a call when the wallet cannot cover the worst-case spend:max_call_minutes defaults to 60 (Meta’s documented soft-cap on a single call). Override per-org via the dashboard at Settings → WhatsApp → Calling → Max call minutes (writes organizations.settings.whatsapp_calling_max_call_minutes).
When the wallet is below the worst-case the API responds 402 WHATSAPP_CALLING_INSUFFICIENT_BALANCE. Top up to retry.
Daily cost cap
Optional opt-in per organisation. When set, the platform tracks total WA-calling spend per UTC day per org and refuses new calls once the cap is reached:organizations.settings.whatsapp_calling_daily_cap_cents). Default is unlimited (cap = 0).
Dashboard surfaces
The WhatsApp dashboard at/messages/whatsapp exposes:
- Calls tab — paginated list of
wa_call_logswith PII-masked phone numbers for viewer-tier roles. - KPI panel — last-30-day call volume, accept rate, average duration, total spend.
- Per-conversation Call button — surfaced inside the inbox conversation header. Disabled (with permission-state hint) when permission is missing / expired / revoked.
- Permission indicator — shows the current permission state in the contact-details panel; offers a “Send re-engagement template” CTA that pre-fills a compose dialog with the
call_permission_requesttemplate. - Incoming-call modal — SSE-driven; mounted next to the PSTN softphone. Accepts or declines the call.
Common errors
API reference
- WhatsApp API → Calls — full endpoint reference for the calling surface.
- Messaging API — for sending the
call_permission_requesttemplate that elicits a grant.