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
| Capability | Requirement |
|---|---|
| User-initiated (inbound) | None beyond a registered phone number |
| Business-initiated (outbound) | ≥ 2,000 business-initiated conversations in a rolling 24h window |
| Both | Number must be CONNECTED per Phone Number Details |
| Permission rules | 1 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 in | USA, 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"
}
}| Field | Notes |
|---|---|
status | ENABLED / DISABLED |
call_icon_visibility | DEFAULT (show), DISABLED (hide) |
callback_permission_status | ENABLED / 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=)
| Field | Required | Notes |
|---|---|---|
start / end | yes | UNIX timestamps |
granularity | yes | HALF_HOUR / DAILY / MONTHLY |
dimensions | no | Breakdowns: DIRECTION (USER/BUSINESS-initiated), COUNTRY, PHONE |
phone_numbers | no | Restrict 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
| Code | Meaning |
|---|---|
131000 | Account not eligible — under the 2,000-conversations / 24h threshold |
131001 | No calling permission from this contact |
131002 | Permission expired (7-day window elapsed) |
131003 | Call limit exceeded (5 connected calls / 24h) |
131004 | Contact revoked permission |
131005 | Call hours closed — outside configured weekly schedule |
131006 | Call 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_requestcomponent before sending. - One call per
phone_number_id, per recipient, per second. Bursts above that get rate-limited at the/callsendpoint withcode 4.