Skip to main content

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:
For a user identified only by a business-scoped user ID, use 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:
If false, do not dial. Have the recipient grant Calling permission through WhatsApp’s supported permission flow, then check again. Limits can prevent a call even while a permission is present. Follow Meta’s permission documentation for the current request flows and limits.
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 that send_call_permission_request has can_perform_action: true, then send a free-form permission request:
For BSUID-only users, use 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:
Store the call ID and associate it with your pending media session. A successful 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 by direction 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:
  1. Revoke permission in WhatsApp. Confirm your application blocks dialing when start_call is unavailable. A deliberate API rejection test must be limited to the consenting test recipient.
  2. Send a permission request, approve it on the handset, and check start_call again.
  3. Answer a call. Verify two-way audio, the intended tool, and caller hangup.
  4. Answer another call and let the agent end it. Verify your bridge sends terminate and releases the pipeline and media connection.
  5. Decline a call, then leave another unanswered. Neither should start an agent; both must release the pending media connection.
  6. Read each saved call through List calls and match it to the webhook sequence by call ID.
Do not use 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.

Billing and troubleshooting

Meta Calling permission, outbound country availability, and Kapso billing readiness are separate checks. A valid permission does not bypass an insufficient-balance or billing-readiness error from the proxy. Inbound call handling and termination use separate actions; do not remove the receiver to troubleshoot an outbound billing error. Before enabling automated callbacks, verify a controlled test on the handset: ringing, answering, two-way audio, recipient identity, and termination. Log the call ID and action status without recording API keys or full credentials. See Call actions, Call permission state, and Meta business-initiated calls.