Skip to main content

Errors worth branching on

These failures cover every phase of the WhatsApp Business Calling lifecycle — the outbound initiate (POST /api/v1/whatsapp/calling/calls), the media update (PATCH /api/v1/whatsapp/calling/calls/:id/session), the hangup (DELETE /api/v1/whatsapp/calling/calls/:id), and the disposition tag (POST /api/v1/whatsapp/calling/calls/:id/disposition). Each block below is a full { error, meta } envelope as the API returns it, and the matrix at the bottom answers retry vs surface for the same classes. For the platform-wide decision table these branches plug into, see the error handling guide.

401 — Unauthorized

A 401 on this page is the bearer key failing before the route ran — it never means the resource is wrong. Rotate the key or re-mint the scoped token; retrying the same request changes nothing.

403 — Forbidden

A 403 means the key authenticated but the operation is gated by scope — check the key’s scopes on the developer page; a 403 is never a data-not-found shape.

422 — Schema

A 422 means the payload did not match the request schema — branch on error.details.field and resend with the MSISDN in E.164 form instead of blind-retrying the same body.

422 — Call permission not granted

The capability gate — fired when the recipient is not a known contact, the permission expired, or the recipient revoked it. A blind retry repeats the same 422; send a call-permission template from the thread first, or surface the dashboard’s grant CTA, then retry only after the recipient grants.

429 — Rate

The Retry-After header and error.details.retry_after are both set on every 429 — resend the SAME request after the lower of the two. The initiate pre-flight also rejects with WHATSAPP_CALLING_DAILY_CAP_EXCEEDED (429) once the tenant’s daily calling cap is crossed — same branch, different window.

402 — Feature gate

Surface the WABA enablement screen; the voice fallback still works for the number. The same 402 envelope carries WHATSAPP_CALLING_INSUFFICIENT_BALANCE when the wallet is below the worst-case per-minute estimate at initiate pre-flight — top up the wallet, then retry.

60-second retry matrix

Worked request and response samples

Copy the request as written, substitute your own ids, and compare the response envelope — a success in the same shape the error sections spell out. The chain below walks one call end to end: initiate → webhook → media update → hang up + disposition → history and cost roll-up.

1. Start a WhatsApp voice call

POST /api/v1/whatsapp/calling/calls
The recipient must already be a contact with an active call-permission grant — the 422 capability gate above is what this route answers with otherwise. Send the recipient as E.164, your WABA sender as from_phone_number_id, and the SDP offer when your media bridge already has one; omit sdp_offer and the callee side exchanges its offer via the accept step instead. The tenant’s own recording-consent and callback-context fields are optional. Request
The returned meta_call_id is the session id — the webhook events, the media update, and the hangup all join on it.

2. Webhook event on ring

Subscribe to whatsapp.call.connected — it fires when the recipient’s client rings, one per call leg, and includes the same meta_call_id so you join it to the initiate response. The sibling lifecycle events whatsapp.call.accepted (media path live) and whatsapp.call.terminated (call ended) carry the same payload shape.

3. Update session media

PATCH /api/v1/whatsapp/calling/calls/:meta_call_id/session
Flip the outbound audio track while the call is in progress — mute an agent microphone, restore it, or gate the track on a recording-consent withdrawal. The route echoes the session back so the dashboard can render the new track state without a re-fetch. Request

4. Hang up and tag a disposition

Two steps — end the call, then file the tenant-owned outcome code your dialer or QA workflow reports on. Hang up
DELETE /api/v1/whatsapp/calling/calls/:meta_call_id
Request
Apply a disposition code
POST /api/v1/whatsapp/calling/calls/:meta_call_id/disposition
Disposition codes are tenant-owned strings — define them in the dialer’s disposition-code registry and apply them after hangup (or while the call is live) so history queries group on them. Request

5. Call history and cost roll-up

Per-call cost rides on each history row (cost_amount_cents / cost_currency); the working-day aggregate for a destination or WABA comes from the analytics endpoint. Per-call history
GET /api/v1/whatsapp/calling/calls
Request
Window totals
GET /api/v1/whatsapp/calling/analytics
Request
Scope either query to a single destination by filtering history on q (free text matches phone columns, contact name, and conversation id); sum cost_amount_cents over the matching rows for a per-destination roll-up, or read totals.total_cost_cents for the WABA-wide 30-day number.