Turn on WhatsApp voice calls for a number, route them to a phone, SIP endpoint or AI agent, ask customers for permission and place outbound calls.
With calling turned on, customers can tap the call button in your WhatsApp chat and reach you, and you can call customers who have said yes. Pozy bridges every call to a destination you choose: a phone line, a SIP endpoint such as an AI voice agent, or a WebSocket media server you run.
#What you need
| Requirement | Detail |
|---|---|
| A Cloud API-only number | Calling is off for numbers in coexistence with the WhatsApp Business app |
| Usage-based billing on the workspace | Calls meter per minute (Pricing) |
| A messaging limit of about 2,000 daily recipients or more | Meta's threshold for calling. A new number starts at TIER_250; send normal traffic until Meta raises it. |
| A country where Meta allows business-initiated calls | Meta blocks outbound calls from numbers registered in the US, Canada, Egypt, Vietnam and Nigeria. Inbound calls to those numbers still work. |
If a requirement is missing, enabling calling returns 422.
#Where a call can go
forwardTo takes one of three kinds of address:
| Prefix | Example | Use it for |
|---|---|---|
tel: |
tel:+351210000123 |
A desk phone or a mobile |
sip: |
sip:agent@voice.example.com |
A PBX, a contact centre or an AI voice agent with a SIP address |
wss: |
wss://media.example.com/calls |
Your own media server. Closing the socket ends the call. |
Calls to a sip: or wss: destination have no phone-network leg, so there is no carrier charge for that leg.
#Turn calling on
#Read the current config
GET /v1/whatsapp/calling?accountId=... returns phoneNumberDocId, the number id every other calling call takes, plus the current destination, recording flag and callDeepLink.
#Enable it with a destination
Call POST /v1/phone-numbers/{id}/whatsapp/calling with the account and forwardTo.
curl -X POST "https://api.pozy.ai/v1/phone-numbers/6715d2a7f3b6e81c4d0a5b97/whatsapp/calling" \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "6712a9d4c0e3b58f1a7d2e64",
"forwardTo": "sip:frontdesk@voice.example.com",
"recordingEnabled": false,
"maxCallDurationSeconds": 1800
}'const res = await fetch("https://api.pozy.ai/v1/phone-numbers/6715d2a7f3b6e81c4d0a5b97/whatsapp/calling", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
accountId: "6712a9d4c0e3b58f1a7d2e64",
forwardTo: "sip:frontdesk@voice.example.com",
recordingEnabled: false,
maxCallDurationSeconds: 1800,
}),
});
if (!res.ok) throw new Error((await res.json()).error);import os
import requests
H = {"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"}
requests.post(
"https://api.pozy.ai/v1/phone-numbers/6715d2a7f3b6e81c4d0a5b97/whatsapp/calling",
headers=H,
json={
"accountId": "6712a9d4c0e3b58f1a7d2e64",
"forwardTo": "sip:frontdesk@voice.example.com",
"recordingEnabled": False,
"maxCallDurationSeconds": 1800,
},
timeout=30,
).raise_for_status()#Share the call link
Once calling is on, callDeepLink in the config is a public https://wa.me/call/... link. Put it on your website, in emails or behind a QR code and it starts a WhatsApp call to the number.
Options on enable and on PATCH:
| Field | Default | Effect |
|---|---|---|
recordingEnabled |
false |
Records calls. A consent prompt plays first, and a per-minute recording charge applies. |
maxCallDurationSeconds |
none | Hangs up after this many seconds (up to 14,400) so a destination that never hangs up cannot run up minutes |
forwardCallerId |
business |
caller shows the customer's number to a sip: destination instead of yours |
sipAuthUsername, sipAuthPassword |
none | Credentials if your SIP destination requires them |
callIconCountries |
none | Two-letter country codes where WhatsApp shows the call button in the chat |
DELETE turns calling off but keeps the destination and credentials, so turning it back on needs no setup.
#Call a customer
WhatsApp requires the customer's permission before a business calls them.
#Check permission
GET /v1/whatsapp/call-permissions?accountId=...&to=... returns the permission status (permanent, temporary with an expiration_time, or no_permission) and which actions are allowed right now.
{
"permission": { "status": "no_permission" },
"actions": [
{ "action_name": "send_call_permission_request", "can_perform_action": true },
{ "action_name": "start_call", "can_perform_action": false }
]
}#Ask for permission if needed
Send the request with POST /v1/whatsapp/calls and action: "send_call_permission_request", optionally with your own bodyText (up to 1,024 characters). The customer taps Allow in the chat. Their answer arrives as call.permission_request.
Meta allows one request per customer per 24 hours and two per 7 days, and only while a 24-hour service window is open.
#Place the call
When start_call is allowed, call POST /v1/whatsapp/calls with to. The call bridges to the number's forwardTo, or to a forwardTo you pass on this call.
curl -X POST https://api.pozy.ai/v1/whatsapp/calls \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: callback-order-4821" \
-d '{
"accountId": "6712a9d4c0e3b58f1a7d2e64",
"to": "+351912345678"
}'const res = await fetch("https://api.pozy.ai/v1/whatsapp/calls", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Idempotency-Key": "callback-order-4821",
"Content-Type": "application/json",
},
body: JSON.stringify({ accountId: "6712a9d4c0e3b58f1a7d2e64", to: "+351912345678" }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.callId, data.status); // "dialing"resp = requests.post(
"https://api.pozy.ai/v1/whatsapp/calls",
headers={**H, "Idempotency-Key": "callback-order-4821"},
json={"accountId": "6712a9d4c0e3b58f1a7d2e64", "to": "+351912345678"},
timeout=30,
)
print(resp.json()["callId"])Always send an Idempotency-Key. A retry after a timeout then returns the first response instead of dialling, and billing, a second call.
To hang up, ringing or answered, inbound or outbound, call POST /v1/voice/calls/{id}/end with the callId (for inbound calls, the call id on call.received).
#Estimate and review costs
Before dialling, GET /v1/whatsapp/calls/estimate with to and minutes returns the per-minute price and a breakdown: Meta's fee, the connection charge and recording if you ask for it.
GET /v1/whatsapp/calls lists past calls, filtered by status, direction, since and until, up to 200 per page. Pass nextCursor back as before for the next page. Each call has its duration, endReason (hangup, no_answer, rejected, error), a recordingUrl when recording was on, and billing:
| Field | Meaning |
|---|---|
billing.billableCostUSD |
What Pozy bills for the call |
billing.recordingCostUSD |
The recording part of that amount |
billing.metaCostUSD |
Meta's own per-minute fee, billed by Meta to your WABA. Shown for reference. Always 0 on inbound calls. |
billing.totalCostUSD |
Both together |
Fetch a recording file with GET /v1/whatsapp/calls/{id}/recording.
#Caller ID on numbers you brought yourself
When a call is forwarded to a tel: destination, the phone that rings shows a caller ID. A number bought through Pozy shows itself straight away. A number you brought from your own carrier cannot until it is verified, because carriers block unverified caller IDs; until then the forwarded call shows a Pozy number and the config reads callerIdMode: "platform".
To verify it:
- Call
POST /v1/phone-numbers/{id}/whatsapp/caller-id-verification. The number receives a one-time code by SMS or voice call. Repeat the call to resend. - Submit the code with
POST .../caller-id-verification/verify.
After that, callerIdMode is business and callerIdVerified is true. Numbers bought through Pozy never need this step and get a 400 if you try.
#Common errors
| Status | Cause | Fix |
|---|---|---|
409 on POST /v1/whatsapp/calls |
No active permission from the customer | Check permission and send a request first |
422 on enable |
Billing, messaging tier or country rule not met | See What you need |
422 on a call |
Calling is off on the number, the country blocks outbound calls, or SIP credentials are missing | Enable calling or fix the destination |
502 on a call |
The call could not be connected and is marked failed |
Try again later with a new idempotency key |
#Endpoints
#Webhook events
| Event | When it fires |
|---|---|
call.received |
Call received. |
call.ended |
Call ended. |
call.failed |
Call failed. |
call.permission_request |
Call permission request reply. |