{"slug":"byo-sip-trunk-vapi","title":"VAPI BYO SIP Trunk — Inbound and Outbound with SignalWire","tags":["signalwire","vapi","byo-trunk","sip","voice-ai","integration"],"agent_summary":"Bridge SignalWire PSTN to VAPI AI agents via BYO SIP. Inbound: PSTN→SignalWire DID→SWML connect→VAPI. Outbound: VAPI→SignalWire SIP Address→PSTN. Includes exact SWML, credential setup, IP allowlist, and app-password requirement.","trigger_phrases":["VAPI SignalWire BYO SIP","VAPI inbound trunk SignalWire","VAPI outbound trunk SignalWire","credential ID sip.vapi.ai","VAPI app password SignalWire"],"runnable":true,"markdown":"\n# VAPI BYO SIP Trunk on SignalWire\n\nSignalWire is the PSTN/SIP backbone. VAPI plugs in as a BYO SIP trunk and runs the AI conversation. SignalWire bridges via the SWML `connect` verb. Every integration requires a SWML script — assigned to a phone number for inbound or to a SIP Address for outbound.\n\n## VAPI Inbound — PSTN → AI\n\n**Flow:**\n\n1. Caller dials a SignalWire number.\n2. SignalWire executes the assigned SWML.\n3. SWML `connect` bridges to VAPI's SIP endpoint using your VAPI credential ID.\n4. VAPI routes the call to the assigned AI assistant.\n\n### SWML\n\n```yaml\nversion: 1.0.0\nsections:\n  main:\n    - connect:\n        to: sip:%{call.to}@YOUR_CREDENTIAL_ID.sip.vapi.ai\n```\n\n`YOUR_CREDENTIAL_ID` is the `credentialId` returned when you create the BYO trunk in VAPI.\n\n### Setup steps\n\n**1. Create the BYO inbound trunk in VAPI.**\n\nPOST to the VAPI credentials/trunks API:\n\n```json\n{\n  \"provider\": \"byo-sip-trunk\",\n  \"name\": \"SignalWire\",\n  \"sipUri\": \"sip.signalwire.com\",\n  \"inboundEnabled\": true\n}\n```\n\nSave the returned `credentialId`. Register all 15 SignalWire SIP IPs in the trunk's IP allowlist (resolve with `dig sip.signalwire.com` — IPs can change, so re-check periodically).\n\n**2. Register the phone number in VAPI.**\n\n```json\n{\n  \"provider\": \"byo-phone-number\",\n  \"number\": \"+1XXXXXXXXXX\",\n  \"credentialId\": \"YOUR_CREDENTIAL_ID\"\n}\n```\n\n**3. Assign the AI assistant** to the registered number in the VAPI dashboard.\n\n**4. Create the SWML script in SignalWire** using the YAML above with your credential ID substituted in.\n\n**5. Assign the SWML script to the inbound DID** in the SignalWire dashboard.\n\n### Test\n\nCall the SignalWire DID from any phone. Call routes to VAPI and the assigned AI answers.\n\n## VAPI Outbound — AI → PSTN\n\n**Flow:**\n\n1. VAPI initiates an outbound call via the BYO SIP trunk.\n2. SignalWire receives the SIP call at the assigned SIP Address.\n3. SWML strips the SIP URI to a clean E.164 destination.\n4. `connect` bridges to PSTN.\n5. 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 both the `sip:` prefix and the `@domain` suffix, leaving raw E.164. `answer_on_bridge: true` is required — otherwise the callee hears dead air.\n\n### Setup steps\n\n**1. Create the SWML script 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 to it.\n- Note the unique SIP domain (e.g., `your-space.sip.signalwire.com`).\n\n**3. Get the app password from SignalWire Support.** This is **not** available in the portal UI. You must open a Support ticket and request the app password for your SIP Address. Without it, the outbound trunk fails authentication.\n\n**4. Create the BYO outbound trunk in VAPI:**\n\n```json\n{\n  \"provider\": \"byo-sip-trunk\",\n  \"name\": \"SignalWire Outbound\",\n  \"sipUri\": \"YOUR_SIGNALWIRE_SIP_DOMAIN\",\n  \"username\": \"+1XXXXXXXXXX\",\n  \"password\": \"YOUR_APP_PASSWORD_FROM_SUPPORT\",\n  \"outboundEnabled\": true\n}\n```\n\n- `sipUri` = SIP domain from step 2.\n- `username` = SignalWire phone number in E.164 format (**not** an alphanumeric username).\n- `password` = app password from Support.\n- Save the returned `credentialId`.\n\n**5. Register the outbound phone number in VAPI:**\n\n```json\n{\n  \"provider\": \"byo-phone-number\",\n  \"number\": \"+1XXXXXXXXXX\",\n  \"credentialId\": \"YOUR_CREDENTIAL_ID\"\n}\n```\n\n**6. Assign the AI assistant** to that number in VAPI.\n\n**7. Trigger outbound calls via the VAPI API** specifying the assistant and destination.\n\n## SignalWire IPs for the inbound allowlist\n\nVAPI's inbound trunk requires the SignalWire SIP IP allowlist to be complete. Resolve:\n\n```bash\ndig sip.signalwire.com\nnslookup sip.signalwire.com\n```\n\nIPs can change. Re-check periodically. Missing IPs = silent dropped inbound calls.\n\n## Troubleshooting\n\n### Inbound not reaching VAPI\n\n- [ ] All SignalWire IPs registered in the VAPI trunk.\n- [ ] `credentialId` in SWML matches the trunk credential.\n- [ ] SWML script assigned to the right DID.\n- [ ] Phone number registered in VAPI in E.164 format.\n- [ ] `inboundEnabled: true` on the VAPI trunk.\n- [ ] SignalWire Dashboard → Logs shows SWML executing.\n\n### Outbound auth errors\n\n- [ ] App password obtained from Support (not guessed).\n- [ ] Auth `username` is the E.164 phone number, not an alphanumeric string.\n- [ ] `sipUri` matches the SignalWire SIP Address domain exactly.\n- [ ] `outboundEnabled: true` on the VAPI trunk.\n- [ ] SWML script is assigned to the SIP Address (not a phone number).\n\n### Wrong destination dialed\n\n- [ ] SWML `to` expression: `%{call.to.replace(/^sip:/i, '').replace(/@.*/, '')}` — both replaces required.\n- [ ] Resulting value is E.164 (e.g., `+14155551234`).\n- [ ] VAPI outbound logs show clean dial string.\n\n## Anti-patterns\n\n- Generating or guessing the app password — it only comes from SignalWire Support.\n- Assigning outbound SWML to a phone number — must be on a SIP Address.\n- Assigning inbound SWML to a SIP Address — must be on a phone number.\n- Only registering some SignalWire IPs in the VAPI inbound trunk — any missing IP drops calls.\n- Auth username as an alphanumeric string instead of E.164 number — VAPI outbound auth will fail.\n- Missing `answer_on_bridge: true` on outbound SWML — callee hears dead air until pickup.\n\n## See also\n\n- [LiveKit BYO SIP trunk](/topic/byo-sip-trunk-livekit)\n- [SWML connect verb](/topic/swml-connect-verb)\n- [SWML overview](/topic/swml-overview)\n","html":"<h1>VAPI BYO SIP Trunk on SignalWire</h1>\n<p>SignalWire is the PSTN/SIP backbone. VAPI plugs in as a BYO SIP trunk and runs the AI conversation. SignalWire bridges via the SWML <code>connect</code> verb. Every integration requires a SWML script — assigned to a phone number for inbound or to a SIP Address for outbound.</p>\n<h2>VAPI Inbound — PSTN → AI</h2>\n<p><strong>Flow:</strong></p>\n<ol>\n<li>Caller dials a SignalWire number.</li>\n<li>SignalWire executes the assigned SWML.</li>\n<li>SWML <code>connect</code> bridges to VAPI's SIP endpoint using your VAPI credential ID.</li>\n<li>VAPI routes the call to the assigned AI assistant.</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_CREDENTIAL_ID.sip.vapi.ai\n</code></pre>\n<p><code>YOUR_CREDENTIAL_ID</code> is the <code>credentialId</code> returned when you create the BYO trunk in VAPI.</p>\n<h3>Setup steps</h3>\n<p><strong>1. Create the BYO inbound trunk in VAPI.</strong></p>\n<p>POST to the VAPI credentials/trunks API:</p>\n<pre><code class=\"language-json\">{\n  \"provider\": \"byo-sip-trunk\",\n  \"name\": \"SignalWire\",\n  \"sipUri\": \"sip.signalwire.com\",\n  \"inboundEnabled\": true\n}\n</code></pre>\n<p>Save the returned <code>credentialId</code>. Register all 15 SignalWire SIP IPs in the trunk's IP allowlist (resolve with <code>dig sip.signalwire.com</code> — IPs can change, so re-check periodically).</p>\n<p><strong>2. Register the phone number in VAPI.</strong></p>\n<pre><code class=\"language-json\">{\n  \"provider\": \"byo-phone-number\",\n  \"number\": \"+1XXXXXXXXXX\",\n  \"credentialId\": \"YOUR_CREDENTIAL_ID\"\n}\n</code></pre>\n<p><strong>3. Assign the AI assistant</strong> to the registered number in the VAPI dashboard.</p>\n<p><strong>4. Create the SWML script in SignalWire</strong> using the YAML above with your credential ID substituted in.</p>\n<p><strong>5. Assign the SWML script to the inbound DID</strong> in the SignalWire dashboard.</p>\n<h3>Test</h3>\n<p>Call the SignalWire DID from any phone. Call routes to VAPI and the assigned AI answers.</p>\n<h2>VAPI Outbound — AI → PSTN</h2>\n<p><strong>Flow:</strong></p>\n<ol>\n<li>VAPI initiates an outbound call via the BYO SIP trunk.</li>\n<li>SignalWire receives the SIP call at the assigned SIP Address.</li>\n<li>SWML strips the SIP URI to a clean E.164 destination.</li>\n<li><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 both the <code>sip:</code> prefix and the <code>@domain</code> suffix, leaving raw E.164. <code>answer_on_bridge: true</code> is required — otherwise the callee hears dead air.</p>\n<h3>Setup steps</h3>\n<p><strong>1. Create the SWML script 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 to it.</li>\n<li>Note the unique SIP domain (e.g., <code>your-space.sip.signalwire.com</code>).</li>\n</ul>\n<p><strong>3. Get the app password from SignalWire Support.</strong> This is <strong>not</strong> available in the portal UI. You must open a Support ticket and request the app password for your SIP Address. Without it, the outbound trunk fails authentication.</p>\n<p><strong>4. Create the BYO outbound trunk in VAPI:</strong></p>\n<pre><code class=\"language-json\">{\n  \"provider\": \"byo-sip-trunk\",\n  \"name\": \"SignalWire Outbound\",\n  \"sipUri\": \"YOUR_SIGNALWIRE_SIP_DOMAIN\",\n  \"username\": \"+1XXXXXXXXXX\",\n  \"password\": \"YOUR_APP_PASSWORD_FROM_SUPPORT\",\n  \"outboundEnabled\": true\n}\n</code></pre>\n<ul>\n<li><code>sipUri</code> = SIP domain from step 2.</li>\n<li><code>username</code> = SignalWire phone number in E.164 format (<strong>not</strong> an alphanumeric username).</li>\n<li><code>password</code> = app password from Support.</li>\n<li>Save the returned <code>credentialId</code>.</li>\n</ul>\n<p><strong>5. Register the outbound phone number in VAPI:</strong></p>\n<pre><code class=\"language-json\">{\n  \"provider\": \"byo-phone-number\",\n  \"number\": \"+1XXXXXXXXXX\",\n  \"credentialId\": \"YOUR_CREDENTIAL_ID\"\n}\n</code></pre>\n<p><strong>6. Assign the AI assistant</strong> to that number in VAPI.</p>\n<p><strong>7. Trigger outbound calls via the VAPI API</strong> specifying the assistant and destination.</p>\n<h2>SignalWire IPs for the inbound allowlist</h2>\n<p>VAPI's inbound trunk requires the SignalWire SIP IP allowlist to be complete. Resolve:</p>\n<pre><code class=\"language-bash\">dig sip.signalwire.com\nnslookup sip.signalwire.com\n</code></pre>\n<p>IPs can change. Re-check periodically. Missing IPs = silent dropped inbound calls.</p>\n<h2>Troubleshooting</h2>\n<h3>Inbound not reaching VAPI</h3>\n<ul>\n<li>[ ] All SignalWire IPs registered in the VAPI trunk.</li>\n<li>[ ] <code>credentialId</code> in SWML matches the trunk credential.</li>\n<li>[ ] SWML script assigned to the right DID.</li>\n<li>[ ] Phone number registered in VAPI in E.164 format.</li>\n<li>[ ] <code>inboundEnabled: true</code> on the VAPI trunk.</li>\n<li>[ ] SignalWire Dashboard → Logs shows SWML executing.</li>\n</ul>\n<h3>Outbound auth errors</h3>\n<ul>\n<li>[ ] App password obtained from Support (not guessed).</li>\n<li>[ ] Auth <code>username</code> is the E.164 phone number, not an alphanumeric string.</li>\n<li>[ ] <code>sipUri</code> matches the SignalWire SIP Address domain exactly.</li>\n<li>[ ] <code>outboundEnabled: true</code> on the VAPI trunk.</li>\n<li>[ ] SWML script is assigned to the SIP Address (not a phone number).</li>\n</ul>\n<h3>Wrong destination dialed</h3>\n<ul>\n<li>[ ] SWML <code>to</code> expression: <code>%{call.to.replace(/^sip:/i, '').replace(/@.*/, '')}</code> — both replaces required.</li>\n<li>[ ] Resulting value is E.164 (e.g., <code>+14155551234</code>).</li>\n<li>[ ] VAPI outbound logs show clean dial string.</li>\n</ul>\n<h2>Anti-patterns</h2>\n<ul>\n<li>Generating or guessing the app password — it only comes from SignalWire Support.</li>\n<li>Assigning outbound SWML to a phone number — must be on a SIP Address.</li>\n<li>Assigning inbound SWML to a SIP Address — must be on a phone number.</li>\n<li>Only registering some SignalWire IPs in the VAPI inbound trunk — any missing IP drops calls.</li>\n<li>Auth username as an alphanumeric string instead of E.164 number — VAPI outbound auth will fail.</li>\n<li>Missing <code>answer_on_bridge: true</code> on outbound SWML — callee hears dead air until pickup.</li>\n</ul>\n<h2>See also</h2>\n<ul>\n<li><a href=\"/topic/byo-sip-trunk-livekit\">LiveKit 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"}