{"slug":"byo-sip-trunk-livekit","title":"LiveKit BYO SIP Trunk — Inbound and Outbound with SignalWire","tags":["signalwire","livekit","byo-trunk","sip","voice-ai","integration"],"agent_summary":"Bridge SignalWire PSTN to LiveKit Agents via BYO SIP. Inbound: PSTN→SignalWire DID→SWML connect→LiveKit SIP domain. Outbound: LiveKit→SignalWire SIP Address→PSTN with TLS transport. Includes exact SWML, recording option, and the app-password requirement.","trigger_phrases":["LiveKit SignalWire BYO SIP","LiveKit inbound trunk","LiveKit outbound trunk SignalWire","LiveKit SIP domain","LiveKit transport TLS 3","LiveKit app password"],"runnable":true,"markdown":"\n# LiveKit BYO SIP Trunk on SignalWire\n\nLiveKit's Agents framework runs the AI conversation; SignalWire handles PSTN bridging. Same shape as the [VAPI BYO setup](/topic/byo-sip-trunk-vapi) — the differences are transport (LiveKit inbound needs `;transport=tcp`, outbound trunk needs TLS) and the destination SIP domain.\n\n## LiveKit Inbound — PSTN → LiveKit\n\n**Flow:**\n\n1. Caller dials a SignalWire DID.\n2. SignalWire executes the SWML.\n3. `connect` bridges to LiveKit's SIP domain.\n4. LiveKit's agent pipeline handles the call.\n\n### SWML\n\n```yaml\nversion: 1.0.0\nsections:\n  main:\n    - connect:\n        to: sip:%{call.to}@your-unique-SIP-domain-from-Livekit.com;transport=tcp\n```\n\n`;transport=tcp` is required for LiveKit inbound. Without it, calls may fail or behave unpredictably.\n\n### Optional — record the call\n\n```yaml\nversion: 1.0.0\nsections:\n  main:\n    - record_call:\n        stereo: true\n        format: mp3\n    - connect:\n        to: sip:%{call.to}@your-unique-SIP-domain-from-Livekit.com;transport=tcp\n```\n\nRecordings appear in the SignalWire Dashboard Logs tab.\n\n### Setup steps\n\n**1. Create the SWML in SignalWire** with your LiveKit SIP domain substituted in.\n\n**2. Assign the SWML to the DID** that receives inbound calls.\n\n**3. Configure LiveKit's inbound SIP trunk:**\n\n- Create an inbound SIP trunk in LiveKit.\n- Add your SignalWire phone number(s) in E.164 format.\n- Save.\n\n## LiveKit Outbound — LiveKit → PSTN\n\n**Flow:**\n\n1. LiveKit initiates a SIP call to a SignalWire SIP Address.\n2. SignalWire executes the SWML at that SIP Address.\n3. SWML `connect` bridges to PSTN.\n4. Callee sees the call from your SignalWire DID.\n\n### SWML\n\n```yaml\nversion: 1.0.0\nsections:\n  main:\n    - connect:\n        answer_on_bridge: true\n        from: \"+1XXXXXXXXXX\"\n        to: \"%{call.to.replace(/^sip:/i, '').replace(/@.*/, '')}\"\n```\n\nThe `to` expression strips `sip:` prefix and `@domain` suffix. `answer_on_bridge: true` is required.\n\n### Optional — outbound with recording (sub-SWML via `confirm`)\n\n```yaml\n# record-helper.yaml\nversion: 1.0.0\nsections:\n  main:\n    - record_call: { stereo: true, format: mp3 }\n\n# main.yaml (assigned to the SIP Address)\nversion: 1.0.0\nsections:\n  main:\n    - connect:\n        answer_on_bridge: true\n        from: \"+1XXXXXXXXXX\"\n        to: \"%{call.to.replace(/^sip:/i, '').replace(/@.*/, '')}\"\n        confirm: https://your.api/record-helper.yaml\n```\n\n### Setup steps\n\n**1. Create the SWML in SignalWire** with your outbound caller ID.\n\n**2. Create a SIP Address in SignalWire:**\n\n- SIP → SIP Addresses → New.\n- Assign the SWML script.\n- Note the SIP domain (e.g., `your-space.sip.signalwire.com`).\n\n**3. Request the app password from SignalWire Support.** Not available in the portal UI. Required for LiveKit's outbound trunk auth.\n\n**4. Create the LiveKit outbound SIP trunk:**\n\n| Field | Value |\n|---|---|\n| Address | Your SignalWire SIP domain |\n| Numbers | Your SignalWire DID(s) |\n| Auth Username | Your SignalWire phone number (E.164) |\n| Auth Password | App password from Support |\n| Transport | `3` (TLS — required) |\n\n**5. Initiate calls** using LiveKit's API — create a SIP Participant, which triggers an outbound call through the trunk.\n\n## SignalWire IPs\n\nWhen registering SignalWire as a BYO inbound trunk in LiveKit, all SignalWire SIP IPs should be allowed:\n\n```bash\ndig sip.signalwire.com\n```\n\nLiveKit's inbound trunk lets you list multiple addresses. Re-check periodically.\n\n## Troubleshooting\n\n### LiveKit inbound calls failing\n\n- [ ] LiveKit SIP domain is correct in the SWML.\n- [ ] `;transport=tcp` appended to the SIP URI.\n- [ ] SWML assigned to the SignalWire DID (not a SIP Address).\n- [ ] SignalWire IPs allowed in LiveKit inbound trunk.\n- [ ] SignalWire Logs tab shows SWML execution.\n\n### LiveKit outbound auth errors\n\n- [ ] App password obtained from SignalWire Support.\n- [ ] Auth username is the E.164 phone number, not a string.\n- [ ] SIP domain matches the SignalWire SIP Address exactly.\n- [ ] Transport set to TLS (`3`).\n- [ ] SWML assigned to the SIP Address (not a phone number).\n- [ ] `answer_on_bridge: true` in the SWML.\n\n## Anti-patterns\n\n- Omitting `;transport=tcp` on the LiveKit inbound SIP URI — silently fails.\n- Skipping TLS (transport `3`) on the LiveKit outbound trunk — auth fails.\n- Using an alphanumeric auth username instead of the E.164 number.\n- Generating the app password locally — must come from SignalWire Support.\n- Assigning outbound SWML to a phone number — must be on a SIP Address.\n\n## See also\n\n- [VAPI BYO SIP trunk](/topic/byo-sip-trunk-vapi)\n- [SWML connect verb](/topic/swml-connect-verb)\n- [SWML overview](/topic/swml-overview)\n","html":"<h1>LiveKit BYO SIP Trunk on SignalWire</h1>\n<p>LiveKit's Agents framework runs the AI conversation; SignalWire handles PSTN bridging. Same shape as the <a href=\"/topic/byo-sip-trunk-vapi\">VAPI BYO setup</a> — the differences are transport (LiveKit inbound needs <code>;transport=tcp</code>, outbound trunk needs TLS) and the destination SIP domain.</p>\n<h2>LiveKit Inbound — PSTN → LiveKit</h2>\n<p><strong>Flow:</strong></p>\n<ol>\n<li>Caller dials a SignalWire DID.</li>\n<li>SignalWire executes the SWML.</li>\n<li><code>connect</code> bridges to LiveKit's SIP domain.</li>\n<li>LiveKit's agent pipeline handles the call.</li>\n</ol>\n<h3>SWML</h3>\n<pre><code class=\"language-yaml\">version: 1.0.0\nsections:\n  main:\n    - connect:\n        to: sip:%{call.to}@your-unique-SIP-domain-from-Livekit.com;transport=tcp\n</code></pre>\n<p><code>;transport=tcp</code> is required for LiveKit inbound. Without it, calls may fail or behave unpredictably.</p>\n<h3>Optional — record the call</h3>\n<pre><code class=\"language-yaml\">version: 1.0.0\nsections:\n  main:\n    - record_call:\n        stereo: true\n        format: mp3\n    - connect:\n        to: sip:%{call.to}@your-unique-SIP-domain-from-Livekit.com;transport=tcp\n</code></pre>\n<p>Recordings appear in the SignalWire Dashboard Logs tab.</p>\n<h3>Setup steps</h3>\n<p><strong>1. Create the SWML in SignalWire</strong> with your LiveKit SIP domain substituted in.</p>\n<p><strong>2. Assign the SWML to the DID</strong> that receives inbound calls.</p>\n<p><strong>3. Configure LiveKit's inbound SIP trunk:</strong></p>\n<ul>\n<li>Create an inbound SIP trunk in LiveKit.</li>\n<li>Add your SignalWire phone number(s) in E.164 format.</li>\n<li>Save.</li>\n</ul>\n<h2>LiveKit Outbound — LiveKit → PSTN</h2>\n<p><strong>Flow:</strong></p>\n<ol>\n<li>LiveKit initiates a SIP call to a SignalWire SIP Address.</li>\n<li>SignalWire executes the SWML at that SIP Address.</li>\n<li>SWML <code>connect</code> bridges to PSTN.</li>\n<li>Callee sees the call from your SignalWire DID.</li>\n</ol>\n<h3>SWML</h3>\n<pre><code class=\"language-yaml\">version: 1.0.0\nsections:\n  main:\n    - connect:\n        answer_on_bridge: true\n        from: \"+1XXXXXXXXXX\"\n        to: \"%{call.to.replace(/^sip:/i, '').replace(/@.*/, '')}\"\n</code></pre>\n<p>The <code>to</code> expression strips <code>sip:</code> prefix and <code>@domain</code> suffix. <code>answer_on_bridge: true</code> is required.</p>\n<h3>Optional — outbound with recording (sub-SWML via <code>confirm</code>)</h3>\n<pre><code class=\"language-yaml\"># record-helper.yaml\nversion: 1.0.0\nsections:\n  main:\n    - record_call: { stereo: true, format: mp3 }\n\n# main.yaml (assigned to the SIP Address)\nversion: 1.0.0\nsections:\n  main:\n    - connect:\n        answer_on_bridge: true\n        from: \"+1XXXXXXXXXX\"\n        to: \"%{call.to.replace(/^sip:/i, '').replace(/@.*/, '')}\"\n        confirm: https://your.api/record-helper.yaml\n</code></pre>\n<h3>Setup steps</h3>\n<p><strong>1. Create the SWML in SignalWire</strong> with your outbound caller ID.</p>\n<p><strong>2. Create a SIP Address in SignalWire:</strong></p>\n<ul>\n<li>SIP → SIP Addresses → New.</li>\n<li>Assign the SWML script.</li>\n<li>Note the SIP domain (e.g., <code>your-space.sip.signalwire.com</code>).</li>\n</ul>\n<p><strong>3. Request the app password from SignalWire Support.</strong> Not available in the portal UI. Required for LiveKit's outbound trunk auth.</p>\n<p><strong>4. Create the LiveKit outbound SIP trunk:</strong></p>\n<p>| Field | Value |\n|---|---|\n| Address | Your SignalWire SIP domain |\n| Numbers | Your SignalWire DID(s) |\n| Auth Username | Your SignalWire phone number (E.164) |\n| Auth Password | App password from Support |\n| Transport | <code>3</code> (TLS — required) |</p>\n<p><strong>5. Initiate calls</strong> using LiveKit's API — create a SIP Participant, which triggers an outbound call through the trunk.</p>\n<h2>SignalWire IPs</h2>\n<p>When registering SignalWire as a BYO inbound trunk in LiveKit, all SignalWire SIP IPs should be allowed:</p>\n<pre><code class=\"language-bash\">dig sip.signalwire.com\n</code></pre>\n<p>LiveKit's inbound trunk lets you list multiple addresses. Re-check periodically.</p>\n<h2>Troubleshooting</h2>\n<h3>LiveKit inbound calls failing</h3>\n<ul>\n<li>[ ] LiveKit SIP domain is correct in the SWML.</li>\n<li>[ ] <code>;transport=tcp</code> appended to the SIP URI.</li>\n<li>[ ] SWML assigned to the SignalWire DID (not a SIP Address).</li>\n<li>[ ] SignalWire IPs allowed in LiveKit inbound trunk.</li>\n<li>[ ] SignalWire Logs tab shows SWML execution.</li>\n</ul>\n<h3>LiveKit outbound auth errors</h3>\n<ul>\n<li>[ ] App password obtained from SignalWire Support.</li>\n<li>[ ] Auth username is the E.164 phone number, not a string.</li>\n<li>[ ] SIP domain matches the SignalWire SIP Address exactly.</li>\n<li>[ ] Transport set to TLS (<code>3</code>).</li>\n<li>[ ] SWML assigned to the SIP Address (not a phone number).</li>\n<li>[ ] <code>answer_on_bridge: true</code> in the SWML.</li>\n</ul>\n<h2>Anti-patterns</h2>\n<ul>\n<li>Omitting <code>;transport=tcp</code> on the LiveKit inbound SIP URI — silently fails.</li>\n<li>Skipping TLS (transport <code>3</code>) on the LiveKit outbound trunk — auth fails.</li>\n<li>Using an alphanumeric auth username instead of the E.164 number.</li>\n<li>Generating the app password locally — must come from SignalWire Support.</li>\n<li>Assigning outbound SWML to a phone number — must be on a SIP Address.</li>\n</ul>\n<h2>See also</h2>\n<ul>\n<li><a href=\"/topic/byo-sip-trunk-vapi\">VAPI BYO SIP trunk</a></li>\n<li><a href=\"/topic/swml-connect-verb\">SWML connect verb</a></li>\n<li><a href=\"/topic/swml-overview\">SWML overview</a></li>\n</ul>\n"}