Skip to main content
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.
This is different from pinless dial-in, 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.

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

Requirements

  • A paid Daily account and a Daily API key.
  • 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:
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:
Use the exact sip_uri returned by the create call — don’t hand-build it. Point your carrier’s origination/termination trunk at that host.

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).
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.
The response includes the sip_uri to give your carrier and the generated hmac:
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.
    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.
  • room_template — room-creation properties, passed through to Daily’s room create. 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.
    In production: set recording, geo, permissions, prejoin, etc. here so every auto-provisioned room is configured consistently.
  • 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:

IP allowlist

trunk_config.allowed_ips — a list of IPv4 addresses or CIDR blocks (up to 25). Only INVITEs from these source IPs are admitted.

Digest credential

trunk_config.credential = { username, password }. Daily challenges the carrier with 407 and verifies the digest. The password is write-only (never returned by GET).
  • 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.
  • 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.

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

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.
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.
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 if you need it. Either way Daily transcodes between the carrier codec and the WebRTC side (see the pricing page 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.
  • sipHeadersX-* headers your carrier attached to the INVITE (up to 32), passed through with their original header names and values (e.g. X-Twilio-CallSid).
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.

Compared with pinless dial-in

Daily has a second inbound-call webhook, pinless dial-in. 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: 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.
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.

Receive the notification

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

Verify the signature

Every notification is signed (there is no unsigned mode). Daily sends two headers:
  • X-Siptrunk-Timestamp — unix epoch milliseconds.
  • X-Siptrunk-SignatureHMAC-SHA256(base64-decoded hmac) over `${timestamp}.${rawBody}`, where rawBody is the request body exactly as received.
Keep your hmac secret private — anyone with it can forge webhook requests. Reject any notification whose signature doesn’t match.
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:
1

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

Join the room

Join room.url with your bot/agent (for example a Pipecat bot).
3

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

Start the conversation

Greet and run your bot logic, using the caller metadata from the notification.
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.

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

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

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 for rates, or contact support.

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

Twilio

Telnyx

Vonage