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
- Your carrier sends an INVITE to your trunk’s SIP URI (
sip:<number>@<domain>-<trunk>.siptrunk.sip-us.daily.co). - Daily immediately replies
180 Ringing, so the caller hears ringback while it provisions. - Daily creates a dial-in room from the trunk’s stored
room_templateand POSTs a signedsip_trunk.incomingwebhook to yournotification.webhook_url— while, in parallel, it brings up the SIP worker. - Your backend returns 2xx and joins the room (e.g. a Pipecat bot). Daily keeps the caller in ringback until that 2xx arrives.
- Daily answers (
200 OK) and bridges the caller into the room — two-way audio, with your bot already there. - Your bot waits for the caller to appear as a participant, then starts the conversation.
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_urlon 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: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:
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).
sip_uri to give your carrier and the generated hmac:
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 astrunk.description.room_template— room-creation properties, passed through to Daily’s room create. An optionalsipblock (display name, video, codecs,dialin_config) is applied when the SIP worker starts; sensible dial-in defaults are used if you omit it. Absoluteexp/nbfare rejected — usetrunk_config.exp_offsetinstead.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.
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.
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 usingAES_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.
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
Thesip_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).
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 POSTssip_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-Signature—HMAC-SHA256(base64-decoded hmac)over`${timestamp}.${rawBody}`, whererawBodyis the request body exactly as received.
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. Onsip_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.
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 twosip_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
sipCallIdHeaderalone — 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 inroom_template.sip.dialin_config, and a trunk sets sensible values for you:
max_idle_timeout_sec(trunk default30) — 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 default0) — how long the caller stays after your bot leaves.
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 receivessip_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 returns404. 407loop — your carrier isn’t answering the digest challenge with the trunk’scredential.- 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_urlis public HTTPS and that you’re verifying against the currenthmac(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 Failedor480 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,PCMUunlessroom_template.sip.codecs.audiosays 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.