WhatsApp Calling
Connect WhatsApp calls to your own voice application while Wabery handles Meta credentials, call-control requests, and signed webhooks. Your application still handles the live audio through WebRTC or SIP.
What Wabery handles
Section titled “What Wabery handles”WhatsApp user ↕ WhatsApp voiceMeta Calling API ↕ call control and WebRTC signaling, or configured SIPWabery API + signed call.event webhooks ↕ SDP / connection data for WebRTC; lifecycle events for both modesYour WebRTC application or SIP system ↕ live audioYour agent, operator, PBX, or voice botWabery provides:
- channel-scoped access to Meta’s calling settings and call permissions;
- call-permission and voice-call interactive messages;
- call actions such as
connect,pre_accept,accept,reject, andterminate; - signed, retryable
call.eventwebhooks that preserve Meta’s original call, status, SDP, connection, and error fields; - the same operations through REST, the TypeScript SDK, and MCP.
Your voice application provides:
- microphone and speaker access;
- WebRTC peer connections, SDP, ICE, and audio processing, or a SIP trunk/PBX;
- ringing, answering, hang-up, device selection, and operator or bot behavior;
- real-time call state and recovery when either participant disconnects.
Before you start
Section titled “Before you start”You need all of these items:
- A dedicated Meta Cloud WhatsApp channel. The shared Wabery Sandbox cannot host your calling configuration.
- An eligible WhatsApp Business Account and phone number in a country where Meta makes Calling API available.
- The Meta permissions already required by Wabery, including advanced access
to
whatsapp_business_managementwhen Wabery manages an end business. - Calling enabled on the individual phone number.
- A project webhook URL. Wabery delivers call signaling to this URL as
call.event. - A WebRTC media application or a configured SIP system.
Meta remains the source of truth for account, number, country, and feature
eligibility. Wabery returns Meta’s error code and fbtrace_id when available.
The project does not need routing_mode: "EXTERNAL" just to receive call
events; call delivery is separate from inbound message routing. It does need a
webhook URL on the project that owns the channel.
Enable calling on a number
Section titled “Enable calling on a number”Read the number first, then enable calling. This makes the call entry point available according to the visibility settings Meta returns for that number.
const before = await wabery.whatsappManagement.getCallingSettings( "channel_...",);
await wabery.whatsappManagement.updateCallingSettings("channel_...", { status: "ENABLED",});
const after = await wabery.whatsappManagement.getCallingSettings( "channel_...",);GET /v1/channels/channel_.../whatsapp-management?operation=calling_settingsAuthorization: Bearer wab_live_...POST /v1/channels/channel_.../whatsapp-managementAuthorization: Bearer wab_live_...Content-Type: application/json
{ "operation": "update_calling_settings", "input": { "status": "ENABLED" }}{ "tool": "wabery_get_whatsapp_management", "arguments": { "channel_id": "channel_...", "operation": "calling_settings" }}{ "tool": "wabery_manage_whatsapp", "arguments": { "channel_id": "channel_...", "operation": "update_calling_settings", "input": { "status": "ENABLED" }, "confirmation_token": "manage_whatsapp" }}Wabery passes the input object as Meta’s calling settings object. You can
therefore use supported Meta fields such as call-icon, callback-permission, and
SIP settings without giving your application the Meta access token. Meta also
supports calling-hour settings on eligible accounts; use them when your team
cannot answer continuously.
To read SIP credentials, set includeSipCredentials: true in the SDK or send
include_sip_credentials=true with REST/MCP. The response contains a secret.
Keep it server-side, do not log it, and never return it to a browser.
Receive a user-initiated call
Section titled “Receive a user-initiated call”When a WhatsApp user calls your business number, Meta sends signaling to
Wabery. Wabery forwards it to your project webhook as call.event.
The steps below are the WebRTC flow. In SIP mode, configure Meta’s SIP settings
and handle SIP signaling and media in your PBX or telephony service instead.
Continue consuming call.event for lifecycle, status, errors, and auditing, but
do not treat the WebRTC SDP example as your SIP INVITE flow.
- Verify the Wabery signature before parsing the request.
- Read
payload.call.idand the SDP or connection data inpayload.call.sessionorpayload.call.connection. - Create the WebRTC answer.
- Optionally send
pre_acceptwith that SDP answer to establish media early. - Send
acceptwith the same SDP answer afterpre_acceptsucceeds. - Send
rejectif you cannot answer, orterminatewhen your side hangs up.
const event = wabery.webhooks.constructEvent( rawRequestBody, request.headers.get("x-wabery-signature") ?? "", process.env.WABERY_WEBHOOK_SECRET ?? "",);
if (event.event === "call.event" && event.payload.kind === "call") { const call = event.payload.call; const answerSdp = await createWebRtcAnswer(call);
await wabery.whatsappManagement.manageCall(event.payload.channel_id, { call_id: call.id, action: "pre_accept", session: { sdp_type: "answer", sdp: answerSdp, }, });
await wabery.whatsappManagement.manageCall(event.payload.channel_id, { call_id: call.id, action: "accept", session: { sdp_type: "answer", sdp: answerSdp, }, });}Meta currently gives the business roughly 30–60 seconds after the connect
webhook to accept a user-initiated call. Do not wait for a slow model or
business workflow. Keep call setup fast, wait for a successful pre_accept
response before sending media, move nonessential work off the signaling path,
and make repeated webhook processing safe.
Start a business-initiated call
Section titled “Start a business-initiated call”The user controls whether your business may call them. Request permission and wait for the user to grant it before initiating the call.
1. Request permission
Section titled “1. Request permission”await wabery.messages.send({ channelId: "channel_...", to: "+15551234567", idempotencyKey: "call-permission-customer-123", interactive: { type: "call_permission_request", body: { text: "Allow our support team to call you on WhatsApp." }, action: { name: "call_permission_request" }, },});Permission requests are WhatsApp messages. Consent and messaging limits still apply. A free-form request must be allowed in the current customer-service window; use Meta’s supported template path when required outside that window. API acceptance does not mean the user granted permission.
2. Check permission
Section titled “2. Check permission”const permission = await wabery.whatsappManagement.getCallPermissions( "channel_...", "WHATSAPP_USER_ID",);Use the WhatsApp user ID expected by Meta. For users represented by a Business Scoped User ID, do not assume that an E.164 phone number is interchangeable.
3. Create an offer and connect
Section titled “3. Create an offer and connect”const offerSdp = await createWebRtcOffer();
await wabery.whatsappManagement.manageCall("channel_...", { to: "WHATSAPP_USER_ID", action: "connect", session: { sdp_type: "offer", sdp: offerSdp, }, biz_opaque_callback_data: "support-case-123",});Meta sends status items such as ringing, accepted, and rejected through the
same call.event webhook. Wabery sets payload.kind to "status" for these
items and preserves biz_opaque_callback_data when Meta returns it.
Add a call button to a message
Section titled “Add a call button to a message”voice_call is an interactive message button. It invites the user to call the
business; it does not establish a live outbound call by itself.
await wabery.messages.send({ channelId: "channel_...", to: "+15551234567", idempotencyKey: "call-button-customer-123", interactive: { type: "voice_call", body: { text: "Would you rather talk to us?" }, action: { name: "voice_call", parameters: { display_text: "Call support" }, }, },});Use call_permission_request when your business intends to call the user. Use
voice_call when you want the user to start the call.
Both are WhatsApp messages and remain subject to Meta’s normal message-window,
template, consent, quality, and rate-limit rules.
Understand call.event
Section titled “Understand call.event”Every delivery uses Wabery’s normal webhook envelope:
{ "event": "call.event", "api_version": "2026-06-18", "payload": { "object": "whatsapp_call", "channel_id": "channel_...", "phone_number_id": "1234567890", "display_phone_number": "+15551234567", "contacts": [], "kind": "call", "call": { "id": "call_...", "event": "connect", "timestamp": "1788364800", "direction": "USER_INITIATED", "session": {} }, "errors": [] }, "sentAt": "2026-09-02T12:00:00.000Z"}payload.kind is call when Meta supplied the item in calls[], and status
when it came from statuses[]. Wabery intentionally preserves the original
item instead of narrowing Meta’s fields, so new signaling fields remain
available without an SDK release.
Webhook delivery can retry. Build idempotency around the call ID, event or status, and timestamp. A duplicate notification must not create a second media session or answer a terminated call.
Call actions
Section titled “Call actions”| Action | Use it when |
|---|---|
connect |
Your business has permission and wants to start a call with an SDP offer. |
pre_accept |
You received a user call and need a short setup phase before accepting it. |
accept |
Your media endpoint is ready and can answer the call. |
reject |
Your business will not answer the incoming call. |
terminate |
Your side is ending an active or pending call. |
Wabery supplies messaging_product: "whatsapp". Pass the remaining fields
required by Meta in the manageCall() input or the REST/MCP input object.
Send terminate when your side ends an active call even if the media path has
already emitted RTCP BYE. Meta uses the API transition to close call state and
calculate usage correctly.
The equivalent REST request is:
POST /v1/channels/channel_.../whatsapp-managementAuthorization: Bearer wab_live_...Content-Type: application/json
{ "operation": "manage_call", "input": { "call_id": "call_...", "action": "terminate" }}Use MCP for calls
Section titled “Use MCP for calls”MCP exposes the same control plane:
wabery_get_whatsapp_managementreadscalling_settingsandcall_permissions;wabery_manage_whatsapprunsupdate_calling_settingsandmanage_call;wabery_send_messagesendscall_permission_requestandvoice_callinteractive messages.
Calling changes contact a real external service and can ring a person. Hosted MCP asks for human approval. Trusted local MCP uses write mode and the tool’s documented confirmation token.
MCP does not stream audio through the model context protocol. Your WebRTC or SIP service must remain online independently of the MCP client.
Read permission:
{ "tool": "wabery_get_whatsapp_management", "arguments": { "channel_id": "channel_...", "operation": "call_permissions", "params": { "user_wa_id": "+15551234567" } }}Change call state:
{ "tool": "wabery_manage_whatsapp", "arguments": { "channel_id": "channel_...", "operation": "manage_call", "input": { "call_id": "call_...", "action": "reject" }, "confirmation_token": "manage_whatsapp" }}To request permission, call wabery_send_message with the same REST-shaped
message body shown in this guide: use channel_id, to, idempotency_key, and
the call_permission_request interactive object. Trusted local MCP uses
confirmation_token: "send_message"; hosted MCP asks the person to approve the
send.
Choose WebRTC or SIP
Section titled “Choose WebRTC or SIP”| Choose | Good fit | You operate |
|---|---|---|
| WebRTC | A custom browser, mobile, or voice-agent application | SDP/ICE, audio devices, peer connection, call UI, and any TURN/media services your design needs |
| SIP | A PBX, contact center, carrier, or existing telephony stack | SIP routing, TLS/SRTP, credentials, codecs, extensions, queues, and recording policy |
SIP credentials belong only in your telephony backend. WebRTC microphone access belongs in your own user-facing application. Neither belongs in a Wabery API request log or MCP conversation.
Plan for cost, privacy, and recording
Section titled “Plan for cost, privacy, and recording”Meta controls Calling API pricing and eligibility for each dedicated WABA. Wabery does not replace or hide Meta’s charges, and your WebRTC, SIP, carrier, or voice-model provider may charge separately. Check the current Meta and media provider pricing before enabling calls for customers.
Audio does not pass through Wabery’s messaging webhook service. If your voice application records or transcribes calls, you are responsible for consent, retention, deletion, access controls, and any notice required in the caller’s jurisdiction. Do not place recordings or transcripts inside call-control payloads.
Troubleshoot setup
Section titled “Troubleshoot setup”| Symptom | Check |
|---|---|
No call.event arrives |
Confirm the channel is connected and the Wabery project has a webhook URL. Reconnect the channel if setup is incomplete; contact Wabery if both settings are correct. |
| The WhatsApp call button is absent | Calling is enabled for that phone number and Meta allows the account, country, and visibility configuration. |
| A business-initiated call is rejected | The user granted permission, the permission is still valid, and the recipient ID is the identity Meta expects. |
| The call rings but has no audio | Your WebRTC/SIP media negotiation, codec, network, and audio-device path—not the Wabery webhook—is incomplete. |
| The same event runs twice | Deduplicate webhook deliveries before changing call state. |
| Meta rejects the operation | Inspect Wabery’s meta_code and fbtrace_id, then compare the submitted action and current call state. |
See Wabery webhooks for signature verification and delivery behavior, and Manage your WhatsApp channel for the complete management API.
Meta controls availability and the underlying protocol. Check the official WhatsApp Calling API documentation before deploying against a new account or country.