# Voice and calls API

Place and receive phone calls on your own numbers, route them to a phone, a SIP endpoint or an AI voice agent, and read every call from one history.

import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

The Voice and calls API runs phone calls on numbers you own on Zernio. Calling is on as soon as a number is provisioned, inbound and outbound, and each call goes where you point it.

## What you can build

- **An AI phone agent.** Point a number at your own agent over `wss://` with `POST /v1/phone-numbers/{id}/voice`, or at Vapi, Retell or ElevenLabs over `sip:`.
- **Click-to-call in your CRM.** Dial a customer with `POST /v1/voice/calls` and bridge them to the rep's phone when they answer.
- **A softphone in the browser.** Place calls over WebRTC from your web app, starting with `POST /v1/voice/calls/web` ([browser calling](/platforms/voice/browser-calling)).
- **A business line with voicemail and an IVR.** Set business hours, a menu, voicemail and a caller blocklist on the same number.
- **Call logs and recordings.** List every call with `GET /v1/calls` and fetch a recording with `GET /v1/calls/{id}/recording`.

## Platforms

<ProductPlatforms product="voice" />

Calls run on [phone numbers](/products/phone-numbers) you buy or port in. WhatsApp calls are a separate feature with their own endpoints ([WhatsApp calling](/platforms/whatsapp/calling)), but they show in the same call history.

## How it works

Every number has a forward destination, set with `POST /v1/phone-numbers/{id}/voice`. Inbound calls ring it, and outbound calls bridge to it when the callee answers.

| Destination | Format | Use it for |
|---|---|---|
| A phone | `tel:+13105559999` | Forwarding to a person's mobile or desk phone |
| A SIP endpoint | `sip:agent@example.com` | Your PBX, or an AI agent on Vapi, Retell or ElevenLabs |
| Your media server | `wss://voice.example.com` | An AI voice agent you run yourself, with live audio both ways |

An outbound call can override the destination with its own `forwardTo`. Its lifecycle arrives as the `call.*` [phone webhooks](/webhooks/phone), such as `call.ended` and `call.failed`.

Calls bill per minute by destination from your [prepaid balance](/pricing/balance) (older accounts: the monthly invoice). Recording and transcription are optional and cost extra. Rates are on [phone pricing](/pricing/phone).

## Your first request

You need an API key and a number with a forward destination. This dials a customer and, once they answer, bridges them to that destination.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data } = await zernio.voice.createVoiceCall({
  body: {
    fromNumber: '+14155550100',
    to: '+13105551234',
    greeting: 'Connecting you now.',
  },
});

console.log(data.callId, data.status);
```
</Tab>
<Tab value="Python">
```python
from zernio import Zernio

client = Zernio()

call = client.voice.create_voice_call(
    from_number="+14155550100",
    to="+13105551234",
    greeting="Connecting you now.",
)

print(call["callId"], call["status"])
```
</Tab>
<Tab value="curl">
```bash
curl -X POST "https://zernio.com/api/v1/voice/calls" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"fromNumber": "+14155550100", "to": "+13105551234", "greeting": "Connecting you now."}'
```
</Tab>
</Tabs>

Response (`200`):

```json
{
  "success": true,
  "callId": "66e5f6a7b8c9d0e1f2a3b4c5",
  "status": "dialing",
  "direction": "outbound",
  "from": "+14155550100",
  "to": "+13105551234",
  "forwardTo": "tel:+13105559999",
  "greeting": "Connecting you now.",
  "recordingEnabled": false,
  "transcriptionEnabled": false
}
```

`dialing` means the call is placed. `fromNumber` is optional when you own exactly one voice-enabled number. Every field is on [Place an outbound call](/voice/create-voice-call).

## Examples

### Answer calls with your AI agent

Point the number at your agent's WebSocket. Zernio answers each inbound call and streams its audio both ways, so the agent hears the caller and speaks back.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
const { data: voice } = await zernio.voice.enableVoiceOnNumber({
  path: { id: '66d4e5f6a7b8c9d0e1f2a3b4' },
  body: { forwardTo: 'wss://your-agent.example.com/media' },
});

console.log(voice.pstnForwardTo);
```
</Tab>
<Tab value="Python">
```python
voice = client.voice.enable_voice_on_number(
    id="66d4e5f6a7b8c9d0e1f2a3b4",
    forward_to="wss://your-agent.example.com/media",
)

print(voice["pstnForwardTo"])
```
</Tab>
<Tab value="curl">
```bash
curl -X POST "https://zernio.com/api/v1/phone-numbers/66d4e5f6a7b8c9d0e1f2a3b4/voice" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"forwardTo": "wss://your-agent.example.com/media"}'
```
</Tab>
</Tabs>

```json
{
  "enabled": true,
  "phoneNumber": "+14155550100",
  "pstnForwardTo": "wss://your-agent.example.com/media",
  "recordingEnabled": false,
  "transcriptionEnabled": false,
  "voicemailEnabled": true
}
```

There is no second phone leg, so you pay only the inbound leg. The protocol and hosted agents are on [AI agents](/platforms/voice/ai-agents).

### Estimate the cost first

Quote a call before dialing. The estimate uses the same formula as the invoice, so the quote and the charge cannot disagree.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
const { data: estimate } = await zernio.voice.getVoiceCallEstimate({
  query: { to: '+13105551234', minutes: 5, recording: true },
});

console.log(estimate.breakdown.billableCostUSD);
```
</Tab>
<Tab value="Python">
```python
estimate = client.voice.get_voice_call_estimate(to="+13105551234", minutes=5, recording=True)

print(estimate["breakdown"]["billableCostUSD"])
```
</Tab>
<Tab value="curl">
```bash
curl "https://zernio.com/api/v1/voice/calls/estimate?to=%2B13105551234&minutes=5&recording=true" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"
```
</Tab>
</Tabs>

```json
{
  "destinationCountry": "US",
  "minutes": 5,
  "perMinuteUsd": 0.044,
  "breakdown": {
    "telnyxCostUSD": 0.1,
    "recordingCostUSD": 0.01,
    "transcriptionCostUSD": 0,
    "billableCostUSD": 0.22,
    "totalCostUSD": 0.22
  }
}
```

`billableCostUSD` is what you pay. A `tel:` forward destination is quoted as a second carrier leg.

### Leave a voicemail on a machine

For outbound campaigns, `voicemailDropMessage` speaks your message when a machine answers, then hangs up. A person who answers is bridged as usual.

```bash
curl -X POST "https://zernio.com/api/v1/voice/calls" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"fromNumber": "+14155550100", "to": "+13105551234", "voicemailDropMessage": "Hi, this is Acme about your order. Call us back at this number."}'
```

It turns on answering-machine detection, which delays the bridge until Zernio knows who answered. See [outbound calls](/platforms/voice/outbound).

## How it behaves

- **Live control.** Transfer a live call with `POST /v1/voice/calls/{id}/transfer`, or hang up with `POST /v1/voice/calls/{id}/end`.
- **Rate limit.** Outbound calls are capped at 60 per rolling hour per account.
- **Retries.** Send an `Idempotency-Key` header so a retry never dials twice.
- **Caller name.** On US numbers, [Branded Calling](/platforms/phone-numbers/branded-calling) shows your business name and logo on the callee's phone.

## If it fails

| Error | Cause | Fix |
|---|---|---|
| `402` | The prepaid balance is empty | Add funds at `funding_url`, then dial again |
| `422` | No voice-enabled number matches `fromNumber`, or it has no forward destination | Set the number's forward, or pass `forwardTo` |
| `429` | More than 60 outbound calls in an hour | Wait and try again |
| `502` | The carrier refused to place the call | The call is marked `failed`. Check the destination number |

## Related

- [Voice setup](/platforms/voice/setup)
- [AI agents](/platforms/voice/ai-agents)
- [Outbound calls](/platforms/voice/outbound)
- [Call history](/platforms/voice/history)
- [Phone webhooks](/webhooks/phone)
- [Phone pricing](/pricing/phone)