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

# SIP trunks

> Connect your existing PBX or SIP trunk to WhatsApp calls on a connected Number.

A SIP trunk connects a phone system you already run, such as a PBX or contact
center platform, to the WhatsApp calls of a connected Number. Incoming WhatsApp
calls can ring on your PBX, and your PBX can call WhatsApp users through the
Number. Polymorfa does not host a PBX and does not provide phone numbers.

SIP trunks are part of Calls. Every team that uses Calls can create trunks in
any of its projects; no enrollment is needed. In the Console, team owners and
admins add, change, and delete trunks. To get the SIP address and ports your
PBX connects to, email [support@polymorfa.com](mailto:support@polymorfa.com).

## How a trunk works

A trunk belongs to a project and has a direction:

| `direction` | Effect                                                                  |
| ----------- | ----------------------------------------------------------------------- |
| `outbound`  | Polymorfa sends incoming WhatsApp calls of routed sessions to your PBX. |
| `inbound`   | Your PBX places WhatsApp calls through a session of the project.        |
| `both`      | Both of the above.                                                      |

On a call, the trunk is a participant named `server:sip:<trunkId>`. Its
connection reports transport `sip`. The people on WhatsApp always hear your
PBX, and your PBX always hears them. It never hears its own audio. Whether your
PBX and your other participants hear each other follows the session's
[conference mode](/guides/communicate/calls#conference-mode): on (the default),
they hear each other; off, each hears only the people on WhatsApp. SIP trunks
carry audio only.

Manage trunks with the Platform API. Reading requires `sessions:read`; every
change requires `sessions:manage`. Use a team key with `projectId`, or a project
token for its own project. Client tokens cannot manage trunks.

The TypeScript SDK adds `Client.sipTrunks` in its next release; until then, call
the API directly. You can also manage trunks in the Console: open a project, go to **Calls**, and
choose **Manage SIP trunks**. See [Calls in the Console](/console/calls#sip-trunks).

## Create a trunk

```bash theme={null}
curl -X POST "https://api.polymorfa.com/platform/sip-trunks" \
  -H "Authorization: Bearer $POLYMORFA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "PROJECT_ID",
    "name": "Head office PBX",
    "direction": "both",
    "outbound": {
      "targetUri": "sip:whatsapp@pbx.example.com:5061",
      "transport": "tls",
      "authUsername": "polymorfa",
      "authPassword": "PBX_PASSWORD"
    },
    "inbound": {
      "session": "support",
      "allowedAddresses": ["203.0.113.10", "198.51.100.0/28"],
      "allowedDestinations": ["+44", "+1415"]
    },
    "codecs": ["PCMA", "PCMU", "opus"],
    "maxConcurrentCalls": 30
  }'
```

The response returns `201` with the trunk and, for `inbound` and `both`
trunks, `inboundCredentials`:

```json theme={null}
{
  "success": true,
  "data": {
    "trunk": { "id": "TRUNK_ID", "direction": "both", "revision": 1, "...": "..." },
    "inboundCredentials": {
      "username": "tq7m4k2xmabn5c3dpe6fr2hw",
      "password": "SHOWN_ONCE",
      "realm": "polymorfa"
    }
  }
}
```

<Warning>
  The inbound password appears only in this response and in a credential
  rotation response. Store it in your PBX right away. Responses never return
  passwords again; `outbound.hasPassword` shows whether one is stored.
</Warning>

A project holds up to 100 trunks. Each trunk carries at most
`maxConcurrentCalls` calls at once (1 to 500, default 30).

## Send WhatsApp calls to your PBX

Set these fields in `outbound`:

| Field          | Description                                                                                                                                                                |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `targetUri`    | `sip:` or `sips:` URI of your PBX with a public host name or IP address and an optional port. A user part, such as `sip:support@pbx.example.com`, becomes the called user. |
| `transport`    | `udp`, `tcp`, or `tls`. A `sips:` URI requires `tls`. The default port is 5060, or 5061 for `tls`.                                                                         |
| `authUsername` | Username Polymorfa answers with when your PBX sends a digest challenge. Set it together with `authPassword`.                                                               |
| `authPassword` | Write-only password for `authUsername`.                                                                                                                                    |
| `fromUser`     | User part of the From header. When unset, calls arrive from `whatsapp`.                                                                                                    |

Then route the session's incoming calls to the trunk:

```bash theme={null}
curl -X PUT "https://api.polymorfa.com/platform/sessions/support/call-settings" \
  -H "Authorization: Bearer $POLYMORFA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "inboundRoute": "sip_trunk",
    "sipTrunkId": "TRUNK_ID",
    "sipClaim": true
  }'
```

The trunk must belong to the session's project and have direction `outbound`
or `both`. Settings you omit keep their values; a later update without
`sipTrunkId` keeps the trunk. To stop sending calls to the trunk, send
`"inboundRoute": "clients"`, which also clears `sipTrunkId`. On a Cloud API
Number, incoming calls reach the trunk only when `hostCloudApiCalls` is `true`;
see [Cloud API Numbers](/guides/communicate/calls#cloud-api-numbers). See
[Update call settings](/guides/communicate/api/update-call-settings).

When a routed call rings, Polymorfa calls `targetUri`. The call still rings for
your SDK participants, and they can answer it first. The request to your PBX
carries the WhatsApp call ID in the `X-Polymorfa-Call-ID` header.

* When your PBX answers, Polymorfa answers the WhatsApp call as
  `server:sip:<trunkId>`. With `sipClaim: true` the answer claims the call: other
  participants stop ringing and cannot join. With `sipClaim: false` the call stays
  shared and other participants can join. See
  [Answer, join, or decline](/guides/communicate/calls#answer-join-or-decline).
* When your PBX does not answer, the WhatsApp call keeps ringing for your other
  participants until it is answered, declined, or the ring window ends.
* When your PBX hangs up, its connection leaves. If the trunk claimed the call,
  the call ends. When the WhatsApp call ends, Polymorfa hangs up your PBX.

The trunk's `call.connection_left` event reports why its connection left:

| `reason`          | Meaning                                                                                                                             |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `left`            | Your PBX hung up.                                                                                                                   |
| `call_ended`      | The WhatsApp call ended.                                                                                                            |
| `claimed`         | Another participant claimed the call first.                                                                                         |
| `sip_busy`        | Your PBX answered busy (`486` or `600`).                                                                                            |
| `sip_declined`    | Your PBX declined the call (`603` or `403`).                                                                                        |
| `sip_no_answer`   | Your PBX did not answer within 60 seconds, or answered `408`, `480`, or `487`.                                                      |
| `sip_auth_failed` | Your PBX rejected `authUsername` and `authPassword`.                                                                                |
| `sip_unavailable` | The target was unreachable, refused the call, or returned an unusable answer, including a media address Polymorfa does not send to. |

A trunk that never answered reports only the departure, with no preceding join.

## Call WhatsApp from your PBX

Configure your PBX with:

* the SIP address and ports Polymorfa support gave you;
* `inboundCredentials.username` and `password` for digest authentication; and
* the WhatsApp number to call, in E.164 format, as the user part of the request
  URI, for example `sip:+14155550100@SIP_ADDRESS`. The leading `+` is optional
  and a `00` prefix is accepted.

Set `inbound.session` to the session that places the calls. Polymorfa accepts a
call only when the digest credentials are valid, the request comes from one of
`allowedAddresses`, the trunk is enabled, its project is active, and the
number matches one of
`allowedDestinations` (or the list is empty). The call counts toward the same
per-minute outbound limit as calls placed through the API from that session.

The trunk claims every call it places. Your PBX receives:

| Response | When                                                                                                                                                                     |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `401`    | The request had no credentials, or it reused an expired or spent challenge. Answer the new challenge.                                                                    |
| `403`    | Authentication failed, the address or destination is not allowed, the trunk is disabled, or calling is turned off for its session.                                       |
| `404`    | The request URI user is not a phone number, or the number is not on WhatsApp.                                                                                            |
| `180`    | The WhatsApp user's phone is ringing.                                                                                                                                    |
| `200`    | The WhatsApp user answered.                                                                                                                                              |
| `408`    | The WhatsApp user did not answer.                                                                                                                                        |
| `480`    | No session is set, or the call could not be connected.                                                                                                                   |
| `486`    | The trunk is already carrying `maxConcurrentCalls` calls.                                                                                                                |
| `488`    | The offer has no supported codec, its media security does not match the transport, or its media address is not accepted (see [Media and security](#media-and-security)). |
| `503`    | Calling capacity or the per-minute limit was reached. Retry later, after the `Retry-After` interval when one is sent.                                                    |
| `603`    | The WhatsApp user declined the call.                                                                                                                                     |

Every authentication refusal is the same `403`, so the response does not
reveal which check failed. Repeated failed authentication temporarily blocks
the sending address. When your PBX cancels a ringing call, the WhatsApp call
ends. When your PBX hangs up a connected call, the call ends for everyone.

## Media and security

* **Codecs.** PCMU and PCMA (8 kHz) and Opus (48 kHz, mono), in the order of
  `codecs`. Your PBX must offer media in the initial INVITE; calls without an
  offer are refused with `488`. Mid-call media changes are refused.
* **Packets.** 20 ms of audio per RTP packet.
* **Encryption.** With `tls`, signaling is encrypted and media uses SRTP with
  SDES keys (`AES_CM_128_HMAC_SHA1_80` or `AES_CM_128_HMAC_SHA1_32`). Plain RTP
  offers over TLS and SRTP offers over UDP or TCP are refused. Use `tls` when
  your PBX is reachable over the internet.
* **Addresses.** Send media from the address in your PBX's session
  description or from the address your signaling comes from. Polymorfa sends
  media back to the address your first media packet arrives from.
* **PBX behind NAT.** A private address (`10.0.0.0/8`, `172.16.0.0/12`,
  `192.168.0.0/16`, `100.64.0.0/10`, or `fc00::/7`) in the session description
  works only when your signaling comes from a public address. Polymorfa then
  sends no media until your PBX's first media packet arrives from that
  signaling address, so your PBX must start sending media.
* **Refused addresses.** Session descriptions with a loopback, link-local,
  multicast, unspecified, or reserved media address are refused: your PBX's
  call gets `488`, and a call to your PBX ends with `sip_unavailable`. Trunk
  targets on private, loopback, or link-local addresses are rejected.

## Manage trunks

| Operation                   | Request                                           |
| --------------------------- | ------------------------------------------------- |
| List a project's trunks     | `GET /platform/sip-trunks?projectId=PROJECT_ID`   |
| Read a trunk                | `GET /platform/sip-trunks/{trunkId}`              |
| Change a trunk              | `PATCH /platform/sip-trunks/{trunkId}`            |
| Delete a trunk              | `DELETE /platform/sip-trunks/{trunkId}`           |
| Rotate the inbound password | `POST /platform/sip-trunks/{trunkId}/credentials` |

* `PATCH` changes only the fields you send. Omit `authPassword` to keep the
  stored password; send `authUsername: null` to remove both. Send
  `expectedRevision` to refuse the change with `409 sip_trunk_revision_conflict`
  when someone else changed the trunk first.
* Changing `direction` to `outbound` removes the inbound credentials. Changing an
  `outbound` trunk to `inbound` or `both` creates a new username; rotate its
  credentials to get the password.
* Rotating credentials replaces the password. The old password stops working
  within about a minute. Rotation returns `409 state_conflict` for `outbound`
  trunks.
* Deleting a trunk that a session still routes to returns
  `409 sip_trunk_in_use`. Route the session back to `clients` first.
* Changes reach call routing within about a minute. Calls in progress continue.
* When a trunk's project is deactivated, changing the trunk or rotating its
  credentials returns `404 resource_not_found`. Polymorfa sends no calls to the
  trunk and refuses its PBX's calls within about a minute. You can still list,
  read, and delete the trunk.

## Errors

| Status | `code`                        | Meaning                                                                                 |
| ------ | ----------------------------- | --------------------------------------------------------------------------------------- |
| `400`  | `invalid_parameter`           | A field is missing or invalid; `param` names it.                                        |
| `400`  | `invalid_sip_trunk`           | The routed trunk is not in the session's project or cannot receive calls.               |
| `403`  | `permission_denied`           | The credential is a client token or cannot reach that project.                          |
| `404`  | `resource_not_found`          | The trunk or project does not exist for your credential, or the project is deactivated. |
| `409`  | `sip_trunk_in_use`            | A session routes calls to the trunk.                                                    |
| `409`  | `sip_trunk_revision_conflict` | The trunk changed after the revision you sent.                                          |
| `409`  | `sip_trunk_limit`             | The project already has 100 trunks.                                                     |
| `503`  | `sip_unavailable`             | SIP trunks are not available right now.                                                 |

See [Errors](/api/errors) for the full error format.
