# WhatsApp calling

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](https://pozy.ai/docs/whatsapp/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=...`](https://pozy.ai/docs/api/getWhatsAppCallingConfig) 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`](https://pozy.ai/docs/api/enableWhatsAppCalling) with the account and `forwardTo`.

```bash cURL
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
  }'
```
```ts Node
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);
```
```python Python
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`](https://pozy.ai/docs/api/updateWhatsAppCalling):

| 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`](https://pozy.ai/docs/api/disableWhatsAppCalling) 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=...`](https://pozy.ai/docs/api/getWhatsAppCallPermissions) returns the permission `status` (`permanent`, `temporary` with an `expiration_time`, or `no_permission`) and which actions are allowed right now.

```json
{
  "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`](https://pozy.ai/docs/api/initiateWhatsAppCall) 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`](https://pozy.ai/docs/api/initiateWhatsAppCall) with `to`. The call bridges to the number's `forwardTo`, or to a `forwardTo` you pass on this call.

```bash cURL
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"
  }'
```
```ts Node
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"
```
```python Python
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`](https://pozy.ai/docs/api/endVoiceCall) with the `callId` (for inbound calls, the call id on `call.received`).

## Estimate and review costs

Before dialling, [`GET /v1/whatsapp/calls/estimate`](https://pozy.ai/docs/api/getWhatsAppCallEstimate) 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`](https://pozy.ai/docs/api/listWhatsAppCalls) 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`](https://pozy.ai/docs/api/getWhatsAppCallRecording).

## 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:

1. Call [`POST /v1/phone-numbers/{id}/whatsapp/caller-id-verification`](https://pozy.ai/docs/api/startWhatsAppCallerIdVerification). The number receives a one-time code by SMS or voice call. Repeat the call to resend.
2. Submit the code with [`POST .../caller-id-verification/verify`](https://pozy.ai/docs/api/verifyWhatsAppCallerId).

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](#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

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/whatsapp/calling`](https://pozy.ai/docs/api/getWhatsAppCallingConfig) | Get calling config for an account |
| [`POST /v1/whatsapp/phone-numbers/{id}/calling`](https://pozy.ai/docs/api/enableWhatsAppCallingLegacy) | Enable calling on a number |
| [`PATCH /v1/whatsapp/phone-numbers/{id}/calling`](https://pozy.ai/docs/api/updateWhatsAppCallingLegacy) | Update calling config |
| [`DELETE /v1/whatsapp/phone-numbers/{id}/calling`](https://pozy.ai/docs/api/disableWhatsAppCallingLegacy) | Disable calling on a number |
| [`GET /v1/whatsapp/call-permissions`](https://pozy.ai/docs/api/getWhatsAppCallPermissions) | Check call permission |
| [`GET /v1/whatsapp/calls`](https://pozy.ai/docs/api/listWhatsAppCalls) | List call history for an account |
| [`POST /v1/whatsapp/calls`](https://pozy.ai/docs/api/initiateWhatsAppCall) | Initiate outbound call |
| [`GET /v1/whatsapp/calls/{id}`](https://pozy.ai/docs/api/getWhatsAppCall) | Get a single call |
| [`GET /v1/whatsapp/calls/{id}/recording`](https://pozy.ai/docs/api/getWhatsAppCallRecording) | Get a call recording |
| [`GET /v1/whatsapp/calls/estimate`](https://pozy.ai/docs/api/getWhatsAppCallEstimate) | Estimate per-minute cost |
| [`GET /v1/phone-numbers/{id}/whatsapp/calling`](https://pozy.ai/docs/api/getWhatsAppCalling) | Get calling config for a number |
| [`POST /v1/phone-numbers/{id}/whatsapp/calling`](https://pozy.ai/docs/api/enableWhatsAppCalling) | Enable calling on a number |
| [`PATCH /v1/phone-numbers/{id}/whatsapp/calling`](https://pozy.ai/docs/api/updateWhatsAppCalling) | Update calling config |
| [`DELETE /v1/phone-numbers/{id}/whatsapp/calling`](https://pozy.ai/docs/api/disableWhatsAppCalling) | Disable calling on a number |
| [`POST /v1/phone-numbers/{id}/whatsapp/caller-id-verification`](https://pozy.ai/docs/api/startWhatsAppCallerIdVerification) | Start caller-ID verification for a customer-brought number |
| [`POST /v1/phone-numbers/{id}/whatsapp/caller-id-verification/verify`](https://pozy.ai/docs/api/verifyWhatsAppCallerId) | Confirm the caller-ID verification code |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`call.received`](https://pozy.ai/docs/webhooks/events#call-received) | Call received. |
| [`call.ended`](https://pozy.ai/docs/webhooks/events#call-ended) | Call ended. |
| [`call.failed`](https://pozy.ai/docs/webhooks/events#call-failed) | Call failed. |
| [`call.permission_request`](https://pozy.ai/docs/webhooks/events#call-permission-request) | Call permission request reply. |
