Check permission before dialing
Complete Calling setup and connect a working media server first. Inbound success does not prove outbound Calling is available for the number or recipient. For a recipient identified by phone number, use the international number with country code, without+ or separators:
recipient=BSUID instead of user_wa_id in the permission query, and recipient instead of to when dialing. Preserve that identity from the webhook.
Check the returned action, not just permission.status:
The TypeScript SDK uses camelCase fields:
actionName and canPerformAction. The REST response uses snake_case. See SDK calls.Request permission in WhatsApp
During an open customer-service window, check thatsend_call_permission_request has can_perform_action: true, then send a free-form permission request:
recipient instead of to in this message request too.
Permission replies arrive on the messages field as interactive.call_permission_reply; keep that handler alongside your Calling receiver. Match the reply using the message’s from and/or from_user_id; a phone number can be omitted. Sending or reading the message does not grant permission. Wait for the recipient to approve it, then check start_call again. Outside the customer-service window, use the template permission flow. Create a template through /meta/whatsapp/{version}/{waba_id}/message_templates with a contextual BODY and a call_permission_request component. After Meta approves it, send that template through the number’s /messages endpoint. A normal quick-reply button does not grant Calling permission.
Send a real SDP offer
Create a WebRTC peer connection with an audio sender and receiver. Generate and set its local SDP offer, including the ICE candidates needed by your deployment. Keep that connection alive while the call rings. In this example,offerSdp is the offer produced by your media server, and recipientPhoneNumber is the recipient whose permission you just checked:
connect response means the request was accepted; it does not prove the recipient answered or that audio flows. Do not automatically retry a dial request after a timeout or uncertain response: it may already have created a paid call.
Apply the answer webhook
Use the same signed Meta receiver as for inbound calls. Route bydirection and SDP type:
Do not treat an outbound answer as a new inbound offer. If an answer or status arrives before the dial response returns the call ID, buffer it briefly by call ID and apply it once the pending session is associated. Ignore duplicate or late events after termination. Keep a timeout for unanswered calls, then clean up the pending session; clear this timer on
ACCEPTED so it cannot cut off an answered call.
The SDP answer can arrive while the handset is still ringing. Apply it to the pending connection, but wait for the matching call status ACCEPTED before starting the agent’s greeting. Handle REJECTED and terminate without starting an agent.
For appointment confirmations, say which business is calling and why, ask whether now is a good time, and check the caller’s actual booking before reading its details. End politely if it is the wrong recipient. A verbal confirmation is not an external calendar update unless your tool performs that update.
Test outbound calls
Use a consenting test recipient and check these cases separately:- Revoke permission in WhatsApp. Confirm your application blocks dialing when
start_callis unavailable. A deliberate API rejection test must be limited to the consenting test recipient. - Send a permission request, approve it on the handset, and check
start_callagain. - Answer a call. Verify two-way audio, the intended tool, and caller hangup.
- Answer another call and let the agent end it. Verify your bridge sends
terminateand releases the pipeline and media connection. - Decline a call, then leave another unanswered. Neither should start an agent; both must release the pending media connection.
- Read each saved call through List calls and match it to the webhook sequence by call ID.
COMPLETED alone as proof of pickup. In handset tests, Meta also sent that terminal status for declined and unanswered calls, without start/end times or duration. Track RINGING, ACCEPTED, and REJECTED separately from termination, and retain the events needed to distinguish those outcomes.

