# Browser calling

Let your users place phone calls from a web page. Mint a short-lived session on your server, register it in the browser, then dial.

Browser calling turns a web page into a phone. Your users call from your app with a headset, using one of your numbers as caller ID, and the call shows up in history like any other outbound call. The API key never leaves your server.

## How it works

The browser registers before anything rings, so a call is never bridged to a page that is not ready.

1. Your server mints a WebRTC session with `POST /v1/voice/calls/web`.
2. The browser registers that session with a WebRTC client.
3. Your server dials with `POST /v1/voice/calls/web/dial`. When the callee answers, the call is bridged to the registered browser.

### Mint a session on your server

```bash cURL
curl -X POST https://api.pozy.ai/v1/voice/calls/web \
  -H "Authorization: Bearer $POZY_API_KEY"
```
```ts Node
const sessionRes = await fetch("https://api.pozy.ai/v1/voice/calls/web", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.POZY_API_KEY}` },
});
const session = await sessionRes.json();
if (!sessionRes.ok) throw new Error(session.error);
// Send session.token and session.credentialId to the browser. Never send your API key.
```
```python Python
import os, requests

session = requests.post(
    "https://api.pozy.ai/v1/voice/calls/web",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    timeout=30,
).json()
```

```json
{
  "success": true,
  "token": "eyJhbGciOi...",
  "credentialId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
  "expiresAt": "2026-10-30T11:00:00Z",
  "sdk": "<WebRTC client package>"
}
```

The token lives about an hour, and it must outlive the whole call, not only the handshake. Mint a fresh session per calling session rather than caching one.

### Register in the browser
`sdk` names the WebRTC client package that accepts this token. Install it in your front end, log in with `token`, and wait for the client's ready event. Registration carries no audio on its own: the dialled leg arrives as an incoming call in the `ringing` state, and your page answers it.

```ts
// `WebRtcClient` stands for the client class exported by the package named in `sdk`.
const client = new WebRtcClient({ login_token: token });

client.on("ready", () => {
  // Registered. Tell your server it can dial now.
  fetch("/api/dial", { method: "POST", body: JSON.stringify({ to: "+13125550188" }) });
});

client.on("call", (call) => {
  if (call.state === "ringing") call.answer();
});

client.connect();
```

Check the package's own reference for the exact event names. The flow is always the same: connect, wait for ready, answer the ringing call, hang up when done.

### Dial from your server
`POST /v1/voice/calls/web/dial` with `to` and the `credentialId` from the first step.

```bash
curl -X POST https://api.pozy.ai/v1/voice/calls/web/dial \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "credentialId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "to": "+13125550188", "fromNumber": "+14155550142" }'
```

The response has the `callId` and `status: "dialing"`.

## Dial options

| Field | What it does |
| --- | --- |
| `to` | Number to call (required) |
| `credentialId` | The session from the first step (required) |
| `fromNumber` | Caller ID. Optional when you own exactly one voice-enabled number. |
| `recordOverride` | Turn recording on or off for this call |
| `ringTimeoutSeconds` | Default 30, up to 120 |

Browser calling is paused on a number while it is attached to a [SIP trunk](https://pozy.ai/docs/telephony/sip-trunks). Otherwise, browser calls follow the normal outbound rules: they appear as outbound in [history](https://pozy.ai/docs/telephony/call-recordings), use the number's recording and transcription settings, carry [branded calling](https://pozy.ai/docs/telephony/branded-calling) when the number has it, end with `call.ended` or `call.failed`, and count toward the outbound cap of 60 calls per rolling hour.

To hang up from your server, call `POST /v1/voice/calls/{id}/end`. To hang up from the page, end the call in the WebRTC client.

## Errors

| Status | Cause | Fix |
| --- | --- | --- |
| `422` | The `credentialId` is unknown or expired, or no voice-enabled number matches `fromNumber` | Mint a new session, register it, then dial |
| `429` | Outbound cap of 60 calls per rolling hour reached | Wait |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`POST /v1/voice/calls/web`](https://pozy.ai/docs/api/createVoiceWebSession) | Mint a browser softphone session |
| [`POST /v1/voice/calls/web/dial`](https://pozy.ai/docs/api/dialVoiceWebCall) | Dial from the browser softphone |
| [`POST /v1/voice/calls/{id}/end`](https://pozy.ai/docs/api/endVoiceCall) | Hang up a live call |
| [`GET /v1/voice/calls/{id}`](https://pozy.ai/docs/api/getVoiceCall) | Get a phone call |

## Webhook events

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