Skip to main content

Documentation Index

Fetch the complete documentation index at: https://orbit-docs.devotel.io/llms.txt

Use this file to discover all available pages before exploring further.

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

CapabilityStatus
Inbound calls (customer calls your business)GA in 195+ countries
Outbound calls (business calls customer)Requires explicit permission
Audio legWhatsApp voice transport (no PSTN cost)
Per-second metering, 1-minute floorYes
Recording (optional, opt-in)Phase 6 — opt-in per call
Multi-WABA per organisationYes

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:
ISO 3166-1 alpha-2Country
USUnited States
CACanada
EGEgypt
VNVietnam
NGNigeria
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.
curl -X POST https://orbit-api.devotel.io/api/v1/whatsapp/connections/{phoneNumberId}/calling/enable \
  -H "X-API-Key: dv_live_sk_..."

Response

{
  "data": {
    "phone_number_id": "1234567890",
    "calling_enabled": true,
    "country_code": "GB",
    "updated_at": "2026-05-09T10:14:22Z"
  },
  "meta": {
    "request_id": "req_xyz789",
    "timestamp": "2026-05-09T10:14:22Z"
  }
}
If the phone number’s country is on the blocklist the call returns 422 with code WHATSAPP_CALLING_COUNTRY_BLOCKED. To disable:
curl -X POST https://orbit-api.devotel.io/api/v1/whatsapp/connections/{phoneNumberId}/calling/disable \
  -H "X-API-Key: dv_live_sk_..."
To read the current state without changing it:
curl https://orbit-api.devotel.io/api/v1/whatsapp/connections/{phoneNumberId}/calling \
  -H "X-API-Key: dv_live_sk_..."
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:
curl -X POST https://orbit-api.devotel.io/api/v1/whatsapp/connections/{phoneNumberId}/calling/register-webhook \
  -H "X-API-Key: dv_live_sk_..."
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.

Step 2 — Calling permissions

Outbound calls require an active permission row for the (contact, waba, phone_number_id) triple. Permission states:
StateMeaning
noneThe contact has never granted call permission for this WABA / number.
grantedAn active grant exists. May have a expires_at if it came from an in-app prompt.
revokedThe contact explicitly toggled off call permission. Outbound calls fail.
expiredThe TTL on an in-app-prompt grant has lapsed. Behaves the same as none.
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:
curl -G https://orbit-api.devotel.io/api/v1/whatsapp/calling/permissions \
  -H "X-API-Key: dv_live_sk_..." \
  --data-urlencode "contact_id=ctc_abc123"
Response:
{
  "data": {
    "items": [
      {
        "id": "waCallPerm_...",
        "contact_id": "ctc_abc123",
        "waba_id": "111222333",
        "phone_number_id": "1234567890",
        "status": "granted",
        "granted_at": "2026-05-09T10:00:00Z",
        "expires_at": null,
        "source": "template_authorized",
        "is_expired": false
      }
    ]
  }
}
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.
curl -X POST https://orbit-api.devotel.io/api/v1/whatsapp/calling/calls \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+447700900123",
    "from_phone_number_id": "1234567890",
    "conversation_id": "conv_abc",
    "recording_consent": false,
    "biz_opaque_callback_data": "support-call-9821"
  }'

Body fields

FieldTypeRequiredNotes
tostringyesE.164 destination. Recipient must be a known contact with active permission.
from_phone_number_idstringyesMeta phone number ID of the calling business sender.
conversation_idstringnoOrbit conversation ID to thread the call into.
recording_consentbooleannotrue = recording will run if Phase 6 recording is enabled.
biz_opaque_callback_datastringnoUp to 512 bytes echoed back on lifecycle webhooks.
sdp_offerstringnoOverride the platform-supplied SDP offer. Ignored unless you control your own audio leg.

Response

{
  "data": {
    "ok": true,
    "meta_call_id": "wacid.HBgMNzcwMDkwMDEyMxUCABEYEjg4QkU0NDQyN0M5NUVBOEU2NQA=",
    "wa_call_log_id": "waCall_abc123",
    "from": "+18005551234",
    "to": "+447700900123",
    "country_code": "GB"
  },
  "meta": { "request_id": "req_xyz789", "timestamp": "2026-05-09T10:14:22Z" }
}

Operator actions on inbound calls

When a customer calls your business, you can accept or decline programmatically:
# Accept
curl -X POST https://orbit-api.devotel.io/api/v1/whatsapp/calling/calls/{metaCallId}/accept \
  -H "X-API-Key: dv_live_sk_..."

# Decline (preferred over leaving the call to ring out — no billing impact, cleaner UX)
curl -X POST https://orbit-api.devotel.io/api/v1/whatsapp/calling/calls/{metaCallId}/decline \
  -H "X-API-Key: dv_live_sk_..."
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.
Event typeTriggered when
whatsapp.call.receivedInbound ring lands on your number (legacy event, preserved for BAU).
whatsapp.call.connectedOutbound call has been initiated by Meta.
whatsapp.call.acceptedInbound call was accepted (by you or by an agent).
whatsapp.call.terminatedCall ended. Carries duration_seconds, end_reason, and final cost.
whatsapp.call.permission_grantedCustomer granted call permission for a (waba, phone_number_id).
whatsapp.call.permission_revokedCustomer toggled off call permission.
Example whatsapp.call.terminated payload:
{
  "type": "whatsapp.call.terminated",
  "data": {
    "wa_call_log_id": "waCall_abc123",
    "meta_call_id": "wacid....",
    "waba_id": "111222333",
    "phone_number_id": "1234567890",
    "from": "+18005551234",
    "to": "+447700900123",
    "direction": "outbound",
    "status": "completed",
    "duration_seconds": 174,
    "end_reason": "normal_clearing",
    "cost_amount_cents": 6,
    "cost_currency": "USD",
    "created_at": "2026-05-09T10:14:22Z",
    "ended_at": "2026-05-09T10:17:16Z"
  }
}
Per-tenant webhook signing is the same Stripe/Svix-style HMAC scheme used across Orbit — see Webhooks → Security.

Billing

RuleDetail
GranularityPer-second metering, 1-minute floor. A 5-second answered call bills 1 minute.
DirectionBoth inbound AND outbound calls are billable.
Failed / no-answerNOT billed. Cost is stamped 0 for idempotency.
Recording (Phase 6)Separate billable item; refundable independently of the call leg.
Pricing tiersLow / mid / high tier per destination country (public.wa_calling_rates seed).
CurrencyMeta bills the platform in USD; the wallet debit converts to your wallet currency at end-of-call. The audit ledger captures both source USD cents and the debited amount.

Pre-flight balance check

The platform refuses to initiate a call when the wallet cannot cover the worst-case spend:
worst_case_cents = max_call_minutes × per_minute_rate_cents
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:
HTTP/1.1 429 Too Many Requests
{
  "error": {
    "code": "WHATSAPP_CALLING_DAILY_CAP_EXCEEDED",
    "message": "Today's WhatsApp calling spend has reached the configured daily cap."
  }
}
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

CodeHTTPCause
WHATSAPP_NOT_CONNECTED404No WhatsApp connection for the supplied from_phone_number_id.
WHATSAPP_CONNECTION_INVALID422Connection has no resolved WABA id; reconnect via Embedded Signup.
WHATSAPP_CALLING_COUNTRY_BLOCKED422Phone number’s country is on Meta’s blocklist.
WHATSAPP_CALLING_NOT_ENABLED422Calling not enabled on the WABA phone number; call /calling/enable first.
WHATSAPP_CALLING_NO_PERMISSION422Recipient is not a contact, or has no active call permission. Send a call_permission_request template first.
WHATSAPP_CALLING_INSUFFICIENT_BALANCE402Wallet balance cannot cover the worst-case spend. Top up.
WHATSAPP_CALLING_DAILY_CAP_EXCEEDED429Per-org daily cap exceeded for today (UTC).

API reference

See also