Voice Calls
How WhatsApp calling differs from a normal dialler
Section titled “How WhatsApp calling differs from a normal dialler”You cannot simply dial a customer. WhatsApp requires the customer to accept a call permission request before your business can call them, and Meta enforces how often you may ask:
| Rule | Limit |
|---|---|
| Permission requests per contact | 1 per 24 hours, 2 per week |
| Calling window once accepted | 7 days |
| Consecutive unanswered calls before permission is revoked | 4 |
Inbound (customer-initiated) calls bypass all of this — they are free and need no permission (except from the countries listed under Geo restrictions).
Request permission to call
Section titled “Request permission to call”curl -X POST https://api.whatbot.in/public/v1/calls/permission-request \ -H "X-API-Key: wa_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "wa_id": "918610173741" }'The contact receives a prompt in WhatsApp asking whether your business may call them.
Their answer arrives asynchronously — subscribe to call.permission.updated (see
Webhooks) or poll the endpoint below.
Response
{ "wa_id": "918610173741", "status": "requested", "requested_at": "2026-08-18T12:04:11+00:00", "request_sent": true, "can_call": false, "can_call_reason": "permission not granted (status=requested)", "can_request": false, "can_request_reason": "limit reached: 1 request per 24h"}This is the same shape GET /permission returns, plus request_sent. The
can_* fields are computed from the row and the clock rather than stored, so
they answer “may I act right now” without you having to re-derive the cadence
and window rules.
Check permission state
Section titled “Check permission state”curl https://api.whatbot.in/public/v1/calls/permission/918610173741 \ -H "X-API-Key: wa_live_xxx"{ "wa_id": "918610173741", "status": "approved", "expires_at": "2026-08-25T12:09:02+00:00", "can_call": true, "can_call_reason": "", "can_request": false, "can_request_reason": "already approved — calling window is still open"}status is one of none, requested, approved, rejected, expired, revoked.
Always branch on can_call rather than re-deriving the rules yourself.
Place a call
Section titled “Place a call”curl -X POST https://api.whatbot.in/public/v1/calls \ -H "X-API-Key: wa_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "wa_id": "918610173741", "sdp": "v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\n..." }'sdp is a WebRTC offer generated by your own media stack. WhatsApp calling uses
WebRTC (ICE + DTLS + SRTP, OPUS by default); Whatbot relays the signalling but never
terminates the audio, so the offer must come from the endpoint that will carry the
call.
Returns 403 with the reason when permission is missing or expired, and 422 when the destination is geo-restricted. The call goes out from the business number of the contact’s most recent conversation, or the workspace’s default number if there is none.
{ "id": "0f6b...", "wa_call_id": "wacid...", "wa_id": "918610173741", "direction": "outbound", "status": "ringing", "started_at": "2026-08-18T12:10:00+00:00", "dest_country": null, "conversation_id": "c9e6f0f7-..."}What happens next — answered, missed, ended — arrives on the call.* webhooks and
in the call log.
Call log
Section titled “Call log”curl "https://api.whatbot.in/public/v1/calls?direction=outbound&limit=20" \ -H "X-API-Key: wa_live_xxx"{ "calls": [ { "id": "0f6b...", "wa_id": "918610173741", "direction": "outbound", "status": "completed", "started_at": "2026-08-18T12:10:00+00:00", "answered_at": "2026-08-18T12:10:06+00:00", "ended_at": "2026-08-18T12:11:02+00:00", "duration_sec": 56, "billable_sec": 60 } ]}| Field | Type | Required | Description |
|---|---|---|---|
| direction | string | inbound or outbound. | |
| status | string | ringing, connected, completed, missed, rejected or failed. | |
| conversation_id | string | Only calls on this conversation. | |
| limit | number | 1–200. Defaults to 50. | |
| offset | number | Skip this many, for paging. Defaults to 0. |
Newest first. Each call carries its conversation_id when one exists, and the
contact’s contact_name when known. wa_id is masked if your workspace masks the
public API.
GET /calls/{call_id} returns one call by its id, or 404.
Billing
Section titled “Billing”WhatsApp bills answered outbound calls only, rounded up to 6-second
increments. Ringing and unanswered calls are free, and inbound calls are always
free. billable_sec is the rounded figure — a 56-second call bills as 60 — and is
0 for anything free.
Geo restrictions
Section titled “Geo restrictions”| Direction | Restriction |
|---|---|
| Inbound | Unavailable for numbers in Cuba, Iran, North Korea, Syria |
| Outbound | Blocked when your business number originates in US, Canada, Egypt, Vietnam, Nigeria |
Note the outbound rule keys off your number’s country, not the customer’s. Blocked attempts return 422 before any call is placed.
Webhook events
Section titled “Webhook events”Subscribe via Webhooks — these are opt-in and not included in the default event set:
| Event | Fires when |
|---|---|
call.permission.requested | You sent a permission request |
call.permission.updated | The contact accepted or declined, or a stale permission was reconciled |
call.started | A customer’s call to you started ringing (inbound only) |
call.completed | An answered call ended |
call.missed | A call ended unanswered, declined or failed |
Payloads are listed on the Webhooks page.
Errors
Section titled “Errors”| Status | Cause |
|---|---|
403 | POST /calls without an approved, unexpired permission |
404 | GET /calls/{call_id} for a call that isn’t in your workspace |
409 | Calling was revoked after four unanswered calls and WhatsApp still permits it (permission request) |
422 | Invalid wa_id; geo-restricted number; the 24-hour window is closed (permission request) |
429 | Permission-request limit reached, or the contact has already approved you |
502 | WhatsApp rejected the request or the call |