Skip to Content

WhatsApp Voice Calling

WhatsApp Voice Calling lets your customers tap a call button in chat (user-initiated) and lets you place outbound calls to customers who’ve granted permission (business-initiated). Calls are voice-only, in-app, billed per call-minute by Meta.

Eligibility quick-reference

CapabilityRequirement
User-initiated (inbound)None beyond a registered phone number
Business-initiated (outbound)≥ 2,000 business-initiated conversations in a rolling 24h window
BothNumber must be CONNECTED per Phone Number Details
Permission rules1 permission request / contact / 24h. Max 2 requests / 7 days. Approved permission valid 7 days. Max 5 connected calls / 24h. 4 consecutive unanswered calls → permission auto-revoked
Not available inUSA, Canada, Egypt, Vietnam, Nigeria, Turkey (Meta-side restriction)

1. Enable calling on a number

POST /api/v25.0/{PHONE_NUMBER_ID}/settings Authorization: Bearer pk_live_<your-key> Content-Type: application/json
{ "messaging_product": "whatsapp", "calling": { "status": "ENABLED", "call_icon_visibility": "DEFAULT", "callback_permission_status": "ENABLED" } }
FieldNotes
statusENABLED / DISABLED
call_icon_visibilityDEFAULT (show), DISABLED (hide)
callback_permission_statusENABLED / DISABLED — gates the business-initiated path

Response — 200

{ "success": true }

2. Read calling settings

GET /api/v25.0/{PHONE_NUMBER_ID}/settings?fields=calling Authorization: Bearer pk_live_<your-key>

Response — 200

{ "calling": { "status": "ENABLED", "call_icon_visibility": "DEFAULT", "callback_permission_status": "ENABLED", "call_hours": { "status": "ENABLED", "timezone": "Asia/Kolkata", "weekly_operating_hours": [ { "day_of_week": "MONDAY", "open_time": "0900", "close_time": "1800" }, { "day_of_week": "TUESDAY", "open_time": "0900", "close_time": "1800" } ], "holiday_schedule": [] }, "sip": { "status": "DISABLED", "servers": [] } }, "id": "112269058640637" }

call_hours and sip are optional sub-settings; the next two sections cover them.

3. Call hours (when the call button is active)

POST /api/v25.0/{PHONE_NUMBER_ID}/settings Authorization: Bearer pk_live_<your-key> Content-Type: application/json
{ "messaging_product": "whatsapp", "calling": { "call_hours": { "status": "ENABLED", "timezone": "Asia/Kolkata", "weekly_operating_hours": [ { "day_of_week": "MONDAY", "open_time": "0900", "close_time": "1800" }, { "day_of_week": "TUESDAY", "open_time": "0900", "close_time": "1800" }, { "day_of_week": "WEDNESDAY", "open_time": "0900", "close_time": "1800" }, { "day_of_week": "THURSDAY", "open_time": "0900", "close_time": "1800" }, { "day_of_week": "FRIDAY", "open_time": "0900", "close_time": "1800" } ], "holiday_schedule": [ { "date": "2026-10-02", "start_time": "0000", "end_time": "2359" } ] } } }

Outside call hours, the in-app call button is hidden and inbound calls fall through to your configured fallback (a text auto-reply via template, typically).

4. SIP routing (optional — route calls to your own PBX)

{ "messaging_product": "whatsapp", "calling": { "sip": { "status": "ENABLED", "servers": [ { "hostname": "sip.example.com", "port": 5060, "transport": "TLS", "username": "splashify-sip", "auth": { "type": "SECRET", "secret_ref": "<your-secret-ref>" } } ] } } }

When SIP is on, Meta bridges the WhatsApp call to your SIP server and your existing PBX / contact-centre handles the audio leg. Without SIP, the call rides Meta’s WebRTC stack and you handle it through the calling webhook (connect → pre_accept → accept → terminate, below).

5. Send a permission request

Permission is a one-shot template send. The template’s category must be UTILITY and it must include a permission_request component.

POST /api/v25.0/{PHONE_NUMBER_ID}/messages Authorization: Bearer pk_live_<your-key> Content-Type: application/json
{ "messaging_product": "whatsapp", "to": "+919999999999", "type": "template", "template": { "name": "call_permission_request_v1", "language": { "code": "en_US" }, "components": [ { "type": "body", "parameters": [ { "type": "text", "text": "Aditya" } ] } ] } }

The customer sees a permission-request button under the template; tapping it sends a permission_update event to your calling webhook (see §10 below). Don’t keep retrying — the per-contact rate limits above are hard.

6. Read current permission status

GET /api/v25.0/{PHONE_NUMBER_ID}/call_permissions?user_wa_id=919999999999 Authorization: Bearer pk_live_<your-key>

Response — 200

{ "data": [ { "user_wa_id": "919999999999", "permission_status": "APPROVED", "expires_at": "2026-05-27T09:00:00Z", "requested_at": "2026-05-20T09:00:00Z", "approved_at": "2026-05-20T09:01:42Z", "connected_call_count": 1, "remaining_calls_24h": 4 } ] }

permission_status values: PENDING, APPROVED, DECLINED, EXPIRED, REVOKED.

7. Initiate an outbound call

POST /api/v25.0/{PHONE_NUMBER_ID}/calls Authorization: Bearer pk_live_<your-key> Content-Type: application/json
{ "messaging_product": "whatsapp", "to": "+919999999999", "action": "connect", "session": { "sdp_type": "OFFER", "sdp": "<your WebRTC SDP offer>" } }

Skip the session block if you’re using SIP routing — Meta bridges to your SIP server.

Response — 200

{ "messaging_product": "whatsapp", "calls": [ { "id": "wacid.HBgMOTE5OTk5OTk5OTk5FQIA…" } ] }

Store the id — every subsequent action on this call uses it.

8. Pre-accept an inbound call (low-latency audio)

When you receive an inbound connect event on your webhook, send pre_accept before accept so Meta can negotiate the media path while your agent UI is still picking up:

POST /api/v25.0/{PHONE_NUMBER_ID}/calls Authorization: Bearer pk_live_<your-key> Content-Type: application/json
{ "messaging_product": "whatsapp", "action": "pre_accept", "call_id": "wacid.…", "session": { "sdp_type": "ANSWER", "sdp": "<your WebRTC SDP answer>" } }

9. Accept / Reject / Terminate

Same endpoint, three different action values:

{ "messaging_product": "whatsapp", "action": "accept", "call_id": "wacid.…" } { "messaging_product": "whatsapp", "action": "reject", "call_id": "wacid.…" } { "messaging_product": "whatsapp", "action": "terminate", "call_id": "wacid.…" }
  • accept — answer a call you’ve pre-accepted. Use after the agent UI has confirmed pickup.
  • reject — decline an inbound call. The caller hears a busy tone.
  • terminate — hang up an active call. Same endpoint both directions (caller or callee).

Response — 200

{ "success": true }

10. Call analytics

Aggregate counts + cost analytics for calls made on your WABA.

GET /api/v25.0/{WABA_ID}?fields=call_analytics.start(<UNIX>).end(<UNIX>).granularity(<HALF_HOUR|DAILY|MONTHLY>).dimensions(<DIRECTION|COUNTRY|PHONE>) Authorization: Bearer pk_live_<your-key>

Query parameters (nested inside fields=)

FieldRequiredNotes
start / endyesUNIX timestamps
granularityyesHALF_HOUR / DAILY / MONTHLY
dimensionsnoBreakdowns: DIRECTION (USER/BUSINESS-initiated), COUNTRY, PHONE
phone_numbersnoRestrict to specific numbers

Response — 200

{ "call_analytics": { "granularity": "HALF_HOUR", "data_points": [ { "start": 1756191627, "end": 1756193427, "total_calls": 120, "completed_calls": 98, "failed_calls": 22, "total_duration_seconds": 14820, "avg_duration_seconds": 151.2, "direction": "BUSINESS_INITIATED" } ] }, "id": "115344761664057" }

cURL

curl -X GET \ "https://api.splashifypro.com/api/v25.0/$WABA_ID?fields=call_analytics.start(1756191627).end(1756209627).granularity(HALF_HOUR).dimensions(DIRECTION)" \ -H "Authorization: Bearer $SPLASHIFY_API_KEY"

11. Call permission templates + call button templates

Two specialised template shapes used with calling. Both are created through the standard Message Templates endpoint with specific component types:

Call permission template

{ "name": "call_permission_request_v1", "language": "en_US", "category": "UTILITY", "components": [ { "type": "BODY", "text": "Hi {{1}}, may we call you about your recent order?" }, { "type": "BUTTONS", "buttons": [ { "type": "CALL_PERMISSION_REQUEST", "text": "Allow Calls" } ] } ] }

Tapping the button sends a permission_update event to your calling webhook (see Voice Calling §10).

Call button template

{ "name": "support_with_call_button", "language": "en_US", "category": "UTILITY", "components": [ { "type": "BODY", "text": "Hi {{1}}, your ticket {{2}} is being looked at." }, { "type": "BUTTONS", "buttons": [ { "type": "PHONE_NUMBER", "text": "Call support", "phone_number": "+919876543210" } ] } ] }

The PHONE_NUMBER button opens a WhatsApp voice call to the listed number when tapped — no permission required, since the user initiates.

Send a template with a call button

Use Send Messages with type: "template" and reference the template by name. The button parameters slot in like any other template button parameter.

12. Calling webhooks

Inbound events arrive on your configured webhook URL inside a normal WhatsApp Cloud API envelope, under a calls array.

connect — inbound call ringing

{ "calls": [ { "id": "wacid.…", "from": "919999999999", "to": "919876543210", "event": "connect", "timestamp": "1717000000", "direction": "inbound", "session": { "sdp_type": "OFFER", "sdp": "<remote SDP>" } } ] }

terminate — call ended

{ "calls": [ { "id": "wacid.…", "event": "terminate", "timestamp": "1717000045", "duration_seconds": 38, "status": "COMPLETED", "end_reason": "CALLEE_ENDED" } ] }

status values: COMPLETED, NO_ANSWER, CALLEE_BUSY, FAILED.

permission_update — permission changed

{ "calls": [ { "event": "permission_update", "user_wa_id": "919999999999", "permission_status": "APPROVED", "expires_at": "2026-05-27T09:00:00Z" } ] }

Fires on APPROVED, DECLINED, REVOKED, and on natural expiry.

Errors

CodeMeaning
131000Account not eligible — under the 2,000-conversations / 24h threshold
131001No calling permission from this contact
131002Permission expired (7-day window elapsed)
131003Call limit exceeded (5 connected calls / 24h)
131004Contact revoked permission
131005Call hours closed — outside configured weekly schedule
131006Call already in progress / invalid call_id for action

cURL — enable + initiate

# 1) Enable calling on the number curl -X POST \ "https://api.splashifypro.com/api/v25.0/$PHONE_NUMBER_ID/settings" \ -H "Authorization: Bearer $SPLASHIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "messaging_product": "whatsapp", "calling": { "status": "ENABLED", "call_icon_visibility": "DEFAULT", "callback_permission_status": "ENABLED" } }' # 2) Initiate an outbound call (after permission) curl -X POST \ "https://api.splashifypro.com/api/v25.0/$PHONE_NUMBER_ID/calls" \ -H "Authorization: Bearer $SPLASHIFY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "messaging_product": "whatsapp", "to": "+919999999999", "action": "connect" }'

Notes

  • Calls do NOT count as conversations for billing or messaging limits. Meta bills calls separately per minute; see your wallet ledger for the per-region rate.
  • SDP exchange is mandatory for the WebRTC path — without SIP routing, you must run your own WebRTC peer (browser or native) and generate offers / answers. There’s no Meta-hosted player.
  • Permission templates must be pre-approved by Meta like any other template. Submit via Message Templates with a permission_request component before sending.
  • One call per phone_number_id, per recipient, per second. Bursts above that get rate-limited at the /calls endpoint with code 4.