> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polymorfa.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Calls

> Place, answer, and end WhatsApp calls, change call settings, speak on calls from server code, and connect SIP trunks.

Control WhatsApp calls from your server with `messaging.voip`. To speak and
listen on a call from server code, use `CallsClient` from `@polymorfa/sdk/calls`.
For browser calling, see the [Calls guide](/guides/calls/overview).

| Task | Permission |
| - | - |
| Check a call, read call settings, permissions, SIP trunks, and the call policy | `sessions:read` |
| Place, answer, decline, and end calls; change call settings, SIP trunks, and the call policy | `sessions:manage` |

## Place a call

```typescript theme={null}
const placed = await messaging.voip.place(
  { session: "<number-id>", to: "+15551234567", participant: "agent-7" },
  { idempotencyKey: "call-order-1042" },
);

console.log(placed.data.data.callId);
```

`session` is the Number that places the call. `participant` names who acts on
the call for your server, as `server:agent-7`. It defaults to `default`. Set
`video: true` for a video call.

Pass an idempotency key so the SDK can retry the request without placing a
second call.

## Check before you call

```typescript theme={null}
const check = await messaging.voip.check({ session: "<number-id>", to: "+15551234567" });

if (!check.data.data.allowed) {
  console.log("Call would fail:", check.data.data.refusal);
}
```

`check` runs the same checks as `place` without placing a call. `refusal` is
`calls_disabled`, `call_recipient_opted_out`, `call_destination_blocked`,
`call_permission_required`, or `call_limit_reached`. `place` runs the checks
again, so a call can still fail if something changed in between.

## Answer, decline, and end calls

Incoming calls arrive as `call.received` webhooks. A call rings until someone
answers or declines it. Nothing answers automatically.

```typescript theme={null}
const accepted = await messaging.voip.accept("<call-id>", {
  exclusive: true,
  participant: "agent-7",
});
console.log(accepted.data.data.answeredBy); // "server:agent-7"

await messaging.voip.reject("<call-id>");
await messaging.voip.end("<call-id>");
```

* `accept` answers a ringing call. Later accepts by other participants join it,
  unless the first participant set `exclusive: true`. Then they fail with
  `409 call_claimed`.
* `reject` declines a ringing call. It fails with `409 call_not_ringing`
  otherwise.
* `end` ends the call for everyone. `leave(callId, { connectionId })` closes one
  of your connections and leaves the call running.
* `addParticipant(callId, { to })` invites another WhatsApp user.

## Ask for call permission

WhatsApp requires a person's permission before an Official API Number calls
them. Read the current state, then ask with a message:

```typescript theme={null}
const permission = await messaging.voip.retrieveCallPermission("<number-id>", "+15551234567");
console.log(permission.data.data.status); // "none", "temporary", "permanent", or "revoked"

if (permission.data.data.status === "none") {
  await messaging.messages.send("<number-id>", {
    conversation: { phoneNumber: "+15551234567" },
    content: { callPermissionRequest: { body: "Can we call you about order 1522?" } },
  });
}
```

WhatsApp allows one request per person every 24 hours and two every 7 days. A
request over the limit fails with `call_permission_request_limited`. Changes
arrive as `call.permission_changed` webhooks. Linked-device Numbers do not use
call permission.

## Change call settings for a Number

```typescript theme={null}
const current = await messaging.voip.retrieveCallSettings("<number-id>");

await messaging.voip.updateCallSettings("<number-id>", {
  callsEnabled: true,
  conferenceMode: false,
  expectedRevision: current.data.data.revision,
});
```

An update changes only the fields you send.

* `callsEnabled: false` turns calling off for the Number. New calls fail with
  `calls_disabled` and incoming calls are declined. Calls in progress continue.
* `conferenceMode` (default `true`) lets every participant you connect hear each
  other. With `false`, each hears only the WhatsApp caller.
* `inboundRoute: "sip_trunk"` with `sipTrunkId` sends incoming calls to a SIP
  trunk. `clients` (the default) rings your apps and browsers.

A stale `expectedRevision` fails with `409 state_conflict`.

## Speak on a call from server code

`CallsClient` follows one Number's calls and gives you each call's audio, for
example to run a voice agent:

```typescript theme={null}
import { CallsClient } from "@polymorfa/sdk/calls";

const calls = new CallsClient({
  apiKey: process.env.POLYMORFA_KEY!,
  session: "<number-id>",
  participant: "voice-agent",
});

calls.on("incoming", async (call) => {
  await call.answer({ exclusive: true });
  call.audio.on("data", (pcm) => console.log("received", pcm.byteLength, "bytes"));
});

await calls.connect();
```

`call.audio` carries signed 16-bit mono PCM at `call.audio.sampleRate`. Write
your own audio with `call.audio.write()`. The client reconnects after a dropped
connection. Call `disconnect()` to stop following the Number.

## Connect a SIP trunk

A SIP trunk connects your PBX to a project's calls. Read the address to
configure in your PBX:

```typescript theme={null}
const endpoint = await platform.sipTrunks.endpoint();
if (endpoint.data.status === "hosted") {
  console.log(endpoint.data.host, endpoint.data.transports, endpoint.data.rtp);
}
```

Then create the trunk on a project client:

```typescript theme={null}
const created = await project.sipTrunks.create({
  name: "Head office PBX",
  direction: "both",
  outbound: { targetUri: "sips:pbx.example.com", transport: "tls" },
  inbound: { session: "<number-id>", allowedAddresses: ["203.0.113.10"] },
});

// Store the inbound password now. The API does not return it again.
console.log(created.data.inboundCredentials);
```

Disable a trunk with `update(trunkId, { enabled: false, expectedRevision })`.
`rotateCredentials(trunkId)` issues a new inbound password. A team client passes
the project ID as the first argument to `list` and `create`. See
[SIP trunks](/guides/calls/sip-trunks).

## Block countries and keep a do-not-call list

The team's call policy blocks calls to country calling codes. It needs a team
key:

```typescript theme={null}
const policy = await platform.callPolicy.retrieve();

await platform.callPolicy.update({
  blockedCountryCodes: ["44", "1876"],
  expectedRevision: policy.data.revision,
});
```

`update` replaces the whole list. Codes are 1 to 4 digits without `+`; `1876`
blocks Jamaica without blocking the rest of `+1`. While any code is blocked,
calls to people whose phone number is unknown are refused too.

The do-not-call list blocks calls to specific people:

```typescript theme={null}
const added = await platform.callOptOuts.create({
  phoneNumber: "+14155550123",
  note: "Asked not to be called",
});

for await (const entry of await platform.callOptOuts.list({ limit: 100 })) {
  console.log(entry.id, entry.phoneNumber ?? entry.bsuid);
}

await platform.callOptOuts.delete(added.data.id);
```

A call to a listed person fails with `call_recipient_opted_out`. Matching uses
the identifier you stored: a phone number entry does not block a call addressed
by user ID. `import({ entries })` adds up to 5,000 entries at once.

For call history and statistics, see [Call records](/sdks/typescript/call-records).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.