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

# SIP Trunk (inbound)

> Point your own SIP carrier at Daily and auto-provision a dial-in room for every inbound call — no per-room setup, no PIN.

A **SIP trunk** lets you bring your own carrier (Twilio, Telnyx, Vonage, or any SIP provider) and send
inbound calls straight to Daily. You create one trunk per domain; every call to that trunk's SIP URI makes Daily
**provision a dial-in room on the fly**, bridge the caller in, and notify your backend so your bot or agent can
join. There's no per-room configuration and no caller PIN.

This suits contact centers, voice bots, and IVR flows where the destination room isn't known until the call arrives.

<Note>
  This is different from [pinless dial-in](/docs/guides/features/dial-in-dial-out/sip-interconnect-pinless), where
  you buy a Daily number (or use a static SIP URI) and connect held calls yourself with `pinlessCallUpdate`. With a
  SIP trunk you bring your **own** carrier and Daily provisions the room automatically.
</Note>

## How it works

1. Your carrier sends an INVITE to your trunk's SIP URI (`sip:<number>@<domain>-<trunk>.siptrunk.sip-us.daily.co`).
2. Daily immediately replies `180 Ringing`, so the caller hears ringback while it provisions.
3. Daily creates a dial-in room from the trunk's stored `room_template` and POSTs a signed `sip_trunk.incoming`
   webhook to your `notification.webhook_url` — while, in parallel, it brings up the SIP worker.
4. Your backend returns 2xx and joins the room (e.g. a Pipecat bot). Daily keeps the caller in ringback until
   that 2xx arrives.
5. Daily answers (`200 OK`) and bridges the caller into the room — two-way audio, with your bot already there.
6. Your bot waits for the caller to appear as a participant, then starts the conversation.

```mermaid theme={null}
sequenceDiagram
    participant Carrier as Your carrier
    participant Daily
    participant You as Your backend/bot
    Carrier->>Daily: INVITE sip:+1…@<domain>-<trunk>.siptrunk…
    Daily-->>Carrier: 180 Ringing (ringback)
    Daily->>Daily: create room (from room_template)
    par
        Daily->>You: POST sip_trunk.incoming (signed)
        You-->>Daily: 2xx, then join room.url
    and
        Daily->>Daily: start SIP worker
    end
    Daily-->>Carrier: 200 OK (two-way audio)
    You->>Carrier: greet once the caller appears
```

<Warning>
  Your webhook is **on the call path**: the caller stays in ringback until you return 2xx, and a failure rejects the
  call with `480`. Respond 2xx immediately and start your bot asynchronously — never do slow work before
  responding. See [Connect your bot](#connect-your-bot).
</Warning>

## Requirements

* A **paid Daily account** and a [Daily API key](/docs/rest-api/authentication).
* **Your own SIP carrier / DID** — Daily terminates the trunk; you bring the phone number and origination trunk.
* A publicly reachable **HTTPS `notification.webhook_url`** on your backend.
* Your carrier must be able to reach Daily's SIP ingress: **UDP/TCP on 5060, TLS on 5061**.

## The trunk SIP URI

Each trunk has a host of the form:

```
<domain>-<trunk_name>.siptrunk.sip-us.daily.co
```

Daily joins the two halves with the hyphen, so a trunk named `support` on the `examplecorp` domain becomes
`examplecorp-support.siptrunk.sip-us.daily.co`. That hyphen is the separator — it is not part of `trunk_name`, which is
why `trunk_name` itself may not contain one.

Your carrier fills the user part with the dialed number, so the address it sends INVITEs to looks like:

```
sip:+15551234567@examplecorp-support.siptrunk.sip-us.daily.co
```

<Tip>
  Use the exact `sip_uri` returned by the [create call](#create-a-trunk) — don't hand-build it. Point your
  carrier's origination/termination trunk at that host.
</Tip>

## Create a trunk

`POST /sip-trunk` with your domain-scoped API key. The `notification.webhook_url` is **required**; `hmac` is
optional — if you omit it, Daily generates one and returns it (see [Notifications](#receive-the-notification)).

<Warning>
  Your webhook must be **live before you create the trunk**: during create (and on any update that touches
  `notification`), Daily sends a signed one-shot `{"event": "sip_trunk.test"}` POST to `webhook_url`. If the
  endpoint doesn't answer 2xx, the API call fails with `400`. Use the probe to verify your signature check too — it
  carries the same `X-Siptrunk-Timestamp` / `X-Siptrunk-Signature` headers real notifications use.
</Warning>

```bash theme={null}
curl --request POST \
  --url https://api.daily.co/v1/sip-trunk \
  --header 'Authorization: Bearer $DAILY_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "trunk_name": "support",
    "description": "Support line — general voice bot",
    "room_template": {
      "sip": { "display_name": "Support Bot" },
      "enable_recording": "cloud"
    },
    "notification": {
      "webhook_url": "https://example.com/daily/sip-trunk"
    },
    "trunk_config": {
      "allowed_ips": ["54.172.60.0/30"],
      "exp_offset": 3600
    }
  }'
```

The response includes the `sip_uri` to give your carrier and the generated `hmac`:

```json theme={null}
{
  "id": "5f8c2d9a-…",
  "trunk_name": "support",
  "sip_uri": "sip:examplecorp-support.siptrunk.sip-us.daily.co",
  "enabled": true,
  "config": {
    "room_template": { "sip": { "display_name": "Support Bot" }, "enable_recording": "cloud" },
    "notification": { "webhook_url": "https://example.com/daily/sip-trunk", "hmac": "<base64-secret>" },
    "trunk_config": { "allowed_ips": ["54.172.60.0/30"], "exp_offset": 3600, "is_open": false }
  }
}
```

**Field notes**

* **`trunk_name`** — lowercase alphanumeric, 3–63 chars, **no hyphens** (it's combined with your domain into the
  host label). The combined `<domain>-<trunk_name>` label must fit in one DNS label (≤ 63 chars total). Immutable
  after create.
* **`description`** — free text (≤2048 chars). It's echoed back to you in the notification as `trunk.description`.
  <Tip>**In production:** put a bot/persona/config identifier here (for example, one trunk per department). Your
  webhook reads `trunk.description` and launches the matching bot or prompt.</Tip>
* **`room_template`** — room-creation properties, passed through to Daily's
  [room create](/reference/rest-api/rooms/create-room). An optional `sip` block (display name, video, codecs,
  `dialin_config`) is applied when the SIP worker starts; sensible dial-in defaults are used if you omit it.
  Absolute `exp`/`nbf` are rejected — use `trunk_config.exp_offset` instead. <Tip>**In production:** set
  recording, `geo`, permissions, prejoin, etc. here so every auto-provisioned room is configured
  consistently.</Tip>
* **`exp_offset`** — room lifetime in seconds (60–86400, default 3600). Size it to your expected maximum call
  duration; idle / no-show rooms self-clean.
* **`notification`** / **`trunk_config`** — covered below.

## Authentication and admission

A trunk must be **either gated or explicitly open — never both, never neither**. Configure at least one admission
mechanism, or explicitly opt into an open trunk:

<CardGroup cols={2}>
  <Card title="IP allowlist" icon="network-wired">
    `trunk_config.allowed_ips` — a list of IPv4 addresses or CIDR blocks (up to 25). Only INVITEs from these source
    IPs are admitted.
  </Card>

  <Card title="Digest credential" icon="key">
    `trunk_config.credential` = `{ username, password }`. Daily challenges the carrier with `407` and verifies the
    digest. The password is **write-only** (never returned by `GET`).
  </Card>
</CardGroup>

```json theme={null}
"trunk_config": {
  "credential": { "username": "examplecarrier", "password": "a-strong-password-12chars" },
  "allowed_ips": ["203.0.113.10", "198.51.100.0/24"]
}
```

* `username`: `^[a-z0-9][a-z0-9._-]{2,63}$`. `password`: 12–64 chars, at least one letter and one digit.
* If you configure **both**, both are enforced — the INVITE must come from an allowlisted IP **and** answer the
  digest challenge.
* <Tip>**In production:** an **IP allowlist** suits CPaaS carriers with stable, published signaling IP ranges
  (Twilio, Telnyx); a **digest credential** suits carriers or on-prem PBXs behind NAT or with dynamic IPs.</Tip>

### Open trunks (`is_open`)

Every trunk needs at least one of three things: `allowed_ips`, a `credential`, or `is_open: true`. A trunk
created with none of them is rejected — running with no admission control has to be a deliberate choice, not
an omission.

<Warning>
  `is_open: true` accepts **any** INVITE to the trunk host, and the host is guessable. Use it only for quick
  local testing with a softphone. **Do not use `is_open` in production.** Gate with `allowed_ips`, a
  `credential`, or both.
</Warning>

## Transport and security

Send SIP over **TLS (port 5061)** in production; UDP/TCP on 5060 is available for testing. The trunk host is
covered by Daily's wildcard TLS certificate — present the trunk host as the SNI.

**Encrypted media (SRTP) is supported on the carrier leg.** If your carrier offers SRTP, Daily negotiates it
with SDES using `AES_CM_128_HMAC_SHA1_80`, `AES_CM_128_HMAC_SHA1_32`, or `F8_128_HMAC_SHA1_80`. If your carrier
offers plain RTP, the call connects unencrypted. Media between Daily and the WebRTC side is always encrypted
(DTLS-SRTP) either way.

<Warning>
  Turn SRTP on **together with TLS signaling on port 5061**. SDES carries the media key in the SDP body, so over
  UDP or TCP that key travels in the clear and the encryption buys you nothing. Some carriers couple the two for
  you — Twilio's Secure Trunking enables TLS and SRTP under a single setting — but others expose them as
  independent switches.
</Warning>

Encryption follows whatever your carrier offers. Daily has no per-trunk setting that requires it and will not
reject a plain-RTP call, so if encrypted media is a requirement for you, enforce it on the carrier side.

## Codecs

By default a trunk room negotiates **one** audio codec with your carrier: `PCMU` (G711µ) unless
`room_template.sip.codecs.audio` names another — `PCMA` (G711a), `G722`, or `OPUS`, for example
`"sip": { "codecs": { "audio": ["G722"] } }`. Make sure your carrier offers that codec in its INVITE: an offer
without it is rejected with `488 Not Acceptable Here`. Daily can instead enable multi-codec negotiation for your
domain, where any of those four codecs offered by the carrier connects, with `codecs.audio` as the order of
preference — contact [support](mailto:help@daily.co) if you need it. Either way Daily transcodes between the
carrier codec and the WebRTC side (see the [pricing page](https://www.daily.co/pricing/) for transcoding charges).

## Caller metadata and custom headers

The `sip_trunk.incoming` notification carries caller context so your bot has it before the conversation starts:

* **`From`**, **`fromDisplayName`**, **`fromDomain`** — the caller's number/identity.
* **`pAssertedIdentity`**, **`diversion`** — network-asserted identity and forwarded-call context.
* **`sipHeaders`** — `X-*` headers your carrier attached to the INVITE (up to 32), passed through with their
  original header names and values (e.g. `X-Twilio-CallSid`).

<Tip>
  **In production:** have your carrier inject `X-*` headers (for example `X-Account-Id`, `X-Department`,
  `X-Ticket-Id`) and read them from `call.sipHeaders` to map the call to your business context or route it to the
  right team. Use `From` / `pAssertedIdentity` for caller identification and screening, and `diversion` for
  forwarded-call handling.
</Tip>

### Compared with pinless dial-in

Daily has a second inbound-call webhook,
[pinless dial-in](/docs/guides/features/dial-in-dial-out/sip-interconnect-pinless). Both deliver an incoming
call to your backend, and the fields they share — `From`, `To`, `sourceIp`, `sipHeaders` — carry the same
meaning and the same names in both.

The differences come from where each sits in the call path:

|                     | SIP trunk                                                                       | Pinless dial-in                                             |
| ------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| Fires               | after the room exists                                                           | before — you create the room, then call `pinlessCallUpdate` |
| Room                | `room` — created before you are notified                                        | not yet known — you create it after the webhook             |
| SIP-level fields    | `sipCallIdHeader`, `requestUri`, `fromDomain`, `pAssertedIdentity`, `diversion` | not available                                               |
| Carrier identifiers | in `sipHeaders` (e.g. `X-Twilio-CallSid`)                                       | `callId`, `callDomain`                                      |
| `sipHeaders`        | `X-*` headers from your carrier's INVITE                                        | present on SIP-interconnect calls; empty for PSTN           |
| Signature headers   | `X-Siptrunk-Signature`, `X-Siptrunk-Timestamp`                                  | `X-Pinless-Signature`, `X-Pinless-Timestamp`                |

A SIP trunk receives your carrier's INVITE directly, so it can pass SIP-level detail through. Pinless calls
reach Daily through an intermediary that terminates the incoming SIP leg, so those headers do not survive; on
PSTN calls there are no SIP headers at all.

<Note>
  `callId` in the pinless webhook and `sipCallIdHeader` here are **not** the same value. `callId` is the
  carrier's own call-record identifier; `sipCallIdHeader` is the SIP `Call-ID` header from the INVITE. A SIP
  trunk cannot offer an equivalent of `callId`, because each carrier names its call-record id differently —
  your carrier's version arrives in `sipHeaders` instead.
</Note>

## Receive the notification

When a call arrives, Daily POSTs `sip_trunk.incoming` to your `notification.webhook_url`:

```json theme={null}
{
  "event": "sip_trunk.incoming",
  "trunk": { "id": "5f8c2d9a-…", "description": "Support line — general voice bot", "trunk_name": "support" },
  "room": { "name": "abc123", "url": "https://examplecorp.daily.co/abc123", "exp": 1837000000 },
  "call": {
    "sipCallIdHeader": "a7bf8461-…",
    "From": "sip:+15551234567@carrier",
    "To": "sip:+15550001111@examplecorp-support.siptrunk.sip-us.daily.co",
    "requestUri": "sip:+15550001111@examplecorp-support.siptrunk.sip-us.daily.co",
    "sourceIp": "54.172.60.1",
    "fromDomain": "carrier.example.com",
    "fromDisplayName": "Jane Doe",
    "diversion": "<sip:+15559998888@carrier>;reason=unconditional",
    "pAssertedIdentity": "\"Jane Doe\" <sip:+15551234567@carrier>",
    "sipHeaders": { "X-Account-Id": "acct_42" }
  },
  "domain": "examplecorp",
  "timestamp": 1837000000000
}
```

### Verify the signature

Every notification is **signed** (there is no unsigned mode). Daily sends two headers:

* `X-Siptrunk-Timestamp` — unix epoch milliseconds.
* `X-Siptrunk-Signature` — `HMAC-SHA256(base64-decoded hmac)` over `` `${timestamp}.${rawBody}` ``, where
  `rawBody` is the request body exactly as received.

```javascript theme={null}
// The hmac returned when you created (or rotated) the trunk.
const hmacSecret = '<base64-secret>';

// Verify against the RAW request body bytes (e.g. express.raw() or a rawBody capture) —
// don't re-serialize the parsed JSON, which may not byte-match what was signed.
const timestamp = req.headers['x-siptrunk-timestamp'];
const signedContent = `${timestamp}.${rawBody}`;
const key = Buffer.from(hmacSecret, 'base64');
const expected = crypto.createHmac('sha256', key).update(signedContent).digest('base64');
// expected should equal req.headers['x-siptrunk-signature']
```

<Warning>
  Keep your `hmac` secret private — anyone with it can forge webhook requests. Reject any notification whose
  signature doesn't match.
</Warning>

**Rotating the secret.** Send `PUT /sip-trunk/{id}` with `{"notification": {"hmac": null}}` — Daily generates a
fresh secret and returns it. (You can also set an explicit value.) A trunk is never left without an hmac. Any
update that touches `notification` re-sends the signed `sip_trunk.test` probe — signed with the **new** secret —
and fails with `400` if your endpoint doesn't accept it.

## Connect your bot

Daily holds the caller in ringback until you acknowledge the notification, so your bot is in the room before the
caller is. On `sip_trunk.incoming`:

<Steps>
  <Step title="Respond 2xx immediately">
    Acknowledge first, then do everything else asynchronously. The caller is waiting on this response; after about
    12 seconds without a 2xx the call is rejected. Respond 2xx even to a notification you have already handled (a
    retry, or a `duplicate: true` branch) — a 4xx rejects that branch.
  </Step>

  <Step title="Join the room">
    Join `room.url` with your bot/agent (for example a [Pipecat bot](https://docs.pipecat.ai/pipecat/telephony/daily-pstn)).
  </Step>

  <Step title="Wait for the caller">
    Wait for the dial-in participant to join with a live audio track — the `participant-joined` event for a
    participant whose `session_id` is not your own. This confirms the caller is actually present; don't greet before
    it.
  </Step>

  <Step title="Start the conversation">
    Greet and run your bot logic, using the caller metadata from the notification.
  </Step>
</Steps>

<Warning>
  **Leave on your own timeout if no caller joins.** You can be notified about a call that never arrives: the caller
  hangs up during ringback, or Daily rejects the call after notifying you. Nothing ends your bot's wait except your
  bot — while it is in the room the room's `max_idle_timeout_sec` does not fire. Treat "no caller within N
  seconds" as normal, not as an error. And don't leave **early**: once your bot has been in the room, leaving
  ejects the SIP worker immediately, and a caller bridged a moment later gets a failure.
</Warning>

### Duplicate notifications (carrier failover)

If your carrier gets no timely response from one Daily SIP node it may fail the same call over to another one. In
that rare case you can receive **two** `sip_trunk.incoming` notifications for one call — the same
`call.sipCallIdHeader` but a **different** `room`. The later notification carries `call.duplicate: true` (and,
when known, `call.duplicateOfRoom` naming the first room).

* Do **not** key your state by `sipCallIdHeader` alone — treat `(sipCallIdHeader, room)` as the key.
* Do **not** drop flagged notifications — respond 2xx and join; the flagged branch is often the one the carrier
  completes, and a 4xx would reject it.
* Join each notified room and leave on your own timeout (the same rule as above). Exactly one room receives the
  caller; the other stays empty and self-cleans.

### When the call ends

Two settings decide how long the caller stays on the line with nobody to talk to. Both live in
`room_template.sip.dialin_config`, and a trunk sets sensible values for you:

* **`max_idle_timeout_sec`** (trunk default `30`) — how long the SIP leg may sit in the room with **no one else
  present**. It arms when the SIP worker starts — roughly when you are notified, not when the caller is bridged —
  and is cancelled while your bot is in the room.
* **`max_idle_timeout_post_conversation_sec`** (trunk default `0`) — how long the caller stays **after**
  your bot leaves.

So by default your bot has about 30 seconds from the notification to be in the room, and the caller is hung up
the moment your bot leaves — no dead air, and no carrier leg left running. Having your bot leave the room is how
you end a call.

Raise `max_idle_timeout_post_conversation_sec` if you hand a call between bots (one leaves, the next
joins); a few seconds covers the gap. Leave it unset and `max_idle_timeout_sec` applies to both windows.

Your 2xx is what bridges the caller, so the caller can be in the room before your bot has finished joining and
published audio. **`hold_music_enabled`** (trunk default `true`) covers that gap: from the moment the call is
answered until the first participant's audio track arrives the caller hears a short music loop, which then fades
out. If your bot is already publishing audio when the caller is bridged, no music plays. The music stops after at
most 60 seconds in any case, so a participant that joins without ever publishing audio does not get it under the
whole call. Set it to `false` in `room_template.sip.dialin_config` if you would rather the line stay silent.

<Note>
  The second window opens once **anyone** has shared the room with the SIP leg — including your bot joining
  before the caller is bridged. With the trunk default of `0`, a bot that leaves before the caller arrives ejects
  the SIP leg at once, so stay until the caller joins or your own timeout fires (see
  [Connect your bot](#connect-your-bot)).
</Note>

## Test it

Before wiring a carrier, point a softphone at your trunk host and place a call — confirm your webhook receives
`sip_trunk.incoming` while you hear ringback, and that you get two-way audio once it has responded 2xx. For a
fast first test you can create an `is_open` trunk, but switch to `allowed_ips` / `credential` before going live.

## Billing

<Note>
  SIP-trunk calls are billed the same as Daily's other SIP dial-in usage, per connected minute — there is no
  additional charge for using a SIP trunk. See the [pricing page](https://www.daily.co/pricing/) for rates, or
  contact [support](mailto:help@daily.co).
</Note>

## Limitations

* **Inbound termination only** — a SIP trunk accepts incoming calls; it does not place outbound calls.
* **No REGISTER-mode** — Daily accepts per-call INVITEs (IP-allowlisted or digest-challenged); it does not accept
  a standing SIP registration from your PBX.

## Troubleshooting

* **Call rejected immediately** — check admission: source IP not in `allowed_ips`, wrong digest credential, or a
  disabled trunk. An unknown trunk host returns `404`.
* **`407` loop** — your carrier isn't answering the digest challenge with the trunk's `credential`.
* **No audio** — usually a codec or TLS/SRTP mismatch; confirm your carrier offers a supported codec and reaches
  Daily on 5061 (TLS) / 5060.
* **Webhook not received / signature fails** — confirm `notification.webhook_url` is public HTTPS and that you're
  verifying against the current `hmac` (re-fetch the trunk if you rotated it).
* **Caller hears ringing, then a failure (`480 Notification Failed`)** — your webhook did not return 2xx within
  the budget: it is slow, down, redirecting, or answering non-2xx. Respond 2xx before doing any work.
* **`480 Provisioning Failed` or `480 Worker Not Ready`** — Daily could not create the room or start the SIP
  worker in time. Nothing on your side causes this; your bot may still have been notified, so it should leave on
  its own timeout as usual. Contact [support](mailto:help@daily.co) if it persists.
* **`488 Not Acceptable Here`** — your carrier's INVITE did not offer a codec the trunk room can negotiate: by
  default the single configured one, `PCMU` unless `room_template.sip.codecs.audio` says otherwise. See
  [Codecs](#codecs).
* **Bot joins but no caller ever arrives** — the caller hung up during ringback, or this was a duplicate branch
  (see above). Leave on your own timeout; this is normal.

## Carrier setup guides

<CardGroup cols={2}>
  <Card title="Twilio" href="/docs/guides/features/dial-in-dial-out/sip-trunk/twilio" />

  <Card title="Telnyx" href="/docs/guides/features/dial-in-dial-out/sip-trunk/telnyx" />

  <Card title="Vonage" href="/docs/guides/features/dial-in-dial-out/sip-trunk/vonage" />
</CardGroup>
