Phone, SMS & voice
Route inbound calls to a phone, a SIP address or an AI agent, add voicemail, business hours and an IVR menu, and place, transfer and end outbound calls.
Every Pozy number takes and makes regular phone calls as soon as it is active. Inbound calls go wherever you point them: a mobile, a SIP endpoint, or an AI voice agent. Outbound calls dial from your number with one request. Every call, in either direction, lands in one call history.
Calls are billed per connected minute by destination. Unanswered, busy and failed calls are not charged.
These are phone network calls. Voice calls inside a WhatsApp chat are a separate feature: see WhatsApp calling. Both appear in the same history.
#Route inbound calls
All inbound settings live on one idempotent endpoint, POST /v1/phone-numbers/{id}/voice. It writes only the fields you send, so you can call it again later to change one setting.
curl -X POST https://api.pozy.ai/v1/phone-numbers/6650d2e1b3c4d5e6f7081923/voice \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"forwardTo": "tel:+13125550188",
"forwardCallerId": "caller",
"voicemailEnabled": true,
"voicemailGreeting": "You have reached Acme. Leave a message and we will call you back."
}'const res = await fetch("https://api.pozy.ai/v1/phone-numbers/6650d2e1b3c4d5e6f7081923/voice", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
forwardTo: "tel:+13125550188",
forwardCallerId: "caller",
voicemailEnabled: true,
voicemailGreeting: "You have reached Acme. Leave a message and we will call you back.",
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.enabled, data.pstnForwardTo);import os, requests
resp = requests.post(
"https://api.pozy.ai/v1/phone-numbers/6650d2e1b3c4d5e6f7081923/voice",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
json={
"forwardTo": "tel:+13125550188",
"forwardCallerId": "caller",
"voicemailEnabled": True,
"voicemailGreeting": "You have reached Acme. Leave a message and we will call you back.",
},
timeout=30,
)
print(resp.json())The response is the full effective configuration (pstnForwardTo, voicemail, hours, IVR, blocklist, recording settings).
#Destinations
forwardTo |
Sends the call to |
|---|---|
tel:+13125550188 |
A phone number |
sip:agent@pbx.example.com |
A SIP endpoint or a hosted voice agent |
wss://agent.example.com/media |
Your own media server, for a custom AI agent |
Leave forwardTo out to keep the current destination. An empty string clears it, which leaves the number able to call out but not to ring anywhere on inbound.
#Inbound features
| Feature | Fields | Behaviour |
|---|---|---|
| Voicemail | voicemailEnabled, voicemailGreeting |
On by default. Catches unanswered calls (about 25 seconds of ringing), failed bridges and after-hours calls. Records up to 2 minutes. |
| Business hours | businessHoursEnabled, businessHoursTimezone, businessHours |
Rings only inside the windows. Outside them the call goes to voicemail. |
| IVR menu | ivrEnabled, ivrPrompt, ivrOptions |
Plays a menu and routes by keypress. Replaces the plain forward. |
| Blocklist | blockedCallers |
Rejects listed numbers with a busy signal. Each write replaces the whole list. |
| Caller ID | forwardCallerId |
Show your number (business, default) or the original caller (caller) on the forwarded leg |
| Recording | recordingEnabled |
Off by default. A consent notice plays first. |
| Transcription | transcriptionEnabled, transcriptionLanguage |
Off by default. Language auto, en or es. |
Business hours and the IVR menu take structured values:
{
"businessHoursEnabled": true,
"businessHoursTimezone": "Europe/Madrid",
"businessHours": [
{ "day": 1, "open": "09:00", "close": "18:00" },
{ "day": 2, "open": "09:00", "close": "18:00" },
{ "day": 6, "open": "10:00", "close": "14:00" }
],
"ivrEnabled": true,
"ivrPrompt": "Press 1 for sales, 2 for support, 3 to talk to our assistant.",
"ivrOptions": [
{ "digit": "1", "forwardTo": "tel:+13125550188", "label": "Sales" },
{ "digit": "2", "forwardTo": "sip:support@pbx.example.com", "label": "Support" },
{ "digit": "3", "forwardTo": "wss://agent.example.com/media", "label": "Assistant" }
]
}- A window has
day(0 is Sunday through 6),openandcloseasHH:MMin the timezone. A day with no window is closed. A window whosecloseis before itsopenruns past midnight. Up to 21 windows. - A menu option has a
digit(0to9,*or#), its ownforwardToand an optionallabel. Up to 12 options. No input or an unknown digit ends the call.
#The order an inbound call follows
- Blocklist. A blocked caller gets a busy signal before the call is answered: no charge, no voicemail, no webhook.
call.receivedfires.- Business hours. Outside the windows, the call goes straight to voicemail (or hangs up if voicemail is off).
- IVR menu, if on. The digit picks a destination.
- Ring the destination. If nobody answers or the bridge fails, voicemail takes the call.
call.ended(orcall.failedon a hard error).
Voicemail sends the account an email. The recording, and the transcript when transcription is on, attach to the call record. There is no separate voicemail webhook: call.ended carries isVoicemail: true.
#Turn calling off
DELETE /v1/phone-numbers/{id}/voice disables calls on the number. Pozy keeps the settings, so enabling again restores them.
#Place an outbound call
POST /v1/voice/calls with to. When the callee answers, Pozy bridges them to the number's forward destination, or to a per-call forwardTo. An optional greeting is spoken to the callee first.
curl -X POST https://api.pozy.ai/v1/voice/calls \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Idempotency-Key: callback-ticket-5521" \
-H "Content-Type: application/json" \
-d '{
"fromNumber": "+14155550142",
"to": "+13125550188",
"forwardTo": "tel:+14155550100",
"greeting": "Hi, this is Acme returning your call. Connecting you now."
}'const callRes = await fetch("https://api.pozy.ai/v1/voice/calls", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Idempotency-Key": "callback-ticket-5521",
"Content-Type": "application/json",
},
body: JSON.stringify({
fromNumber: "+14155550142",
to: "+13125550188",
forwardTo: "tel:+14155550100",
greeting: "Hi, this is Acme returning your call. Connecting you now.",
}),
});
const call = await callRes.json();
if (!callRes.ok) throw new Error(call.error);
console.log(call.callId, call.status); // "dialing"resp = requests.post(
"https://api.pozy.ai/v1/voice/calls",
headers={
"Authorization": f"Bearer {os.environ['POZY_API_KEY']}",
"Idempotency-Key": "callback-ticket-5521",
},
json={
"fromNumber": "+14155550142",
"to": "+13125550188",
"forwardTo": "tel:+14155550100",
"greeting": "Hi, this is Acme returning your call. Connecting you now.",
},
timeout=30,
)
print(resp.json()["callId"])A 200 means the call is dialing. Everything after that is asynchronous: follow it with GET /v1/voice/calls/{id}, or with call.ended and call.failed. Outbound phone calls do not send call.received.
| Field | What it does |
|---|---|
to |
Number to call (required) |
fromNumber |
Which of your numbers to call from. Optional when you own exactly one voice-enabled number. |
forwardTo |
Who the callee is bridged to: tel:, sip: or wss:// (an AI agent) |
greeting |
Spoken to the callee before the bridge, up to 1,000 characters |
amd |
Answering-machine detection. The bridge waits until Pozy knows whether a person answered. |
voicemailDropMessage |
With amd, speak this to an answering machine and hang up |
recordOverride, transcribeOverride, transcriptionLanguage |
Override the number's defaults for this call |
ringTimeoutSeconds |
How long to ring. Default 30, up to 120. |
Send an Idempotency-Key so a retry does not dial (and bill) a second call.
#Estimate the cost first
GET /v1/voice/calls/estimate?to=%2B13125550188&minutes=5&recording=true returns perMinuteUsd and a breakdown whose billableCostUSD is what you will be charged, using the same formula as the invoice. Use it to show prices to your users or to block expensive routes. Estimates cover outbound destinations only.
#Transfer and hang up
POST /v1/voice/calls/{id}/transferwithto(a+E164number or asip:URI) hands a live call to someone else. The new leg always shows your number.wss://is not a valid transfer target.POST /v1/voice/calls/{id}/endhangs up. It is safe to repeat: ending a finished call returns200with its current status. It also ends WhatsApp calls.
Final duration and cost are written when the hangup lands, so a call record can show its previous status for a moment.
#Webhook events
| Call | Events, in order |
|---|---|
| Inbound | call.received at ring time, then call.ended or call.failed |
| Outbound phone call (API or browser) | call.ended or call.failed only |
call.ended carries durationSeconds, endReason (hangup, no_answer, rejected or error), isVoicemail, callErrors, a cost breakdown in billing, and a signed recordingUrl when recording was on. Unanswered, busy and rejected calls end with call.ended, so branch on endReason. call.failed is for hard errors before or during the bridge.
On an inbound call.ended with endReason: "no_answer", text the caller back: send an SMS from call.to (your number) to call.from. The caller gets an answer even when nobody picked up. call.received also carries a conversationId if you would rather reply in the existing inbox thread.
| Event | When it fires |
|---|---|
call.received |
Call received. |
call.ended |
Call ended. |
call.failed |
Call failed. |
call.permission_request |
Call permission request reply. |
#Limits
| Limit | Value |
|---|---|
| Outbound calls | 60 per rolling hour across all numbers on the key. Past it, 429. |
| Business hours | 21 windows |
| IVR options | 12 |
| Ringing before voicemail | About 25 seconds |
| Voicemail message | 2 minutes |
| Emergency calls | Not supported |
#Errors
| Status | Code | Cause | Fix |
|---|---|---|---|
422 |
No voice-enabled number matches fromNumber, or the number has no destination and the call sent no forwardTo |
Set forwardTo on the number or the call |
|
409 |
Transfer on a call that is not connected yet or already ended | Transfer only while live | |
409 |
invalid_resource_state |
The number is attached to a SIP trunk, which owns its calls | Detach it first |
422 |
The number is a WhatsApp number on your own carrier, so Pozy cannot route its calls | Use a number bought or ported into Pozy | |
429 |
Outbound cap reached | Wait for the rolling hour | |
502 |
The carrier refused to place the call. It is marked failed. |
Retry |
#Endpoints
| Endpoint | What it does |
|---|---|
POST /v1/phone-numbers/{id}/voice |
Enable phone calling on a number |
DELETE /v1/phone-numbers/{id}/voice |
Disable phone calling on a number |
POST /v1/voice/calls |
Place an outbound phone call |
GET /v1/voice/calls/estimate |
Estimate call cost |
GET /v1/voice/calls |
List phone calls |
GET /v1/voice/calls/{id} |
Get a phone call |
POST /v1/voice/calls/{id}/transfer |
Blind-transfer a live call |
POST /v1/voice/calls/{id}/end |
Hang up a live call |