Skip to main content

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

If the phone number’s country is on the blocklist the call returns 422 with code WHATSAPP_CALLING_COUNTRY_BLOCKED. To disable:
To read the current state without changing it:
The state response is cached for 5 minutes — the dashboard reflects mutations immediately because every enable / disable invalidates the cache.

Migrate an older WABA to calling-aware webhooks

WABAs onboarded before 2026-05-09 do not have the calls field on their Meta webhook subscription. Calling lifecycle webhooks will not arrive until you re-register. Owner / admin only:
The endpoint is idempotent — re-running on a connection that already subscribes to 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 the devotel-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 the devotel-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.
Read all permissions for a contact:
Response:
Permission state mutations are driven by Meta lifecycle webhooks — your application does NOT need to maintain this state manually.

Step 3 — Initiate an outbound call

Owner / admin / developer roles. The call is gated through the full pre-flight chain:
  1. WABA connection lookup (404 if unknown phone_number_id).
  2. Country blocklist (422 WHATSAPP_CALLING_COUNTRY_BLOCKED if blocked).
  3. Calling enabled on the number (422 WHATSAPP_CALLING_NOT_ENABLED).
  4. Recipient is a known contact (422 WHATSAPP_CALLING_NO_PERMISSION).
  5. Active call permission for (contact, waba, phone_number_id) (422 WHATSAPP_CALLING_NO_PERMISSION).
  6. Wallet balance covers the worst-case spend (402 WHATSAPP_CALLING_INSUFFICIENT_BALANCE).
  7. Daily cost cap not exceeded (429 WHATSAPP_CALLING_DAILY_CAP_EXCEEDED).
  8. Meta POST /{phone-number-id}/calls round-trip.

Body fields

Response

Operator actions on inbound calls

When a customer calls your business, you can accept or decline programmatically:
The dashboard’s incoming-call modal at /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 the wa_call_log_id you can use to join Orbit’s persisted call row. Example whatsapp.call.terminated payload:
Per-tenant webhook signing is the same Stripe/Svix-style HMAC scheme used across Orbit — see Webhooks → Security.

Billing

Pre-flight balance check

The platform refuses to initiate a call when the wallet cannot cover the worst-case spend:
Where 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:
Configure under Settings → WhatsApp → Calling → Daily spend cap (writes 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_logs with 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_request template.
  • Incoming-call modal — SSE-driven; mounted next to the PSTN softphone. Accepts or declines the call.

Common errors

API reference

See also