From 3785dd52c225557d46711a89f102b8d48e9813de Mon Sep 17 00:00:00 2001 From: Devon-White Date: Fri, 4 Sep 2026 16:59:34 -0400 Subject: [PATCH 001/103] docs: add outbound calling guide --- fern/docs.yml | 6 + .../pages/ai/guides/Integrations/vapi.mdx | 305 ++++++++++++++++ .../Integrations/vapi/inbound-calls.mdx | 194 ---------- .../Integrations/vapi/outbound-calls.mdx | 212 ----------- .../pages/calling/voice/outbound-calling.mdx | 343 ++++++++++++++++++ 5 files changed, 654 insertions(+), 406 deletions(-) create mode 100644 fern/products/platform/pages/ai/guides/Integrations/vapi.mdx delete mode 100644 fern/products/platform/pages/ai/guides/Integrations/vapi/inbound-calls.mdx delete mode 100644 fern/products/platform/pages/ai/guides/Integrations/vapi/outbound-calls.mdx create mode 100644 fern/products/platform/pages/calling/voice/outbound-calling.mdx diff --git a/fern/docs.yml b/fern/docs.yml index 48c3fd1571..55f4806cec 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -237,6 +237,12 @@ redirects: - source: /docs/platform/ai/prompt-engineering/where-to-apply destination: /docs/platform/ai/prompt-engineering + # The VAPI inbound and outbound guides were merged into one integration guide. + - source: /docs/platform/ai/vapi/inbound + destination: /docs/platform/ai/vapi + - source: /docs/platform/ai/vapi/outbound + destination: /docs/platform/ai/vapi + # SWML methods reorganized into calling/ and messaging/ subsections. # The old methods overview lived at /docs/swml/reference, which was # entirely calling-flavored — point it at the new calling overview. diff --git a/fern/products/platform/pages/ai/guides/Integrations/vapi.mdx b/fern/products/platform/pages/ai/guides/Integrations/vapi.mdx new file mode 100644 index 0000000000..6f52503a0b --- /dev/null +++ b/fern/products/platform/pages/ai/guides/Integrations/vapi.mdx @@ -0,0 +1,305 @@ +--- +title: VAPI integration +slug: /ai/vapi +description: Connect VAPI AI assistants to SignalWire phone numbers over SIP trunking, for inbound calls to an assistant and outbound calls from it. +max-toc-depth: 3 +--- + +[signup]: https://signalwire.com/signup +[vapi]: https://dashboard.vapi.ai/ +[vapi-sip-guide]: https://docs.vapi.ai/advanced/sip/sip-trunk +[vapi-test-guide]: https://docs.vapi.ai/advanced/sip/sip-trunk#test-your-sip-trunk +[sw-firewall-guide]: /docs/platform/allow-signalwire-ips-through-your-firewall +[resources]: https://my.signalwire.com?page=resources +[addresses-guide]: /docs/platform/addresses +[swml-guide]: /docs/swml +[swml-connect]: /docs/swml/reference/calling/connect +[outbound-calling]: /docs/platform/voice/outbound-calling + +VAPI assistants need phone numbers, and SignalWire supplies them over SIP trunking in both +directions. Callers dial your SignalWire number and reach the assistant, and the assistant places +calls that show your SignalWire number as caller ID. + +Each direction is its own SIP trunk in VAPI and its own SWML script in SignalWire. Set up the one +you need, or both. The VAPI side follows VAPI's own [SIP trunk setup guide][vapi-sip-guide] with +SignalWire-specific values. + +## What you'll need + +- A SignalWire account with at least one phone number ([sign up here][signup]) +- A [VAPI][vapi] account with API access +- Your VAPI private API key (found in your VAPI dashboard) +- For outbound calls, a SIP domain app password from SignalWire Support (requested below) + +## Inbound calls to your assistant + +VAPI has to accept calls from SignalWire's network, and SignalWire has to know which assistant a +number belongs to. You create a trunk in VAPI that trusts SignalWire's IP addresses, register your +number with it, and then route the number to VAPI with a SWML script. + +### Create the inbound SIP trunk in VAPI + +Run the API call below to create the trunk, making sure to replace `YOUR_VAPI_PRIVATE_KEY` with your actual API key. + +Save the `id` from the response - you'll need this credential ID for the next step. + + +These IP addresses are current as of this guide's publication, but SignalWire IPs can change and should be programmatically monitored to avoid any impact on calls. You can gather SignalWire's latest IPs by performing a DIG or nslookup of `sip.signalwire.com`. + +See our guide on [allowing SignalWire IPs through firewalls][sw-firewall-guide] for more details. + + +```bash +curl -X POST "https://api.vapi.ai/credential" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_VAPI_PRIVATE_KEY" \ + -d '{ + "provider": "byo-sip-trunk", + "name": "SignalWire Inbound Trunk", + "gateways": [ + { "ip": "170.64.128.96", "inboundEnabled": true }, + { "ip": "198.13.56.186", "inboundEnabled": true }, + { "ip": "104.248.176.184", "inboundEnabled": true }, + { "ip": "152.42.144.114", "inboundEnabled": true }, + { "ip": "104.248.150.114", "inboundEnabled": true }, + { "ip": "138.68.125.160", "inboundEnabled": true }, + { "ip": "159.65.244.171", "inboundEnabled": true }, + { "ip": "167.99.198.84", "inboundEnabled": true }, + { "ip": "13.245.35.235", "inboundEnabled": true }, + { "ip": "108.61.169.31", "inboundEnabled": true }, + { "ip": "137.184.4.155", "inboundEnabled": true }, + { "ip": "188.166.126.7", "inboundEnabled": true }, + { "ip": "139.59.34.94", "inboundEnabled": true }, + { "ip": "165.232.186.228", "inboundEnabled": true }, + { "ip": "157.175.131.128", "inboundEnabled": true } + ] + }' +``` + +### Register your SignalWire phone number + +Now register your SignalWire phone number with VAPI using the credential ID from above: + +```bash +curl -X POST "https://api.vapi.ai/phone-number" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_VAPI_PRIVATE_KEY" \ + -d '{ + "provider": "byo-phone-number", + "name": "SignalWire Inbound Number", + "number": "+15551234567", + "numberE164CheckEnabled": true, + "credentialId": "YOUR_CREDENTIAL_ID" + }' +``` + +Make sure to replace: +- `YOUR_VAPI_PRIVATE_KEY` with your actual API key +- `+15551234567` with your SignalWire phone number in E.164 format (including the + and country code) +- `YOUR_CREDENTIAL_ID` with the credential ID from the previous step + +### Assign your assistant + +In your VAPI dashboard, find the phone number you just registered and assign it to one of your AI +assistants. Configure the inbound settings only. If you also set up outbound calls below, you +register the same number a second time against the outbound trunk. + +### Create the routing script in SignalWire + +In your SignalWire dashboard, go to [**Resources**][resources] → **Add** → **Script** → **SWML Script**. This script will handle all incoming calls by immediately connecting them to VAPI: + +```yaml +version: 1.0.0 +sections: + main: + - connect: + to: 'sip:%{call.to}@YOUR_CREDENTIAL_ID.sip.vapi.ai' +``` + +Replace `YOUR_CREDENTIAL_ID` with the credential ID from the VAPI trunk setup above. The `%{call.to}` variable ensures VAPI receives the original dialed number, which helps with call routing and analytics. + + +This SWML script uses the [`connect` method][swml-connect] to bridge the incoming call directly to VAPI's SIP endpoint. +The call happens in real time with no delays. Learn more about SWML in our [complete guide][swml-guide]. + + +### Connect the script to your phone number + +Go to **Phone Numbers** in your SignalWire dashboard, find your number, and click **Edit Settings**. Under the voice settings, assign your new SWML script to handle incoming calls. + +### Test inbound calls + +Call your SignalWire phone number. Your VAPI assistant answers within a few seconds. If it doesn't, +the [troubleshooting section](#troubleshoot-the-vapi-integration) below covers the common causes. + +## Outbound calls from your assistant + +For outbound calls the roles reverse. SignalWire publishes a SIP address that VAPI dials, and a SWML +script bridges each call onto the public telephone network with your SignalWire number as caller +ID. VAPI authenticates to that SIP address with a password issued by SignalWire Support. + +### Create the outbound routing script + +In your SignalWire dashboard, go to [**Resources**][resources] → **Add** → **Script** → **SWML Script**. This script will handle outbound calls by connecting them through the PSTN: + +```yaml +version: 1.0.0 +sections: + main: + - connect: + answer_on_bridge: true + from: +1A-Number-From-Your-Space-here + to: '%{call.to.replace(/^sip:/i, '''').replace(/@.*/, '''')}' +``` + +Replace `"+1A-Number-From-Your-Space-here"` with an actual phone number from your SignalWire account. +This will be the caller ID shown to people who receive calls from your VAPI assistant. + + +This SWML script uses the [`connect` method][swml-connect] with `answer_on_bridge: true` to ensure calls connect properly. +The `to` field uses a regular expression to extract the phone number from VAPI's SIP format and route it through the PSTN. + + +### Add a SIP address to your script + +After saving your SWML script, you'll need to create a SIP address that VAPI can connect to: + +1. In your saved SWML script, navigate to the **Addresses & Phone Numbers** section +2. Click **Add** and select **SIP Address** +3. Configure the SIP address settings and save + +After configuration, note down your unique SIP domain app. It will look something like: `test-space-vapi.dapp.signalwire.com` + +Learn more about addresses in our [Call Fabric Addresses documentation][addresses-guide]. + + +To complete this setup, you must contact SignalWire Support to generate a password for your SIP domain app. VAPI needs this password for authentication. + +Contact support by: +- Clicking the "Help?" button in your SignalWire Space +- Emailing support@signalwire.com + +Provide your SIP domain app and let them know you need a password for VAPI integration. + + +### Create the outbound SIP trunk in VAPI + +Run this API call to create the outbound trunk, making sure to replace the placeholder values with your actual configuration: + +```bash +curl -X POST "https://api.vapi.ai/credential" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_VAPI_PRIVATE_KEY" \ + -d '{ + "provider": "byo-sip-trunk", + "name": "SignalWire Outbound Trunk", + "gateways": [{ + "ip": "YOUR_SIGNALWIRE_SIP_DOMAIN", + "inboundEnabled": false + }], + "outboundLeadingPlusEnabled": true, + "outboundAuthenticationPlan": { + "authUsername": "YOUR_SIGNALWIRE_PHONE_NUMBER", + "authPassword": "YOUR_SIGNALWIRE_PASSWORD" + } + }' +``` + +Make sure to replace: +- `YOUR_VAPI_PRIVATE_KEY` - Your VAPI API key +- `YOUR_SIGNALWIRE_SIP_DOMAIN` - Your SIP domain app from the previous step (e.g., `test-space-vapi.dapp.signalwire.com`) +- `YOUR_SIGNALWIRE_PHONE_NUMBER` - Your SignalWire phone number in E.164 format (e.g., `+15551234567`) +- `YOUR_SIGNALWIRE_PASSWORD` - The password provided by SignalWire Support + +Save the `id` from the response - you'll need this credential ID for the next step. + +### Register your SignalWire phone number for outbound + +Register your SignalWire phone number with VAPI for outbound calling using the credential ID from above: + +```bash +curl -X POST "https://api.vapi.ai/phone-number" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_VAPI_PRIVATE_KEY" \ + -d '{ + "provider": "byo-phone-number", + "name": "SignalWire Outbound Number", + "number": "+15551234567", + "numberE164CheckEnabled": true, + "credentialId": "YOUR_CREDENTIAL_ID" + }' +``` + +Replace: +- `YOUR_VAPI_PRIVATE_KEY` - Your VAPI API key +- `+15551234567` - Your actual SignalWire phone number in E.164 format +- `YOUR_CREDENTIAL_ID` - The credential ID from the trunk creation step + +### Assign your assistant for outbound calls + +In your VAPI dashboard, find the phone number you just registered and assign it to one of your AI assistants. Configure the outbound settings to specify which assistant should handle outbound calls. + + +If you've already set up inbound calling with the same number, VAPI will indicate this is a duplicate number. This is completely normal and expected - you're using the same phone number for both inbound and outbound calling with different configurations. + + +### Test outbound calls + +Your VAPI assistant can now make outbound calls through SignalWire. The calls will show your SignalWire phone number as the caller ID. + +To test your setup, refer to VAPI's official documentation on [testing your SIP trunk][vapi-test-guide] for specific instructions on initiating outbound calls. + +## Troubleshoot the VAPI integration + +You can monitor call logs in both platforms. SignalWire shows the call routing, and VAPI shows the +assistant interaction details. + +### Inbound calls aren't reaching VAPI + +Check that your SWML script has the correct credential ID and is assigned to your phone number. +Also, verify that your phone number is set to accept voice calls. Phone numbers must be in E.164 +format (`+1234567890`) with no spaces or special characters. + +### VAPI can't connect on inbound calls + +Make sure all SignalWire IP addresses are in your VAPI inbound trunk. If SignalWire has added new +IP addresses since you created the trunk, you'll need to update it. + +### Outbound calls from VAPI fail to authenticate + +Double-check that you're using the correct SIP domain app password from SignalWire Support. The +authentication username should be your phone number in E.164 format. + +### Outbound calls from VAPI don't connect + +Verify your outbound SWML script has the correct caller ID number and that it's a valid number from +your SignalWire account. Ensure your SIP domain app is properly configured in SignalWire and that +VAPI can resolve the domain name. + +## Next steps + + + + Place outbound calls from SignalWire directly, including calls handled by a SignalWire AI agent + + + + Add IVR menus, call screening, or business hours logic before connecting to VAPI + + + + Every option on the method both routing scripts use + + diff --git a/fern/products/platform/pages/ai/guides/Integrations/vapi/inbound-calls.mdx b/fern/products/platform/pages/ai/guides/Integrations/vapi/inbound-calls.mdx deleted file mode 100644 index 7752d50f83..0000000000 --- a/fern/products/platform/pages/ai/guides/Integrations/vapi/inbound-calls.mdx +++ /dev/null @@ -1,194 +0,0 @@ ---- -id: 85a7837b-5b92-4673-afe5-192995b0fbfb -title: VAPI inbound calling -subtitle: Route incoming calls to your VAPI AI assistants using SignalWire's phone network -slug: /ai/vapi/inbound -description: Route incoming calls from SignalWire phone numbers to VAPI AI assistants using SIP trunking ---- - - -[signup]: https://signalwire.com/signup -[vapi]: https://dashboard.vapi.ai/ -[vapi-sip-guide]: https://docs.vapi.ai/advanced/sip/sip-trunk -[sw-firewall-guide]: /docs/platform/allow-signalwire-ips-through-your-firewall -[resources]: https://my.signalwire.com?page=resources -[swml-guide]: /docs/swml/ - -VAPI provides powerful AI voice assistants, but these assistants need phone numbers to receive calls. SignalWire's phone network can route incoming calls directly to your VAPI assistants using SIP trunking. This guide shows you how to connect them. - -When you're finished, callers will dial your SignalWire phone number and immediately connect to your VAPI AI assistant - no additional routing or servers needed. - -## Setup overview - - - -### Configure VAPI for SignalWire - -Set up a SIP trunk with SignalWire's IP addresses and register your phone number. - -### Create call routing in SignalWire - -Build a SWML script that forwards incoming calls to your VAPI assistant. - -### Connect and test - -Assign the routing script to your phone number and verify calls reach your AI assistant. - - - -## What you'll need - -- A SignalWire account with at least one phone number ([sign up here][signup]) -- A [VAPI][vapi] account with API access -- Your VAPI private API key (found in your VAPI dashboard) - -## Setting up VAPI to receive SignalWire calls - -VAPI needs to know that calls will be coming from SignalWire's network. We'll create a SIP trunk that includes all of SignalWire's IP addresses, then register your phone number with that trunk. - -This follows VAPI's official [SIP trunk setup guide][vapi-sip-guide], but with SignalWire-specific configuration. - -### Create the SIP trunk - -Run the API call below to create the trunk, making sure to replace `YOUR_VAPI_PRIVATE_KEY` with your actual API key. - -Save the `id` from the response - you'll need this credential ID for the next step. - - -These IP addresses are current as of this guide's publication, but SignalWire IPs can change and should be programmatically monitored to avoid any impact on calls. You can gather SignalWire's latest IPs by performing a DIG or nslookup of `sip.signalwire.com`. - -See our guide on [allowing SignalWire IPs through firewalls][sw-firewall-guide] for more details. - - -```bash -curl -X POST "https://api.vapi.ai/credential" \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer YOUR_VAPI_PRIVATE_KEY" \ - -d '{ - "provider": "byo-sip-trunk", - "name": "SignalWire Inbound Trunk", - "gateways": [ - { "ip": "170.64.128.96", "inboundEnabled": true }, - { "ip": "198.13.56.186", "inboundEnabled": true }, - { "ip": "104.248.176.184", "inboundEnabled": true }, - { "ip": "152.42.144.114", "inboundEnabled": true }, - { "ip": "104.248.150.114", "inboundEnabled": true }, - { "ip": "138.68.125.160", "inboundEnabled": true }, - { "ip": "159.65.244.171", "inboundEnabled": true }, - { "ip": "167.99.198.84", "inboundEnabled": true }, - { "ip": "13.245.35.235", "inboundEnabled": true }, - { "ip": "108.61.169.31", "inboundEnabled": true }, - { "ip": "137.184.4.155", "inboundEnabled": true }, - { "ip": "188.166.126.7", "inboundEnabled": true }, - { "ip": "139.59.34.94", "inboundEnabled": true }, - { "ip": "165.232.186.228", "inboundEnabled": true }, - { "ip": "157.175.131.128", "inboundEnabled": true } - ] - }' -``` - - -### Register your SignalWire phone number - -Now register your SignalWire phone number with VAPI using the credential ID from above: - -```bash -curl -X POST "https://api.vapi.ai/phone-number" \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer YOUR_VAPI_PRIVATE_KEY" \ - -d '{ - "provider": "byo-phone-number", - "name": "SignalWire Inbound Number", - "number": "+15551234567", - "numberE164CheckEnabled": true, - "credentialId": "YOUR_CREDENTIAL_ID" - }' -``` - -Make sure to replace: -- `YOUR_VAPI_PRIVATE_KEY` with your actual API key -- `+15551234567` with your SignalWire phone number in E.164 format (including the + and country code) -- `YOUR_CREDENTIAL_ID` with the credential ID from the previous step - -### Assign your AI assistant - -In your VAPI dashboard, find the phone number you just registered and assign it to one of your AI assistants. You'll only need to configure the inbound settings - we're not using this number for outbound calls. - ---- - -## Configuring SignalWire to route calls - -Now we need to tell SignalWire where to send incoming calls. We'll create a simple SWML script that forwards calls to your VAPI assistant. - -### Create the routing script - -In your SignalWire dashboard, go to [**Resources**][resources] → **Add** → **Script** → **SWML Script**. This script will handle all incoming calls by immediately connecting them to VAPI: - -```yaml -version: 1.0.0 -sections: - main: - - connect: - to: 'sip:%{call.to}@YOUR_CREDENTIAL_ID.sip.vapi.ai' -``` - -Replace `YOUR_CREDENTIAL_ID` with the credential ID from the VAPI trunk setup above. The `%{call.to}` variable ensures VAPI receives the original dialed number, which helps with call routing and analytics. - - -This SWML script uses the `connect` method to bridge the incoming call directly to VAPI's SIP endpoint. -The call happens in real time with no delays. Learn more about SWML in our [complete guide][swml-guide]. - - -### Connect the script to your phone number - -Go to **Phone Numbers** in your SignalWire dashboard, find your number, and click **Edit Settings**. Under the voice settings, assign your new SWML script to handle incoming calls. - -That's it - your phone number is now connected to your VAPI assistant. - ---- - -## Testing and troubleshooting - -Call your SignalWire phone number and you should hear your VAPI assistant answer within a few seconds. - -If something isn't working, here are the most common issues: - -**Calls aren't reaching VAPI**: Check that your SWML script has the correct credential ID and is assigned to your phone number. Also, verify that your phone number is set to accept voice calls. - -**VAPI can't connect**: Make sure all SignalWire IP addresses are in your VAPI trunk. If SignalWire has added new IP addresses since you created the trunk, you'll need to update it. - -**Wrong number format errors**: Phone numbers must be in E.164 format (`+1234567890`) with no spaces or special characters. - -You can monitor call logs in both platforms - SignalWire shows the initial call routing, while VAPI shows the assistant interaction details. - ---- - -## What's next - -Now that you have basic inbound routing working, you might want to explore: - - - - Let your VAPI assistant make outbound calls through SignalWire - - - - Add IVR menus, call screening, or business hours logic before connecting to VAPI - - - - Best practices for creating effective VAPI assistants - - diff --git a/fern/products/platform/pages/ai/guides/Integrations/vapi/outbound-calls.mdx b/fern/products/platform/pages/ai/guides/Integrations/vapi/outbound-calls.mdx deleted file mode 100644 index bce5eb560c..0000000000 --- a/fern/products/platform/pages/ai/guides/Integrations/vapi/outbound-calls.mdx +++ /dev/null @@ -1,212 +0,0 @@ ---- -id: ee4080eb-4dce-457f-a2cc-b21537700230 -title: Outbound calling -subtitle: Let your VAPI AI assistants initiate calls through SignalWire's phone network -slug: /ai/vapi/outbound -description: Configure VAPI AI assistants to make outbound calls through SignalWire's infrastructure ---- - - -[signup]: https://signalwire.com/signup -[vapi]: https://dashboard.vapi.ai/ -[addresses-guide]: /docs/platform/addresses -[vapi-sip-guide]: https://docs.vapi.ai/advanced/sip/sip-trunk -[resources]: https://my.signalwire.com?page=resources -[vapi-test-guide]: https://docs.vapi.ai/advanced/sip/sip-trunk#test-your-sip-trunk - -While VAPI provides powerful AI voice assistants, they need a way to make outbound calls to your customers or contacts. -SignalWire's phone network can handle outbound calls from your VAPI assistants using SIP trunking. This guide shows you how to connect them for outbound calling. - -When you're finished, your VAPI assistants will be able to initiate calls using your SignalWire phone numbers, giving you complete control -over both inbound and outbound AI-powered communications. - -## Setup overview - - - -### Configure SignalWire for outbound routing - -Create a SWML script and SIP domain to handle outbound calls from VAPI. - -### Set up VAPI outbound trunk - -Configure VAPI with your SignalWire SIP domain and authentication details. - -### Connect and test - -Register your phone number with VAPI and verify outbound calling works. - - - -## What you'll need - -- A SignalWire account with at least one phone number ([sign up here][signup]) -- A [VAPI][vapi] account with API access -- Your VAPI private API key (found in your VAPI dashboard) -- Access to SignalWire support for SIP domain app password generation - -## Setting up SignalWire for outbound calls - -SignalWire needs to be configured to receive and route outbound call requests from VAPI. This involves creating a SWML script that handles -the call routing and setting up a SIP address. - -### Create the outbound routing script - -In your SignalWire dashboard, go to [**Resources**][resources] → **Add** → **Script** → **SWML Script**. This script will handle outbound calls by connecting them through the PSTN: - -```yaml -version: 1.0.0 -sections: - main: - - connect: - answer_on_bridge: true - from: +1A-Number-From-Your-Space-here - to: '%{call.to.replace(/^sip:/i, '''').replace(/@.*/, '''')}' -``` - -Replace `"+1A-Number-From-Your-Space-here"` with an actual phone number from your SignalWire account. -This will be the caller ID shown to people who receive calls from your VAPI assistant. - - -This SWML script uses the `connect` method with `answer_on_bridge: true` to ensure calls connect properly. -The `to` field uses a regular expression to extract the phone number from VAPI's SIP format and route it through the PSTN. - - -### Add a SIP address to your script - -After saving your SWML script, you'll need to create a SIP address that VAPI can connect to: - -1. In your saved SWML script, navigate to the **Addresses & Phone Numbers** section -2. Click **Add** and select **SIP Address** -3. Configure the SIP address settings and save - -After configuration, note down your unique SIP domain app. It will look something like: `test-space-vapi.dapp.signalwire.com` - -Learn more about addresses in our [Call Fabric Addresses documentation][addresses-guide]. - - -**Important**: To complete this setup, you must contact SignalWire Support to generate a password for your SIP domain app. VAPI needs this password for authentication. - -Contact support by: -- Clicking the "Help?" button in your SignalWire Space -- Emailing support@signalwire.com - -Provide your SIP domain app and let them know you need a password for VAPI integration. - - ---- - -## Configuring VAPI for outbound calls - -Now we'll set up VAPI to make outbound calls through your SignalWire SIP domain app. This follows VAPI's official [SIP trunk setup guide][vapi-sip-guide], but with SignalWire-specific configuration. - -### Create the outbound SIP trunk - -Run this API call to create the outbound trunk, making sure to replace the placeholder values with your actual configuration: - -```bash -curl -X POST "https://api.vapi.ai/credential" \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer YOUR_VAPI_PRIVATE_KEY" \ - -d '{ - "provider": "byo-sip-trunk", - "name": "SignalWire Outbound Trunk", - "gateways": [{ - "ip": "YOUR_SIGNALWIRE_SIP_DOMAIN", - "inboundEnabled": false - }], - "outboundLeadingPlusEnabled": true, - "outboundAuthenticationPlan": { - "authUsername": "YOUR_SIGNALWIRE_PHONE_NUMBER", - "authPassword": "YOUR_SIGNALWIRE_PASSWORD" - } - }' -``` - -Make sure to replace: -- `YOUR_VAPI_PRIVATE_KEY` - Your VAPI API key -- `YOUR_SIGNALWIRE_SIP_DOMAIN` - Your SIP domain app from the previous step (e.g., `test-space-vapi.dapp.signalwire.com`) -- `YOUR_SIGNALWIRE_PHONE_NUMBER` - Your SignalWire phone number in E.164 format (e.g., `+15551234567`) -- `YOUR_SIGNALWIRE_PASSWORD` - The password provided by SignalWire Support - -Save the `id` from the response - you'll need this credential ID for the next step. - -### Register your SignalWire phone number for outbound - -Register your SignalWire phone number with VAPI for outbound calling using the credential ID from above: - -```bash -curl -X POST "https://api.vapi.ai/phone-number" \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer YOUR_VAPI_PRIVATE_KEY" \ - -d '{ - "provider": "byo-phone-number", - "name": "SignalWire Outbound Number", - "number": "+15551234567", - "numberE164CheckEnabled": true, - "credentialId": "YOUR_CREDENTIAL_ID" - }' -``` - -Replace: -- `YOUR_VAPI_PRIVATE_KEY` - Your VAPI API key -- `+15551234567` - Your actual SignalWire phone number in E.164 format -- `YOUR_CREDENTIAL_ID` - The credential ID from the trunk creation step - -### Assign your AI assistant for outbound calls - -In your VAPI dashboard, find the phone number you just registered and assign it to one of your AI assistants. Configure the outbound settings to specify which assistant should handle outbound calls. - - -If you've already set up inbound calling with the same number, VAPI will indicate this is a duplicate number. This is completely normal and expected - you're using the same phone number for both inbound and outbound calling with different configurations. - - ---- - -## Testing your outbound setup - -Your VAPI assistant can now make outbound calls through SignalWire. The calls will show your SignalWire phone number as the caller ID. - -To test your setup, refer to VAPI's official documentation on [testing your SIP trunk][vapi-test-guide] for specific instructions on initiating outbound calls. - ---- - -## Troubleshooting - -**Authentication failures**: Double-check that you're using the correct SIP domain app password from SignalWire Support. The authentication username should be your phone number in E.164 format. - -**Calls not connecting**: Verify your SWML script has the correct caller ID number and that it's a valid number from your SignalWire account. - -**Domain app issues**: Ensure your SIP domain app is properly configured in SignalWire and that VAPI can resolve the domain name. - ---- - -## What's next - -Now that you have outbound calling configured, you might want to explore: - - - - Complete your integration by setting up inbound calls from SignalWire to VAPI - - - - Add call screening, business hours logic, or complex routing before outbound calls - - - - Best practices for creating effective VAPI assistants for outbound campaigns - - diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx new file mode 100644 index 0000000000..ba1524143e --- /dev/null +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -0,0 +1,343 @@ +--- +title: Outbound calling +slug: /voice/outbound-calling +description: Place outbound calls from SignalWire with the Calling API, an AI agent, the Compatibility API, or a Relay server, then confirm the call connected. +max-toc-depth: 3 +--- + +[calling-api]: /docs/apis/rest/calls/call-commands +[compat-create-call]: /docs/compatibility-api/rest/calls/create-a-call +[compat-status-callback]: /docs/compatibility-api/rest/calls/webhooks/voice-status-callback +[relay-dial-python]: /docs/server-sdks/reference/python/relay/client/dial +[relay-dial-ts]: /docs/server-sdks/reference/typescript/relay/client/dial +[browser-outbound]: /docs/browser-sdk/v4/guides/outbound-calls +[sip-credentials]: /docs/platform/voice/sip/sip-credentials +[caller-id]: /docs/platform/voice/how-to-set-caller-id-or-cnam +[stir-shaken]: /docs/platform/voice/stir-shaken +[spam-labels]: /docs/platform/voice/resolving-spam-labels +[trial-mode]: /docs/platform/trial-mode +[international]: /docs/platform/how-to-enable-international-services +[api-credentials]: /docs/platform/your-signalwire-api-space +[phone-numbers]: /docs/platform/phone-numbers +[error-codes]: /docs/apis/error-codes +[swml-ai]: /docs/swml/reference/calling/ai +[swml-recipe]: /docs/swml/guides/make-and-receive-calls +[swml-webhook-security]: /docs/swml/guides/webhook-security +[swml-expressions]: /docs/swml/reference/expressions +[ai-quickstart]: /docs/platform/ai/quickstart +[ai-best-practices]: /docs/platform/ai/best-practices +[tcpa]: /docs/platform/compliance/tcpa + +An outbound call takes one request: the number to call, the SignalWire number it comes from, and +what should happen when someone answers. SignalWire places the call and hands control to your +SWML, cXML, or Relay code. + +The examples below all place the same call. Bayview Taxi phones a rider to say their driver is on +the way, first as a spoken announcement and then as a conversation with an AI agent. + +## Before you dial + +You need a [phone number][phone-numbers] purchased in your project, or a +[verified caller ID][caller-id], to call from. Any number you pass as `from` or `caller_id` on a +call to the public telephone network must be one of those, in E.164 format such as `+15551234567`. + +You also need your Project ID and an API token with voice permissions from the +[API credentials][api-credentials] page of your Dashboard. + +Two account settings decide whether the call is allowed at all. A project in +[trial mode][trial-mode] can only call purchased and verified numbers and can't call +internationally. Outside trial mode, calls to other countries still need +[international dialing enabled][international] for your Space. + +## Choose how to place the call + +| Method | Pick it when | What controls the call | +|---|---|---| +| [Calling API][calling-api] | Your code makes one HTTP request per call | SWML fetched from your URL or sent inline | +| AI agent | The person you call should talk to an agent | The SWML [`ai` method][swml-ai] | +| [Compatibility API][compat-create-call] | You're porting cXML code from another provider | cXML fetched from your URL | +| Relay SDK | Your server stays on the call and reacts to events in real time | Your [Python][relay-dial-python] or [TypeScript][relay-dial-ts] code over a WebSocket | +| [Browser SDK][browser-outbound] | A user clicks to call from a web page | Your browser code | +| SIP device | A registered SIP phone or PBX dials out through SignalWire | The call handler on its [SIP credential][sip-credentials] | + +The first four are shown below. The Browser SDK guide and the SIP credentials page each cover their own outbound flow. + +## Place a call with the Calling API + +Send a `dial` command to the [Calling API][calling-api]. The request needs `from`, `to`, and one +of `url` or `swml`. With `url`, SignalWire requests your endpoint when the call is created and runs +the SWML it returns. With `swml`, you send the document inline as a JSON object. + + + +```bash +curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ + -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "+15551234567", + "to": "+15557654321", + "url": "https://example.com/swml/driver-on-the-way", + "status_url": "https://example.com/call-status", + "status_events": ["answered", "ended"] + } + }' +``` + + +```bash +curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ + -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "+15551234567", + "to": "+15557654321", + "status_url": "https://example.com/call-status", + "status_events": ["answered", "ended"], + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { "play": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } + ] + } + } + } + }' +``` + + + +The response returns the new call right away, before anyone answers: + +```json +{ + "id": "0e9c80d7-a149-4917-892d-420043709f45", + "from": "+15551234567", + "to": "+15557654321", + "direction": "outbound-api", + "status": "queued", + "created_at": "2026-09-04T15:20:00Z" +} +``` + +Keep the `id`. Every other Calling API command, such as ending or transferring the call, takes it. +Progress arrives at `status_url` as webhooks for the events you list in `status_events`: `created`, +`ringing`, `answered`, and `ended`. If you omit `status_events`, you get `ended` only. + +Three optional parameters cover most other needs. `caller_id` shows a different number of yours to +the person you call. `timeout` sets how many seconds to ring, from 1 to 600. `custom_variables` +attaches up to 20 key-value strings to the call, which your SWML reads as `${envs.}` using +[SWML expressions][swml-expressions]. The full list is on the [Calling API reference][calling-api]. + +## Place a call with an AI agent + +An AI agent is an outbound call whose SWML runs the [`ai` method][swml-ai]. The rider can ask how +far away the driver is or say they no longer need the ride, and the agent answers or acts. Use the +same `dial` command, and either send the agent inline or point `url` at an agent you serve from +your own server, as in the [AI quickstart][ai-quickstart]. + + + +```bash +curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ + -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "+15551234567", + "to": "+15557654321", + "status_url": "https://example.com/call-status", + "status_events": ["answered", "ended"], + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { + "ai": { + "params": { + "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + "static_greeting_no_barge": true + }, + "prompt": { + "text": "You are calling to tell the rider their driver is about five minutes away. Answer questions about the pickup. If the rider says they no longer need the ride, or asks not to be contacted again, call cancel_pickup before saying anything else." + }, + "SWAIG": { + "functions": [ + { + "function": "cancel_pickup", + "description": "Cancel the pickup and record that the rider does not want further calls", + "parameters": { "type": "object", "properties": {} }, + "web_hook_url": "https://example.com/swaig/cancel-pickup" + } + ] + } + } + } + ] + } + } + } + }' +``` + + +```bash +curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ + -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "+15551234567", + "to": "+15557654321", + "url": "https://signalwire:YOUR_AGENT_PASSWORD@agent.example.com/", + "status_url": "https://example.com/call-status", + "status_events": ["answered", "ended"] + } + }' +``` + + + +The `static_greeting` plays in full before the agent's first turn, so the AI disclosure is heard +even if the rider starts talking. The `cancel_pickup` function calls your server, which cancels the +ride in your dispatch system and records the opt-out before the agent confirms anything aloud. + + +An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). +Check consent, your do-not-call list, and the local calling hours in your code before you send the +`dial` request, because once the call is placed it has already happened. The [TCPA guide][tcpa] and +the compliance section of [AI best practices][ai-best-practices] cover each obligation. This is +technical guidance, not legal advice. + + +## Place a call with the Compatibility API + +Code written against cXML uses the Compatibility API's [Create a Call][compat-create-call] +endpoint. The request is form-encoded, `From` and `To` are required, and `Url` points at the cXML +document that runs when the call is answered. Call status webhooks go to `StatusCallback`, with the +`completed` event by default. See the [voice status callback][compat-status-callback] for the +payload. + +```bash +curl -X POST "https://YOUR_SPACE.signalwire.com/api/laml/2010-04-01/Accounts/YOUR_PROJECT_ID/Calls" \ + -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ + --data-urlencode "From=+15551234567" \ + --data-urlencode "To=+15557654321" \ + --data-urlencode "Url=https://example.com/cxml/driver-on-the-way.xml" \ + --data-urlencode "StatusCallback=https://example.com/call-status" +``` + +If you're writing new code rather than porting, use the Calling API above. The AI agent example +and the call commands in this guide run on SWML, which the Calling API executes and the +Compatibility API doesn't. + +## Place a call from your server with Relay + +Relay keeps your server on a WebSocket for the life of the call, so you place the call and then +act on it in the same program. The [`dial`][relay-dial-python] method waits for an answer and +returns a call object you control directly. + +```python +import asyncio +from signalwire.relay import RelayClient + +client = RelayClient( + project="YOUR_PROJECT_ID", + token="YOUR_API_TOKEN", + host="YOUR_SPACE.signalwire.com", + contexts=["default"], +) + +async def main(): + async with client: + call = await client.dial( + devices=[[{ + "type": "phone", + "params": { + "from_number": "+15551234567", + "to_number": "+15557654321", + "timeout": 30, + }, + }]], + ) + action = await call.play([{ + "type": "tts", + "params": {"text": "Hi, this is Bayview Taxi. Your driver is on the way."}, + }]) + await action.wait() + await call.hangup() + +asyncio.run(main()) +``` + +`dial` raises `RelayError` if the call fails or nobody answers within `dial_timeout`, which +defaults to 120 seconds. The [TypeScript client][relay-dial-ts] has the same method. + +## Confirm the call connected + +With the Calling API, a `200` response with a call `id` means SignalWire accepted the request, not +that anyone answered. The `answered` event at your `status_url` is the confirmation, followed by +`ended` when the call finishes. If `ended` arrives without `answered`, the destination didn't pick +up or rejected the call. With Relay, a returned call object is the confirmation, and a +`RelayError` is the failure. + +The likeliest failure is a `422` before the call is placed, with an error code such as +`not_purchased_or_verified`. That means the `from` or `caller_id` number isn't purchased in this +project or verified as a caller ID. The next section covers the other common causes. + +## Troubleshoot outbound calls + +### The request is rejected before the call is placed + +A `422` response carries an [error code][error-codes] that names the problem. `not_purchased_or_verified` +means the `from` number isn't yours. `not_valid_for_caller_id` means `from` or `caller_id` isn't an +E.164 number, caller ID string, or SIP URI. `invalid_destination_number` and +`destination_number_not_supported` point at `to`. `insufficient_balance` means the project can't pay +for the call. A `dial` request that has neither `url` nor `swml` is also rejected. + +### The call ends without being answered + +Check the account limits first. A project in [trial mode][trial-mode] can only reach purchased and +verified numbers. International destinations fail until you [enable international +dialing][international]. If those are fine, a `timeout` shorter than the destination's ring time +ends the call early, and a `max_price_per_minute` below the route's price rejects it before it +rings. + +### The person you call sees a spam warning or the wrong caller ID + +The displayed number is `from`, or `caller_id` when you set it, and both must be numbers you own. +Set the caller name with the [Caller ID and CNAM][caller-id] guide. A "Spam likely" label on your +number is a reputation problem rather than a configuration one. Start with the +[spam labels][spam-labels] guide and confirm your calls carry [STIR/SHAKEN][stir-shaken] +attestation. + +### The call connects but your SWML or cXML doesn't run + +SignalWire requests `url` with `POST` unless you set `url_method`, and the endpoint has to return a +SWML document. Set `fallback_url` so a failed fetch still gets instructions, and secure the endpoint +as described in [SWML webhook security][swml-webhook-security]. An inline `swml` value must be a +JSON object, not a string of escaped JSON. + +## Next steps + + + + Every `dial` parameter, plus the commands that control a call after it's placed. + + + Handle the inbound side and build the SWML your outbound calls run. + + + Prompt design, speech hints, and the compliance controls an outbound agent needs. + + + Control the number and name the person you call sees. + + From 3c70c1f01ad61e47c4418c1e359f8b5fec0e0498 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 09:24:26 -0400 Subject: [PATCH 002/103] docs: restructure outbound calling guide around control style, not surface Group by hand-off vs. stay-on-the-call control instead of one section per API, cover the Server SDK's REST client (Python + TypeScript) and the Browser SDK, add a themed SVG lifecycle diagram for human readers with the mermaid source kept for the LLM export, and drop the Compatibility API and cXML from this guide's scope. Co-Authored-By: Claude Sonnet 5 --- .../img/outbound-call-lifecycle-themed.svg | 166 +++++++ .../pages/calling/voice/outbound-calling.mdx | 465 +++++++++++------- 2 files changed, 452 insertions(+), 179 deletions(-) create mode 100644 fern/assets/images/img/outbound-call-lifecycle-themed.svg diff --git a/fern/assets/images/img/outbound-call-lifecycle-themed.svg b/fern/assets/images/img/outbound-call-lifecycle-themed.svg new file mode 100644 index 0000000000..3a329f1bfb --- /dev/null +++ b/fern/assets/images/img/outbound-call-lifecycle-themed.svg @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OUTBOUND CALL LIFECYCLE + Solid: actions on the call · Dashed: what your code receives + + + + + + + + + + + Your code + API request, SDK, or browser + + + + + SignalWire + Places and controls the call + + + + + Rider's phone + The number in to + + + dial: from, to, and what to run + + + 1 + + + call id · status: queued + + + 2 + + + rings + + + 3 + + + status webhook: ringing + + + + answers + + + 4 + + + status webhook: answered + + + + + Your call logic runs + The SWML you supplied plays out, + or your Relay code drives the live call + + + hangs up + + + 5 + + + status webhook: ended + + + + diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index ba1524143e..82e3c779d1 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -1,16 +1,18 @@ --- title: Outbound calling slug: /voice/outbound-calling -description: Place outbound calls from SignalWire with the Calling API, an AI agent, the Compatibility API, or a Relay server, then confirm the call connected. +description: Place an outbound call from the Calling API, the Server SDK over REST or Relay, or the Browser SDK, choose the SWML that runs when it's answered, and confirm it connected. max-toc-depth: 3 --- [calling-api]: /docs/apis/rest/calls/call-commands -[compat-create-call]: /docs/compatibility-api/rest/calls/create-a-call -[compat-status-callback]: /docs/compatibility-api/rest/calls/webhooks/voice-status-callback +[sdk-rest-dial-python]: /docs/server-sdks/reference/python/rest/calling/dial +[sdk-rest-dial-ts]: /docs/server-sdks/reference/typescript/rest/calling/dial [relay-dial-python]: /docs/server-sdks/reference/python/relay/client/dial [relay-dial-ts]: /docs/server-sdks/reference/typescript/relay/client/dial +[relay-guide]: /docs/server-sdks/guides/relay-client [browser-outbound]: /docs/browser-sdk/v4/guides/outbound-calls +[browser-auth]: /docs/browser-sdk/v4/guides/authentication [sip-credentials]: /docs/platform/voice/sip/sip-credentials [caller-id]: /docs/platform/voice/how-to-set-caller-id-or-cnam [stir-shaken]: /docs/platform/voice/stir-shaken @@ -21,7 +23,6 @@ max-toc-depth: 3 [phone-numbers]: /docs/platform/phone-numbers [error-codes]: /docs/apis/error-codes [swml-ai]: /docs/swml/reference/calling/ai -[swml-recipe]: /docs/swml/guides/make-and-receive-calls [swml-webhook-security]: /docs/swml/guides/webhook-security [swml-expressions]: /docs/swml/reference/expressions [ai-quickstart]: /docs/platform/ai/quickstart @@ -29,11 +30,67 @@ max-toc-depth: 3 [tcpa]: /docs/platform/compliance/tcpa An outbound call takes one request: the number to call, the SignalWire number it comes from, and -what should happen when someone answers. SignalWire places the call and hands control to your -SWML, cXML, or Relay code. +what should happen when someone answers. SignalWire rings the destination and, when the call +connects, either runs the call logic you supplied or hands the live call back to your code. -The examples below all place the same call. Bayview Taxi phones a rider to say their driver is on -the way, first as a spoken announcement and then as a conversation with an AI agent. +Every example on this page places the same call. Bayview Taxi phones a rider to say their driver is +on the way, first as a spoken announcement and then as a conversation with an AI agent. + +## How an outbound call works + +There are two ways to control a call you place, and every SignalWire surface uses one of them. + +**Hand SignalWire the call logic.** Your request names a SWML document that SignalWire runs when +the call is answered, through the [Calling API][calling-api] or the Server SDK's REST client. The +request returns as soon as the call is queued, and progress arrives later as status webhooks. +Nothing of yours has to stay running. + +**Keep your code on the call.** The Server SDK's Relay client holds a WebSocket open, so `dial` +waits for the answer and returns a call object you drive directly. The Browser SDK does the same +from the user's device, where `dial` returns a call whose media you attach to the page. + +The sequence below follows the hand-off flow. Solid arrows are actions on the call, and dashed +arrows are what comes back to your code. + + + +Your code sends dial with from, to, and what to run. SignalWire returns the call id with status queued, rings the rider's phone, and sends a ringing status webhook. The rider answers and SignalWire sends an answered webhook, then your SWML runs or your Relay code drives the call. When the rider hangs up, SignalWire sends an ended webhook. + + + + + +```mermaid +sequenceDiagram + autonumber + participant App as Your code + participant SW as SignalWire + participant Rider as Rider's phone + + App->>SW: dial from, to, and what to run + SW-->>App: call id, status queued + SW->>Rider: rings + SW-->>App: status webhook ringing + Rider->>SW: answers + SW-->>App: status webhook answered + Note over SW,Rider: Your SWML runs,
or your Relay code drives the call + Rider->>SW: hangs up + SW-->>App: status webhook ended +``` + +
+ +| Surface | Control style | What runs the call | +|---|---|---| +| [Calling API][calling-api] | Hand off | SWML fetched from your URL or sent inline | +| Server SDK, REST client | Hand off | The same SWML, from [Python][sdk-rest-dial-python] or [TypeScript][sdk-rest-dial-ts] | +| Server SDK, [Relay client][relay-guide] | Stay on the call | Your Python or TypeScript code over a WebSocket | +| [Browser SDK][browser-outbound] | Stay on the call | Your browser code | +| SIP device | Hand off | The call handler on its [SIP credential][sip-credentials] | + +A registered SIP phone or PBX dials out through SignalWire without any code of yours. The +[SIP credentials][sip-credentials] page covers that setup, and the rest of this guide covers the +programmatic surfaces. ## Before you dial @@ -42,34 +99,24 @@ You need a [phone number][phone-numbers] purchased in your project, or a call to the public telephone network must be one of those, in E.164 format such as `+15551234567`. You also need your Project ID and an API token with voice permissions from the -[API credentials][api-credentials] page of your Dashboard. +[API credentials][api-credentials] page of your Dashboard. Browser calls use a +[Subscriber Access Token][browser-auth] your backend issues for the user instead. Two account settings decide whether the call is allowed at all. A project in [trial mode][trial-mode] can only call purchased and verified numbers and can't call internationally. Outside trial mode, calls to other countries still need [international dialing enabled][international] for your Space. -## Choose how to place the call +## Place the call -| Method | Pick it when | What controls the call | -|---|---|---| -| [Calling API][calling-api] | Your code makes one HTTP request per call | SWML fetched from your URL or sent inline | -| AI agent | The person you call should talk to an agent | The SWML [`ai` method][swml-ai] | -| [Compatibility API][compat-create-call] | You're porting cXML code from another provider | cXML fetched from your URL | -| Relay SDK | Your server stays on the call and reacts to events in real time | Your [Python][relay-dial-python] or [TypeScript][relay-dial-ts] code over a WebSocket | -| [Browser SDK][browser-outbound] | A user clicks to call from a web page | Your browser code | -| SIP device | A registered SIP phone or PBX dials out through SignalWire | The call handler on its [SIP credential][sip-credentials] | - -The first four are shown below. The Browser SDK guide and the SIP credentials page each cover their own outbound flow. - -## Place a call with the Calling API +Each tab below places the announcement call with a different surface. They all point at the same +SWML URL and ask for the same two status events, so the [call logic](#choose-what-runs-on-the-call) +and the [confirmation](#confirm-the-call-connected) that follow apply to every one of them. -Send a `dial` command to the [Calling API][calling-api]. The request needs `from`, `to`, and one -of `url` or `swml`. With `url`, SignalWire requests your endpoint when the call is created and runs -the SWML it returns. With `swml`, you send the document inline as a JSON object. +### Hand SignalWire the call logic - + ```bash curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ @@ -86,33 +133,50 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ }' ``` - -```bash -curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ - -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "command": "dial", - "params": { - "from": "+15551234567", - "to": "+15557654321", - "status_url": "https://example.com/call-status", - "status_events": ["answered", "ended"], - "swml": { - "version": "1.0.0", - "sections": { - "main": [ - { "play": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } - ] - } - } - } - }' + +```python +from signalwire.rest import RestClient + +client = RestClient( + project="YOUR_PROJECT_ID", + token="YOUR_API_TOKEN", + host="YOUR_SPACE.signalwire.com", +) + +call = client.calling.dial( + from_="+15551234567", + to="+15557654321", + url="https://example.com/swml/driver-on-the-way", + status_url="https://example.com/call-status", + status_events=["answered", "ended"], +) +print(call["id"]) +``` + + +```typescript +import { RestClient } from "@signalwire/sdk"; + +const client = new RestClient({ + project: "YOUR_PROJECT_ID", + token: "YOUR_API_TOKEN", + host: "YOUR_SPACE.signalwire.com", +}); + +const call = await client.calling.dial({ + from: "+15551234567", + to: "+15557654321", + url: "https://example.com/swml/driver-on-the-way", + status_url: "https://example.com/call-status", + status_events: ["answered", "ended"], +}); +console.log(call.id); ``` -The response returns the new call right away, before anyone answers: +All three send the same `dial` command, so their parameters match. The response returns the new +call before anyone answers: ```json { @@ -127,123 +191,13 @@ The response returns the new call right away, before anyone answers: Keep the `id`. Every other Calling API command, such as ending or transferring the call, takes it. Progress arrives at `status_url` as webhooks for the events you list in `status_events`: `created`, -`ringing`, `answered`, and `ended`. If you omit `status_events`, you get `ended` only. +`ringing`, `answered`, and `ended`. If you omit `status_events`, you get `ended` only. The full +parameter list is on the [Calling API reference][calling-api]. -Three optional parameters cover most other needs. `caller_id` shows a different number of yours to -the person you call. `timeout` sets how many seconds to ring, from 1 to 600. `custom_variables` -attaches up to 20 key-value strings to the call, which your SWML reads as `${envs.}` using -[SWML expressions][swml-expressions]. The full list is on the [Calling API reference][calling-api]. - -## Place a call with an AI agent - -An AI agent is an outbound call whose SWML runs the [`ai` method][swml-ai]. The rider can ask how -far away the driver is or say they no longer need the ride, and the agent answers or acts. Use the -same `dial` command, and either send the agent inline or point `url` at an agent you serve from -your own server, as in the [AI quickstart][ai-quickstart]. +### Keep your code on the call - -```bash -curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ - -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "command": "dial", - "params": { - "from": "+15551234567", - "to": "+15557654321", - "status_url": "https://example.com/call-status", - "status_events": ["answered", "ended"], - "swml": { - "version": "1.0.0", - "sections": { - "main": [ - { - "ai": { - "params": { - "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", - "static_greeting_no_barge": true - }, - "prompt": { - "text": "You are calling to tell the rider their driver is about five minutes away. Answer questions about the pickup. If the rider says they no longer need the ride, or asks not to be contacted again, call cancel_pickup before saying anything else." - }, - "SWAIG": { - "functions": [ - { - "function": "cancel_pickup", - "description": "Cancel the pickup and record that the rider does not want further calls", - "parameters": { "type": "object", "properties": {} }, - "web_hook_url": "https://example.com/swaig/cancel-pickup" - } - ] - } - } - } - ] - } - } - } - }' -``` - - -```bash -curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ - -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "command": "dial", - "params": { - "from": "+15551234567", - "to": "+15557654321", - "url": "https://signalwire:YOUR_AGENT_PASSWORD@agent.example.com/", - "status_url": "https://example.com/call-status", - "status_events": ["answered", "ended"] - } - }' -``` - - - -The `static_greeting` plays in full before the agent's first turn, so the AI disclosure is heard -even if the rider starts talking. The `cancel_pickup` function calls your server, which cancels the -ride in your dispatch system and records the opt-out before the agent confirms anything aloud. - - -An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). -Check consent, your do-not-call list, and the local calling hours in your code before you send the -`dial` request, because once the call is placed it has already happened. The [TCPA guide][tcpa] and -the compliance section of [AI best practices][ai-best-practices] cover each obligation. This is -technical guidance, not legal advice. - - -## Place a call with the Compatibility API - -Code written against cXML uses the Compatibility API's [Create a Call][compat-create-call] -endpoint. The request is form-encoded, `From` and `To` are required, and `Url` points at the cXML -document that runs when the call is answered. Call status webhooks go to `StatusCallback`, with the -`completed` event by default. See the [voice status callback][compat-status-callback] for the -payload. - -```bash -curl -X POST "https://YOUR_SPACE.signalwire.com/api/laml/2010-04-01/Accounts/YOUR_PROJECT_ID/Calls" \ - -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ - --data-urlencode "From=+15551234567" \ - --data-urlencode "To=+15557654321" \ - --data-urlencode "Url=https://example.com/cxml/driver-on-the-way.xml" \ - --data-urlencode "StatusCallback=https://example.com/call-status" -``` - -If you're writing new code rather than porting, use the Calling API above. The AI agent example -and the call commands in this guide run on SWML, which the Calling API executes and the -Compatibility API doesn't. - -## Place a call from your server with Relay - -Relay keeps your server on a WebSocket for the life of the call, so you place the call and then -act on it in the same program. The [`dial`][relay-dial-python] method waits for an answer and -returns a call object you control directly. - + ```python import asyncio from signalwire.relay import RelayClient @@ -276,17 +230,163 @@ async def main(): asyncio.run(main()) ``` + + +```typescript +import { RelayClient } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "YOUR_PROJECT_ID", + token: "YOUR_API_TOKEN", + host: "YOUR_SPACE.signalwire.com", + contexts: ["default"], +}); + +await client.connect(); + +const call = await client.dial([[{ + type: "phone", + params: { + from_number: "+15551234567", + to_number: "+15557654321", + timeout: 30, + }, +}]]); +const action = await call.play([ + { type: "tts", text: "Hi, this is Bayview Taxi. Your driver is on the way." }, +]); +await action.wait(); +await call.hangup(); + +await client.disconnect(); +``` + + +```typescript +import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; + +// SAT is a Subscriber Access Token your backend issued for this user. +const client = new SignalWire(new StaticCredentialProvider({ token: SAT })); -`dial` raises `RelayError` if the call fails or nobody answers within `dial_timeout`, which -defaults to 120 seconds. The [TypeScript client][relay-dial-ts] has the same method. +// Audio only, the default for a phone-style call. +const call = await client.dial("+15557654321"); +call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); + +hangupButton.onclick = () => call.hangup(); +``` + + + +With Relay there is no document to write. [`dial`][relay-dial-python] waits for the answer and +returns a call object, so the announcement is a `play` on that object and the hangup is your call +to make. The `devices` list also takes several numbers to ring in turn or at once. If nobody answers +within `dial_timeout`, which defaults to 120 seconds, `dial` raises `RelayError`. The +[TypeScript client][relay-dial-ts] behaves the same way. + +A [Browser SDK][browser-outbound] call is placed from the user's device with the token your backend +issued, so the token has to be allowed to reach phone numbers. The same `dial` reaches rooms, other +users, and SIP endpoints by changing the destination string. + +## Choose what runs on the call + +On the hand-off surfaces, the SWML behind `url` is what the person you call hears. SignalWire +requests it when the call is created, with `POST` unless you set `url_method`, and runs whatever it +returns. Serve a spoken announcement, or an AI agent that can hold a conversation. + + + +```yaml +version: 1.0.0 +sections: + main: + - play: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." +``` + + +```yaml +version: 1.0.0 +sections: + main: + - ai: + params: + static_greeting: "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice." + static_greeting_no_barge: true + prompt: + text: | + You are calling to tell the rider their driver is about five minutes away. + Answer questions about the pickup. If the rider says they no longer need the + ride, or asks not to be contacted again, call cancel_pickup before saying + anything else. + SWAIG: + functions: + - function: cancel_pickup + description: Cancel the pickup and record that the rider does not want further calls + parameters: + type: object + properties: {} + web_hook_url: https://example.com/swaig/cancel-pickup +``` + + + +An AI agent is an outbound call whose SWML runs the [`ai` method][swml-ai]. The rider can ask how +far away the driver is or say they no longer need the ride, and the agent answers or acts. The +`static_greeting` plays in full before the agent's first turn, so the AI disclosure is heard even +if the rider starts talking. The `cancel_pickup` function calls your server, which cancels the ride +in your dispatch system and records the opt-out before the agent confirms anything aloud. To serve +the agent from your own code instead of a static document, point `url` at a Server SDK agent as in +the [AI quickstart][ai-quickstart]. + + +An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). +Check consent, your do-not-call list, and the local calling hours in your code before you send the +`dial` request, because once the call is placed it has already happened. The [TCPA guide][tcpa] and +the compliance section of [AI best practices][ai-best-practices] cover each obligation. This is +technical guidance, not legal advice. + + +### Send the logic inline and carry data onto the call + +You don't have to host the document. Send it in the `swml` field instead of `url`, as a JSON +object rather than a string of escaped JSON. To make the logic specific to one call, attach your +own values in `custom_variables` and read them in the SWML as `${envs.}` using +[SWML expressions][swml-expressions]. + +```bash +curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ + -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "+15551234567", + "to": "+15557654321", + "custom_variables": { "driver_name": "Sam", "eta_minutes": "5" }, + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { "play": "say:Hi, this is Bayview Taxi. ${envs.driver_name} is on the way and will arrive in about ${envs.eta_minutes} minutes." } + ] + } + } + } + }' +``` + +`custom_variables` takes up to 20 string pairs, and the same pairs are sent to your endpoint when +SignalWire fetches SWML from `url`. Two more parameters shape most other calls. `caller_id` shows a +different number of yours to the person you call. `timeout` sets how many seconds to ring, from 1 +to 600. ## Confirm the call connected -With the Calling API, a `200` response with a call `id` means SignalWire accepted the request, not -that anyone answered. The `answered` event at your `status_url` is the confirmation, followed by +On the hand-off surfaces, a `200` response with a call `id` means SignalWire accepted the request, +not that anyone answered. The `answered` event at your `status_url` is the confirmation, followed by `ended` when the call finishes. If `ended` arrives without `answered`, the destination didn't pick -up or rejected the call. With Relay, a returned call object is the confirmation, and a -`RelayError` is the failure. +up or rejected the call. With Relay, a returned call object is the confirmation and `RelayError` is +the failure. In the browser, `status$` reaching `connected` is the confirmation, and a `dial()` that +rejects with `CallCreateError` means the token isn't allowed to reach that destination. The likeliest failure is a `422` before the call is placed, with an error code such as `not_purchased_or_verified`. That means the `from` or `caller_id` number isn't purchased in this @@ -294,7 +394,8 @@ project or verified as a caller ID. The next section covers the other common cau ## Troubleshoot outbound calls -### The request is rejected before the call is placed + + A `422` response carries an [error code][error-codes] that names the problem. `not_purchased_or_verified` means the `from` number isn't yours. `not_valid_for_caller_id` means `from` or `caller_id` isn't an @@ -302,7 +403,8 @@ E.164 number, caller ID string, or SIP URI. `invalid_destination_number` and `destination_number_not_supported` point at `to`. `insufficient_balance` means the project can't pay for the call. A `dial` request that has neither `url` nor `swml` is also rejected. -### The call ends without being answered + + Check the account limits first. A project in [trial mode][trial-mode] can only reach purchased and verified numbers. International destinations fail until you [enable international @@ -310,7 +412,8 @@ dialing][international]. If those are fine, a `timeout` shorter than the destina ends the call early, and a `max_price_per_minute` below the route's price rejects it before it rings. -### The person you call sees a spam warning or the wrong caller ID + + The displayed number is `from`, or `caller_id` when you set it, and both must be numbers you own. Set the caller name with the [Caller ID and CNAM][caller-id] guide. A "Spam likely" label on your @@ -318,26 +421,30 @@ number is a reputation problem rather than a configuration one. Start with the [spam labels][spam-labels] guide and confirm your calls carry [STIR/SHAKEN][stir-shaken] attestation. -### The call connects but your SWML or cXML doesn't run + + SignalWire requests `url` with `POST` unless you set `url_method`, and the endpoint has to return a SWML document. Set `fallback_url` so a failed fetch still gets instructions, and secure the endpoint as described in [SWML webhook security][swml-webhook-security]. An inline `swml` value must be a JSON object, not a string of escaped JSON. + + + ## Next steps Every `dial` parameter, plus the commands that control a call after it's placed. - - Handle the inbound side and build the SWML your outbound calls run. + + Stay on the call from your server and react to events as they happen. - - Prompt design, speech hints, and the compliance controls an outbound agent needs. + + Destinations, media options, and call controls for click-to-call pages. - - Control the number and name the person you call sees. + + Handle the inbound side and build the SWML your outbound calls run. From 80f3ebc3c4ee3116f5b18a02a8e27753389edbaf Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 11:47:33 -0400 Subject: [PATCH 003/103] docs: clarify outbound calling examples and improve diagram readability --- .../img/outbound-call-lifecycle-themed.svg | 241 ++++++------------ .../pages/calling/voice/outbound-calling.mdx | 129 +++++----- 2 files changed, 144 insertions(+), 226 deletions(-) diff --git a/fern/assets/images/img/outbound-call-lifecycle-themed.svg b/fern/assets/images/img/outbound-call-lifecycle-themed.svg index 3a329f1bfb..f2facb907a 100644 --- a/fern/assets/images/img/outbound-call-lifecycle-themed.svg +++ b/fern/assets/images/img/outbound-call-lifecycle-themed.svg @@ -1,166 +1,85 @@ - + + Outbound call with the Calling API or Server SDK REST client + Your code sends dial with from, to, and SWML. SignalWire returns a call id with status queued and rings the rider's phone. The rider answers, SignalWire sends an answered webhook, and your SWML runs. When the call finishes, SignalWire sends an ended webhook. - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + Your code + + + + + SignalWire + + + + + Rider’s phone + + + dial + from, to, SWML + + call id, queued + + + + rings + + answers + + + + answered + webhook + + + + Your SWML runs + Call finishes + + ended + webhook + diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 82e3c779d1..ea396f8a6c 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -1,7 +1,7 @@ --- title: Outbound calling slug: /voice/outbound-calling -description: Place an outbound call from the Calling API, the Server SDK over REST or Relay, or the Browser SDK, choose the SWML that runs when it's answered, and confirm it connected. +description: Understand how outbound calls work, place a call with the Calling API or SDKs, and choose how to control it when someone answers. max-toc-depth: 3 --- @@ -26,6 +26,7 @@ max-toc-depth: 3 [swml-webhook-security]: /docs/swml/guides/webhook-security [swml-expressions]: /docs/swml/reference/expressions [ai-quickstart]: /docs/platform/ai/quickstart +[tool-calling]: /docs/platform/ai/tool-calling [ai-best-practices]: /docs/platform/ai/best-practices [tcpa]: /docs/platform/compliance/tcpa @@ -33,28 +34,30 @@ An outbound call takes one request: the number to call, the SignalWire number it what should happen when someone answers. SignalWire rings the destination and, when the call connects, either runs the call logic you supplied or hands the live call back to your code. -Every example on this page places the same call. Bayview Taxi phones a rider to say their driver is -on the way, first as a spoken announcement and then as a conversation with an AI agent. +Bayview Taxi needs to tell a rider their driver is on the way. The examples show an automated +announcement, an AI conversation, and a dispatcher calling from a browser. ## How an outbound call works -There are two ways to control a call you place, and every SignalWire surface uses one of them. +Your application can supply instructions for SignalWire to run or control the call as it happens. -**Hand SignalWire the call logic.** Your request names a SWML document that SignalWire runs when -the call is answered, through the [Calling API][calling-api] or the Server SDK's REST client. The -request returns as soon as the call is queued, and progress arrives later as status webhooks. -Nothing of yours has to stay running. +**Hand SignalWire the call logic.** Use the [Calling API][calling-api] or the Server SDK's REST +client to supply a SignalWire Markup Language (SWML) document. It describes what happens when +someone answers, such as playing an announcement or starting an AI conversation. The request +returns as soon as the call is queued. SignalWire runs the call logic and sends progress updates +to your configured webhook endpoint. **Keep your code on the call.** The Server SDK's Relay client holds a WebSocket open, so `dial` -waits for the answer and returns a call object you drive directly. The Browser SDK does the same -from the user's device, where `dial` returns a call whose media you attach to the page. +waits for the answer and returns a call object you drive directly. For a dispatcher speaking to +the rider from a web app, the Browser SDK connects the device's microphone and speaker to the call. -The sequence below follows the hand-off flow. Solid arrows are actions on the call, and dashed -arrows are what comes back to your code. +This sequence shows a Calling API or Server SDK REST call with `answered` and `ended` webhooks +enabled, as in the examples below. Solid arrows show the request and phone activity; dashed +arrows show the response and webhooks sent to your code. -Your code sends dial with from, to, and what to run. SignalWire returns the call id with status queued, rings the rider's phone, and sends a ringing status webhook. The rider answers and SignalWire sends an answered webhook, then your SWML runs or your Relay code drives the call. When the rider hangs up, SignalWire sends an ended webhook. +Your code sends a dial request with from, to, and SWML. SignalWire returns a call id with status queued and rings the rider's phone. When the rider answers, SignalWire sends an answered webhook and runs your SWML. When the call finishes, SignalWire sends an ended webhook. @@ -62,35 +65,31 @@ arrows are what comes back to your code. ```mermaid sequenceDiagram - autonumber participant App as Your code participant SW as SignalWire participant Rider as Rider's phone - App->>SW: dial from, to, and what to run + App->>SW: dial: from, to, SWML SW-->>App: call id, status queued SW->>Rider: rings - SW-->>App: status webhook ringing Rider->>SW: answers SW-->>App: status webhook answered - Note over SW,Rider: Your SWML runs,
or your Relay code drives the call - Rider->>SW: hangs up + Note over SW,Rider: Your SWML runs + Note over SW,Rider: Call finishes SW-->>App: status webhook ended ``` -| Surface | Control style | What runs the call | +| API or SDK | Control style | What runs the call | |---|---|---| | [Calling API][calling-api] | Hand off | SWML fetched from your URL or sent inline | | Server SDK, REST client | Hand off | The same SWML, from [Python][sdk-rest-dial-python] or [TypeScript][sdk-rest-dial-ts] | | Server SDK, [Relay client][relay-guide] | Stay on the call | Your Python or TypeScript code over a WebSocket | | [Browser SDK][browser-outbound] | Stay on the call | Your browser code | -| SIP device | Hand off | The call handler on its [SIP credential][sip-credentials] | -A registered SIP phone or PBX dials out through SignalWire without any code of yours. The -[SIP credentials][sip-credentials] page covers that setup, and the rest of this guide covers the -programmatic surfaces. +A registered SIP phone or phone system can also dial out through SignalWire. The +[SIP credentials][sip-credentials] page covers that setup. ## Before you dial @@ -109,12 +108,14 @@ internationally. Outside trial mode, calls to other countries still need ## Place the call -Each tab below places the announcement call with a different surface. They all point at the same -SWML URL and ask for the same two status events, so the [call logic](#choose-what-runs-on-the-call) -and the [confirmation](#confirm-the-call-connected) that follow apply to every one of them. +Choose an example for where your application runs and how you want to control the call. ### Hand SignalWire the call logic +These three examples send the same request. Replace `url` with an endpoint serving the +[announcement or AI agent SWML](#choose-what-runs-on-the-call), and `status_url` with your webhook +endpoint. Each asks for an update when the call is answered and when it ends. + ```bash @@ -196,6 +197,10 @@ parameter list is on the [Calling API reference][calling-api]. ### Keep your code on the call +The Relay examples play the announcement directly from your server. The Browser SDK example +opens a live audio call so the dispatcher can speak to the rider. These examples don't use a +SWML URL or the REST status webhooks. + ```python @@ -267,6 +272,8 @@ import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; // SAT is a Subscriber Access Token your backend issued for this user. const client = new SignalWire(new StaticCredentialProvider({ token: SAT })); +const remoteAudio = document.querySelector("#remote-audio")!; +const hangupButton = document.querySelector("#hangup")!; // Audio only, the default for a phone-style call. const call = await client.dial("+15557654321"); @@ -277,21 +284,20 @@ hangupButton.onclick = () => call.hangup(); -With Relay there is no document to write. [`dial`][relay-dial-python] waits for the answer and -returns a call object, so the announcement is a `play` on that object and the hangup is your call -to make. The `devices` list also takes several numbers to ring in turn or at once. If nobody answers -within `dial_timeout`, which defaults to 120 seconds, `dial` raises `RelayError`. The -[TypeScript client][relay-dial-ts] behaves the same way. +With Relay, `dial` waits for the answer, `play` speaks the announcement, and `hangup` ends the call. +The [Python][relay-dial-python] and [TypeScript][relay-dial-ts] references cover timeouts and +ringing several destinations. -A [Browser SDK][browser-outbound] call is placed from the user's device with the token your backend -issued, so the token has to be allowed to reach phone numbers. The same `dial` reaches rooms, other -users, and SIP endpoints by changing the destination string. +For the browser example, use an HTTPS page with `` and +``. Run the dialing code from a user action, such as a +**Call rider** button, and use a token allowed to reach phone numbers. The +[Browser SDK outbound guide][browser-outbound] includes a complete page with media and call controls. ## Choose what runs on the call -On the hand-off surfaces, the SWML behind `url` is what the person you call hears. SignalWire -requests it when the call is created, with `POST` unless you set `url_method`, and runs whatever it -returns. Serve a spoken announcement, or an AI agent that can hold a conversation. +For the Calling API and Server SDK's REST client, the SWML behind `url` controls what the rider +hears after answering. Serve one of these documents at that URL to choose between a spoken +announcement and an AI conversation. You can change the call logic without changing how you dial. @@ -314,28 +320,21 @@ sections: prompt: text: | You are calling to tell the rider their driver is about five minutes away. - Answer questions about the pickup. If the rider says they no longer need the - ride, or asks not to be contacted again, call cancel_pickup before saying - anything else. - SWAIG: - functions: - - function: cancel_pickup - description: Cancel the pickup and record that the rider does not want further calls - parameters: - type: object - properties: {} - web_hook_url: https://example.com/swaig/cancel-pickup + Answer questions using that estimate. You cannot change bookings or + contact preferences. If asked, explain that the rider needs to contact + Bayview Taxi to make those changes. Do not claim to have made a change. ``` -An AI agent is an outbound call whose SWML runs the [`ai` method][swml-ai]. The rider can ask how -far away the driver is or say they no longer need the ride, and the agent answers or acts. The -`static_greeting` plays in full before the agent's first turn, so the AI disclosure is heard even -if the rider starts talking. The `cancel_pickup` function calls your server, which cancels the ride -in your dispatch system and records the opt-out before the agent confirms anything aloud. To serve -the agent from your own code instead of a static document, point `url` at a Server SDK agent as in -the [AI quickstart][ai-quickstart]. +The [`ai` method][swml-ai] turns the announcement into a conversation. This example only answers +questions about the arrival estimate. With `static_greeting_no_barge` enabled, the greeting plays +in full before the agent's first turn, even if the rider starts talking. + +To let the agent change a booking or record a request to stop future calls, connect it to your +backend with [tool calling][tool-calling]. Treat those as separate actions: stopping future calls +doesn't cancel the current ride. The [AI quickstart][ai-quickstart] shows how to serve an agent +from your own code. An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). @@ -374,19 +373,19 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ }' ``` -`custom_variables` takes up to 20 string pairs, and the same pairs are sent to your endpoint when -SignalWire fetches SWML from `url`. Two more parameters shape most other calls. `caller_id` shows a -different number of yours to the person you call. `timeout` sets how many seconds to ring, from 1 -to 600. +The same variables are available when you serve SWML from `url`, so one document can personalize +many calls. The [Calling API reference][calling-api] covers variable limits and options such as +caller ID and ring time. ## Confirm the call connected -On the hand-off surfaces, a `200` response with a call `id` means SignalWire accepted the request, -not that anyone answered. The `answered` event at your `status_url` is the confirmation, followed by -`ended` when the call finishes. If `ended` arrives without `answered`, the destination didn't pick -up or rejected the call. With Relay, a returned call object is the confirmation and `RelayError` is -the failure. In the browser, `status$` reaching `connected` is the confirmation, and a `dial()` that -rejects with `CallCreateError` means the token isn't allowed to reach that destination. +For REST calls, a `200` response with a call `id` means SignalWire accepted the request. Listen +for `answered` at your `status_url` to confirm the destination picked up, and `ended` to know +the call finished. + +With Relay, `dial` returns a call object once the destination answers, or raises `RelayError` if +the dial fails. In the browser, watch for `status$` to reach `connected`. If `dial()` rejects, +check that the token is allowed to reach the destination. The likeliest failure is a `422` before the call is placed, with an error code such as `not_purchased_or_verified`. That means the `from` or `caller_id` number isn't purchased in this From 190c92913e86c8c6f8df74549e99c32281f4eb96 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 11:55:32 -0400 Subject: [PATCH 004/103] docs: separate server and browser outbound calling examples --- .../pages/calling/voice/outbound-calling.mdx | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index ea396f8a6c..13fe3f7bb4 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -106,9 +106,11 @@ Two account settings decide whether the call is allowed at all. A project in internationally. Outside trial mode, calls to other countries still need [international dialing enabled][international] for your Space. -## Place the call +## Call from a server -Choose an example for where your application runs and how you want to control the call. +From your backend, use the Calling API or the Server SDK's REST client to supply call logic, +or the Server SDK's Relay client to control the live call. Both approaches can deliver the +announcement to the rider. ### Hand SignalWire the call logic @@ -197,9 +199,8 @@ parameter list is on the [Calling API reference][calling-api]. ### Keep your code on the call -The Relay examples play the announcement directly from your server. The Browser SDK example -opens a live audio call so the dispatcher can speak to the rider. These examples don't use a -SWML URL or the REST status webhooks. +These Relay examples play the announcement directly from your server. Your code waits for the +rider to answer, speaks the message, and ends the call. @@ -266,7 +267,22 @@ await call.hangup(); await client.disconnect(); ``` - + + +With Relay, `dial` waits for the answer, `play` speaks the announcement, and `hangup` ends the call. +The [Python][relay-dial-python] and [TypeScript][relay-dial-ts] references cover timeouts and +ringing several destinations. + +## Call from a browser + +The Browser SDK opens a live audio call so the dispatcher can speak to the rider from a web app. +It uses a Subscriber Access Token issued by your backend and connects the device's microphone +and speaker to the call. + +Use an HTTPS page with `` and +``. Run the dialing code from a user action, such as a +**Call rider** button, and use a token allowed to reach phone numbers. + ```typescript import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; @@ -281,17 +297,9 @@ call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); hangupButton.onclick = () => call.hangup(); ``` - - - -With Relay, `dial` waits for the answer, `play` speaks the announcement, and `hangup` ends the call. -The [Python][relay-dial-python] and [TypeScript][relay-dial-ts] references cover timeouts and -ringing several destinations. -For the browser example, use an HTTPS page with `` and -``. Run the dialing code from a user action, such as a -**Call rider** button, and use a token allowed to reach phone numbers. The -[Browser SDK outbound guide][browser-outbound] includes a complete page with media and call controls. +The [Browser SDK outbound guide][browser-outbound] includes a complete page with media and call +controls. ## Choose what runs on the call From 7d5d0c01ee15afd2d32de6cf3557cbb568ebbeb5 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 12:30:50 -0400 Subject: [PATCH 005/103] docs: simplify outbound calling guide and link it from the Voice overview Reduce the up-front load on a first-time reader: - cut the duplicated framing so the intro, the concept prose, and the API table each say one thing instead of three - move the announcement SWML next to the dial request it feeds, removing the forward jump to a later section - collapse the inline swml and custom_variables path into an accordion - move the trial-mode and international constraint into a Note - drop the 422 paragraph that repeated the first troubleshooting accordion - retitle sections to name the outcome rather than the pattern Add a "Place an outbound call" card to the Voice overview's Get started grid. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 166 ++++++++---------- .../platform/pages/calling/voice/overview.mdx | 3 + 2 files changed, 81 insertions(+), 88 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 13fe3f7bb4..c96b980203 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -31,29 +31,23 @@ max-toc-depth: 3 [tcpa]: /docs/platform/compliance/tcpa An outbound call takes one request: the number to call, the SignalWire number it comes from, and -what should happen when someone answers. SignalWire rings the destination and, when the call -connects, either runs the call logic you supplied or hands the live call back to your code. +what should happen when someone answers. Bayview Taxi needs to tell a rider their driver is on the way. The examples show an automated announcement, an AI conversation, and a dispatcher calling from a browser. ## How an outbound call works -Your application can supply instructions for SignalWire to run or control the call as it happens. +The request is the same everywhere — `dial`, with a `from`, a `to`, and something to run. What +differs between the APIs is who controls the call once the rider answers. -**Hand SignalWire the call logic.** Use the [Calling API][calling-api] or the Server SDK's REST -client to supply a SignalWire Markup Language (SWML) document. It describes what happens when -someone answers, such as playing an announcement or starting an AI conversation. The request -returns as soon as the call is queued. SignalWire runs the call logic and sends progress updates -to your configured webhook endpoint. +Hand the call logic to SignalWire as a SignalWire Markup Language (SWML) document and the request +returns as soon as the call is queued. SignalWire places the call, runs your document when someone +answers, and posts progress to your webhook endpoint. The alternative is to keep your own code on +the call over an open connection and drive each step yourself as it happens. -**Keep your code on the call.** The Server SDK's Relay client holds a WebSocket open, so `dial` -waits for the answer and returns a call object you drive directly. For a dispatcher speaking to -the rider from a web app, the Browser SDK connects the device's microphone and speaker to the call. - -This sequence shows a Calling API or Server SDK REST call with `answered` and `ended` webhooks -enabled, as in the examples below. Solid arrows show the request and phone activity; dashed -arrows show the response and webhooks sent to your code. +The sequence below traces a hand-off call with `answered` and `ended` webhooks enabled. Dashed +arrows are what SignalWire sends back to your code. @@ -81,12 +75,12 @@ sequenceDiagram -| API or SDK | Control style | What runs the call | +| API or SDK | Who controls the call | What runs it | |---|---|---| -| [Calling API][calling-api] | Hand off | SWML fetched from your URL or sent inline | -| Server SDK, REST client | Hand off | The same SWML, from [Python][sdk-rest-dial-python] or [TypeScript][sdk-rest-dial-ts] | -| Server SDK, [Relay client][relay-guide] | Stay on the call | Your Python or TypeScript code over a WebSocket | -| [Browser SDK][browser-outbound] | Stay on the call | Your browser code | +| [Calling API][calling-api] | SignalWire | SWML fetched from your URL or sent inline | +| Server SDK, REST client | SignalWire | The same SWML, from [Python][sdk-rest-dial-python] or [TypeScript][sdk-rest-dial-ts] | +| Server SDK, [Relay client][relay-guide] | Your server | Your Python or TypeScript, over an open WebSocket | +| [Browser SDK][browser-outbound] | Your browser | Your JavaScript, using the device's microphone and speaker | A registered SIP phone or phone system can also dial out through SignalWire. The [SIP credentials][sip-credentials] page covers that setup. @@ -101,22 +95,30 @@ You also need your Project ID and an API token with voice permissions from the [API credentials][api-credentials] page of your Dashboard. Browser calls use a [Subscriber Access Token][browser-auth] your backend issues for the user instead. -Two account settings decide whether the call is allowed at all. A project in -[trial mode][trial-mode] can only call purchased and verified numbers and can't call -internationally. Outside trial mode, calls to other countries still need + +A project in [trial mode][trial-mode] can only call purchased and verified numbers, and can't call +internationally at all. Outside trial mode, calls to other countries still need [international dialing enabled][international] for your Space. + ## Call from a server -From your backend, use the Calling API or the Server SDK's REST client to supply call logic, -or the Server SDK's Relay client to control the live call. Both approaches can deliver the -announcement to the rider. +From your backend you can hand the call logic to SignalWire, or stay on the call and drive it +yourself. Both deliver the same announcement to the rider. -### Hand SignalWire the call logic +### Let SignalWire run the call -These three examples send the same request. Replace `url` with an endpoint serving the -[announcement or AI agent SWML](#choose-what-runs-on-the-call), and `status_url` with your webhook -endpoint. Each asks for an update when the call is answered and when it ends. +Serve a SWML document at a URL SignalWire can reach. This one plays the announcement: + +```yaml +version: 1.0.0 +sections: + main: + - play: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." +``` + +Then point a `dial` request at that URL. These three examples send the same request, each asking +for an update when the call is answered and when it ends. @@ -193,11 +195,46 @@ call before anyone answers: ``` Keep the `id`. Every other Calling API command, such as ending or transferring the call, takes it. + Progress arrives at `status_url` as webhooks for the events you list in `status_events`: `created`, -`ringing`, `answered`, and `ended`. If you omit `status_events`, you get `ended` only. The full +`ringing`, `answered`, and `ended`. Omit `status_events` and you get `ended` only. The full parameter list is on the [Calling API reference][calling-api]. -### Keep your code on the call + + +You don't have to host the document. Send it in the `swml` field instead of `url`, as a JSON +object rather than a string of escaped JSON. To make the logic specific to one call, attach your +own values in `custom_variables` and read them in the SWML as `${envs.}` using +[SWML expressions][swml-expressions]. + +```bash +curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ + -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "+15551234567", + "to": "+15557654321", + "custom_variables": { "driver_name": "Sam", "eta_minutes": "5" }, + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { "play": "say:Hi, this is Bayview Taxi. ${envs.driver_name} is on the way and will arrive in about ${envs.eta_minutes} minutes." } + ] + } + } + } + }' +``` + +The same variables are available when you serve SWML from `url`, so one document can personalize +many calls. + + + +### Stay on the call from your code These Relay examples play the announcement directly from your server. Your code waits for the rider to answer, speaks the message, and ends the call. @@ -301,22 +338,12 @@ hangupButton.onclick = () => call.hangup(); The [Browser SDK outbound guide][browser-outbound] includes a complete page with media and call controls. -## Choose what runs on the call +## Run an AI agent on the call -For the Calling API and Server SDK's REST client, the SWML behind `url` controls what the rider -hears after answering. Serve one of these documents at that URL to choose between a spoken -announcement and an AI conversation. You can change the call logic without changing how you dial. +On calls where SignalWire runs the logic, swapping the SWML document changes what the rider hears +without changing how you dial. Replace the `play` method with the [`ai` method][swml-ai] to turn +the announcement into a conversation: - - -```yaml -version: 1.0.0 -sections: - main: - - play: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." -``` - - ```yaml version: 1.0.0 sections: @@ -332,12 +359,9 @@ sections: contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change. ``` - - -The [`ai` method][swml-ai] turns the announcement into a conversation. This example only answers -questions about the arrival estimate. With `static_greeting_no_barge` enabled, the greeting plays -in full before the agent's first turn, even if the rider starts talking. +This agent only answers questions about the arrival estimate. With `static_greeting_no_barge` +enabled, the greeting plays in full before the agent's first turn, even if the rider starts talking. To let the agent change a booking or record a request to stop future calls, connect it to your backend with [tool calling][tool-calling]. Treat those as separate actions: stopping future calls @@ -352,39 +376,6 @@ the compliance section of [AI best practices][ai-best-practices] cover each obli technical guidance, not legal advice. -### Send the logic inline and carry data onto the call - -You don't have to host the document. Send it in the `swml` field instead of `url`, as a JSON -object rather than a string of escaped JSON. To make the logic specific to one call, attach your -own values in `custom_variables` and read them in the SWML as `${envs.}` using -[SWML expressions][swml-expressions]. - -```bash -curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ - -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "command": "dial", - "params": { - "from": "+15551234567", - "to": "+15557654321", - "custom_variables": { "driver_name": "Sam", "eta_minutes": "5" }, - "swml": { - "version": "1.0.0", - "sections": { - "main": [ - { "play": "say:Hi, this is Bayview Taxi. ${envs.driver_name} is on the way and will arrive in about ${envs.eta_minutes} minutes." } - ] - } - } - } - }' -``` - -The same variables are available when you serve SWML from `url`, so one document can personalize -many calls. The [Calling API reference][calling-api] covers variable limits and options such as -caller ID and ring time. - ## Confirm the call connected For REST calls, a `200` response with a call `id` means SignalWire accepted the request. Listen @@ -395,12 +386,11 @@ With Relay, `dial` returns a call object once the destination answers, or raises the dial fails. In the browser, watch for `status$` to reach `connected`. If `dial()` rejects, check that the token is allowed to reach the destination. -The likeliest failure is a `422` before the call is placed, with an error code such as -`not_purchased_or_verified`. That means the `from` or `caller_id` number isn't purchased in this -project or verified as a caller ID. The next section covers the other common causes. - ## Troubleshoot outbound calls +The likeliest failure is a `422` before the call is ever placed, usually because the `from` number +isn't one you own. + diff --git a/fern/products/platform/pages/calling/voice/overview.mdx b/fern/products/platform/pages/calling/voice/overview.mdx index 66eb307434..10c58c1a15 100644 --- a/fern/products/platform/pages/calling/voice/overview.mdx +++ b/fern/products/platform/pages/calling/voice/overview.mdx @@ -21,6 +21,9 @@ Whether building a UCaaS solution, modernizing a legacy IVR, augmenting CX with The fundamentals of your first calling app + + Dial from your backend or the browser, and choose what runs when someone answers + Get started with our Compatibility API From af1e61b68730ecbc4279e5b3d598f36b206c8af6 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 14:57:05 -0400 Subject: [PATCH 006/103] docs: lead outbound calling guide with server and browser paths --- .../pages/calling/voice/outbound-calling.mdx | 215 +++++++++--------- 1 file changed, 106 insertions(+), 109 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index c96b980203..70884a0044 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -1,7 +1,7 @@ --- title: Outbound calling slug: /voice/outbound-calling -description: Understand how outbound calls work, place a call with the Calling API or SDKs, and choose how to control it when someone answers. +description: Choose server-side automation or client-side browser calling, then place an outbound call with the appropriate API or SDK. max-toc-depth: 3 --- @@ -30,24 +30,54 @@ max-toc-depth: 3 [ai-best-practices]: /docs/platform/ai/best-practices [tcpa]: /docs/platform/compliance/tcpa -An outbound call takes one request: the number to call, the SignalWire number it comes from, and -what should happen when someone answers. +An outbound call is a call your application places to someone else's phone. Bayview Taxi might +send an automated driver-arrival announcement or let a dispatcher speak to the rider from a web app. -Bayview Taxi needs to tell a rider their driver is on the way. The examples show an automated -announcement, an AI conversation, and a dispatcher calling from a browser. +## Choose server-side or client-side calling -## How an outbound call works +Start with what your application needs to do: -The request is the same everywhere — `dial`, with a `from`, a `to`, and something to run. What -differs between the APIs is who controls the call once the rider answers. +| You want to… | Where your calling code runs | Start here | +|---|---|---| +| Play an announcement, run an AI agent, or automate a call | Your backend, using the Calling API or Server SDK | [Call from a server](#call-from-a-server) | +| Let a person talk through a web app's microphone and speaker | The user's browser, using the Browser SDK | [Call from a browser](#call-from-a-browser) | + +A button in a web app can also ask your backend to start an automated call. That uses the +server-side path; use the Browser SDK when the person in the browser will participate in the call. + +A registered SIP phone or phone system can also dial out through SignalWire. The +[SIP credentials][sip-credentials] page covers that setup. + +## Before you dial -Hand the call logic to SignalWire as a SignalWire Markup Language (SWML) document and the request -returns as soon as the call is queued. SignalWire places the call, runs your document when someone -answers, and posts progress to your webhook endpoint. The alternative is to keep your own code on -the call over an open connection and drive each step yourself as it happens. +You need a [phone number][phone-numbers] purchased in your project, or a +[verified caller ID][caller-id], to call from. Any number you pass as `from` or `caller_id` on a +call to the public telephone network must be one of those, in E.164 format such as `+15551234567`. -The sequence below traces a hand-off call with `answered` and `ended` webhooks enabled. Dashed -arrows are what SignalWire sends back to your code. + +A project in [trial mode][trial-mode] can only call purchased and verified numbers, and can't call +internationally at all. Outside trial mode, calls to other countries still need +[international dialing enabled][international] for your Space. + + +## Call from a server + +Run these examples on your backend. You need your Project ID and an API token with voice +permissions from the [API credentials][api-credentials] page of your Dashboard. Keep the API +token on the server. + +There are two ways to control a server-initiated call: + +- **Calling API or Server SDK REST client:** Send SignalWire a SignalWire Markup Language (SWML) + document to run when the rider answers. Start here for an announcement or AI agent. +- **Server SDK Relay client:** Keep a WebSocket connection open and control the call from your + Python or TypeScript code. See [Control the call with Relay](#control-the-call-with-relay). + +### Let SignalWire run the call + +Your server sends the dial request. SignalWire queues the call, runs your SWML when someone +answers, and sends progress to your server's webhook endpoint. This diagram shows the flow with +`answered` and `ended` webhooks enabled. @@ -75,39 +105,6 @@ sequenceDiagram -| API or SDK | Who controls the call | What runs it | -|---|---|---| -| [Calling API][calling-api] | SignalWire | SWML fetched from your URL or sent inline | -| Server SDK, REST client | SignalWire | The same SWML, from [Python][sdk-rest-dial-python] or [TypeScript][sdk-rest-dial-ts] | -| Server SDK, [Relay client][relay-guide] | Your server | Your Python or TypeScript, over an open WebSocket | -| [Browser SDK][browser-outbound] | Your browser | Your JavaScript, using the device's microphone and speaker | - -A registered SIP phone or phone system can also dial out through SignalWire. The -[SIP credentials][sip-credentials] page covers that setup. - -## Before you dial - -You need a [phone number][phone-numbers] purchased in your project, or a -[verified caller ID][caller-id], to call from. Any number you pass as `from` or `caller_id` on a -call to the public telephone network must be one of those, in E.164 format such as `+15551234567`. - -You also need your Project ID and an API token with voice permissions from the -[API credentials][api-credentials] page of your Dashboard. Browser calls use a -[Subscriber Access Token][browser-auth] your backend issues for the user instead. - - -A project in [trial mode][trial-mode] can only call purchased and verified numbers, and can't call -internationally at all. Outside trial mode, calls to other countries still need -[international dialing enabled][international] for your Space. - - -## Call from a server - -From your backend you can hand the call logic to SignalWire, or stay on the call and drive it -yourself. Both deliver the same announcement to the rider. - -### Let SignalWire run the call - Serve a SWML document at a URL SignalWire can reach. This one plays the announcement: ```yaml @@ -180,8 +177,9 @@ console.log(call.id); -All three send the same `dial` command, so their parameters match. The response returns the new -call before anyone answers: +The Calling API and Server SDK REST clients ([Python][sdk-rest-dial-python] or +[TypeScript][sdk-rest-dial-ts]) send the same `dial` command, so their parameters match. +The response returns the new call before anyone answers: ```json { @@ -194,11 +192,13 @@ call before anyone answers: } ``` -Keep the `id`. Every other Calling API command, such as ending or transferring the call, takes it. +A `200` response with a call `id` means SignalWire accepted the request. It does not mean the +rider answered. Keep the `id` to end or transfer the call with other Calling API commands. Progress arrives at `status_url` as webhooks for the events you list in `status_events`: `created`, `ringing`, `answered`, and `ended`. Omit `status_events` and you get `ended` only. The full -parameter list is on the [Calling API reference][calling-api]. +parameter list is on the [Calling API reference][calling-api]. Listen for `answered` to confirm +the rider picked up and `ended` to know the call finished. @@ -234,7 +234,45 @@ many calls. -### Stay on the call from your code +### Run an AI agent with SWML + +On calls where SignalWire runs the logic, swapping the SWML document changes what the rider hears +without changing how you dial. Replace the `play` method with the [`ai` method][swml-ai] to turn +the announcement into a conversation: + +```yaml +version: 1.0.0 +sections: + main: + - ai: + params: + static_greeting: "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice." + static_greeting_no_barge: true + prompt: + text: | + You are calling to tell the rider their driver is about five minutes away. + Answer questions using that estimate. You cannot change bookings or + contact preferences. If asked, explain that the rider needs to contact + Bayview Taxi to make those changes. Do not claim to have made a change. +``` + +This agent only answers questions about the arrival estimate. With `static_greeting_no_barge` +enabled, the greeting plays in full before the agent's first turn, even if the rider starts talking. + +To let the agent change a booking or record a request to stop future calls, connect it to your +backend with [tool calling][tool-calling]. Treat those as separate actions: stopping future calls +doesn't cancel the current ride. The [AI quickstart][ai-quickstart] shows how to serve an agent +from your own code. + + +An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). +Check consent, your do-not-call list, and the local calling hours in your code before you send the +`dial` request, because once the call is placed it has already happened. The [TCPA guide][tcpa] and +the compliance section of [AI best practices][ai-best-practices] cover each obligation. This is +technical guidance, not legal advice. + + +### Control the call with Relay These Relay examples play the announcement directly from your server. Your code waits for the rider to answer, speaks the message, and ends the call. @@ -306,19 +344,24 @@ await client.disconnect(); -With Relay, `dial` waits for the answer, `play` speaks the announcement, and `hangup` ends the call. +With Relay, `dial` returns a call object when the rider answers, or raises `RelayError` if dialing +fails. Then `play` speaks the announcement and `hangup` ends the call. The [Python][relay-dial-python] and [TypeScript][relay-dial-ts] references cover timeouts and ringing several destinations. ## Call from a browser -The Browser SDK opens a live audio call so the dispatcher can speak to the rider from a web app. -It uses a Subscriber Access Token issued by your backend and connects the device's microphone -and speaker to the call. +Run the Browser SDK in your frontend so the dispatcher can speak to the rider using the device's +microphone and speaker. This path has two parts: + +1. **Backend:** Authenticate the user and issue a [Subscriber Access Token (SAT)][browser-auth] + that allows them to call phone numbers. Your Project API Token stays on the backend. +2. **Browser:** Use that SAT with `@signalwire/js` to dial the rider and handle live audio and + call controls. The example below runs here. Use an HTTPS page with `` and ``. Run the dialing code from a user action, such as a -**Call rider** button, and use a token allowed to reach phone numbers. +**Call rider** button. ```typescript import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; @@ -335,61 +378,15 @@ call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); hangupButton.onclick = () => call.hangup(); ``` -The [Browser SDK outbound guide][browser-outbound] includes a complete page with media and call -controls. - -## Run an AI agent on the call - -On calls where SignalWire runs the logic, swapping the SWML document changes what the rider hears -without changing how you dial. Replace the `play` method with the [`ai` method][swml-ai] to turn -the announcement into a conversation: - -```yaml -version: 1.0.0 -sections: - main: - - ai: - params: - static_greeting: "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice." - static_greeting_no_barge: true - prompt: - text: | - You are calling to tell the rider their driver is about five minutes away. - Answer questions using that estimate. You cannot change bookings or - contact preferences. If asked, explain that the rider needs to contact - Bayview Taxi to make those changes. Do not claim to have made a change. -``` - -This agent only answers questions about the arrival estimate. With `static_greeting_no_barge` -enabled, the greeting plays in full before the agent's first turn, even if the rider starts talking. - -To let the agent change a booking or record a request to stop future calls, connect it to your -backend with [tool calling][tool-calling]. Treat those as separate actions: stopping future calls -doesn't cancel the current ride. The [AI quickstart][ai-quickstart] shows how to serve an agent -from your own code. - - -An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). -Check consent, your do-not-call list, and the local calling hours in your code before you send the -`dial` request, because once the call is placed it has already happened. The [TCPA guide][tcpa] and -the compliance section of [AI best practices][ai-best-practices] cover each obligation. This is -technical guidance, not legal advice. - - -## Confirm the call connected - -For REST calls, a `200` response with a call `id` means SignalWire accepted the request. Listen -for `answered` at your `status_url` to confirm the destination picked up, and `ended` to know -the call finished. - -With Relay, `dial` returns a call object once the destination answers, or raises `RelayError` if -the dial fails. In the browser, watch for `status$` to reach `connected`. If `dial()` rejects, -check that the token is allowed to reach the destination. +Watch for `status$` to reach `connected` to confirm the browser call connected. If `dial()` +rejects, check that the token is allowed to reach the destination. The +[Browser SDK outbound guide][browser-outbound] includes a complete page with media and call controls. ## Troubleshoot outbound calls -The likeliest failure is a `422` before the call is ever placed, usually because the `from` number -isn't one you own. +For Calling API and Server SDK REST requests, a `422` means the call was rejected before it was +placed. Check the error code first. For browser setup and token issues, see +[Browser SDK authentication][browser-auth]. From c3fb5c4154c4cada7450309d92d8df65edd52644 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 15:12:17 -0400 Subject: [PATCH 007/103] docs: organize outbound calling concepts before examples --- .../pages/calling/voice/outbound-calling.mdx | 309 ++++++++++-------- 1 file changed, 179 insertions(+), 130 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 70884a0044..0e02a795c1 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -1,7 +1,7 @@ --- title: Outbound calling slug: /voice/outbound-calling -description: Choose server-side automation or client-side browser calling, then place an outbound call with the appropriate API or SDK. +description: Learn how REST requests, inbound call forwarding, and SDK requests start outbound calls, with server and browser examples. max-toc-depth: 3 --- @@ -29,30 +29,71 @@ max-toc-depth: 3 [tool-calling]: /docs/platform/ai/tool-calling [ai-best-practices]: /docs/platform/ai/best-practices [tcpa]: /docs/platform/compliance/tcpa +[swml-connect]: /docs/swml/reference/calling/connect +[forward-calls]: /docs/swml/guides/forward-calls +[forward-node]: /docs/call-flow-builder/reference/forward-to-phone -An outbound call is a call your application places to someone else's phone. Bayview Taxi might -send an automated driver-arrival announcement or let a dispatcher speak to the rider from a web app. +An outbound call is a call SignalWire places to a phone number or SIP endpoint. It can start +with a REST API request, an inbound call being forwarded, or a request from an SDK. -## Choose server-side or client-side calling +## How outbound calls start -Start with what your application needs to do: +### REST API request -| You want to… | Where your calling code runs | Start here | -|---|---|---| -| Play an announcement, run an AI agent, or automate a call | Your backend, using the Calling API or Server SDK | [Call from a server](#call-from-a-server) | -| Let a person talk through a web app's microphone and speaker | The user's browser, using the Browser SDK | [Call from a browser](#call-from-a-browser) | +Your backend sends a `dial` command to the [Calling API][calling-api] with a destination, +a caller ID, and instructions for the call. Use this for scheduled calls, notifications, or other +automation. The [REST example](#place-a-call-with-rest) shows the request directly with curl +and through the Server SDK's REST clients. -A button in a web app can also ask your backend to start an automated call. That uses the -server-side path; use the Browser SDK when the person in the browser will participate in the call. +### Forwarding an inbound call -A registered SIP phone or phone system can also dial out through SignalWire. The -[SIP credentials][sip-credentials] page covers that setup. +Someone calls your SignalWire number, and its call flow dials another destination and connects +the two calls. The call to the second destination is the outbound part of that conversation. + +Configure this with SWML's [`connect` method][swml-connect] or Call Flow Builder's +[Forward to Phone node][forward-node]. The incoming call triggers the outbound call through that +configuration. See the [forwarding example](#forward-an-inbound-call). + +### SDK request + +Your application calls an SDK method to dial a destination. The SDK you use depends on where +your code runs: + +- **Server SDK:** Your backend uses the REST client to send a Calling API request, or the + [Relay client][relay-guide] to dial and control the call over a WebSocket connection. +- **Browser SDK:** Your frontend dials from the user's device and connects its microphone and + speaker to the call. Use this when a person will speak through your web app. + +A button in a web app can also ask your backend to start an automated call. In that case, the +backend makes the Calling API or Server SDK request. The Browser SDK is for participating in the +call from the browser. + +A registered SIP phone or phone system can also dial out through SignalWire. See +[SIP credentials][sip-credentials] for that setup. + +## What happens when someone answers + +The call's instructions determine what the recipient hears: + +- **REST-initiated calls** run a SignalWire Markup Language (SWML) document. Send it inline in + `swml`, or provide a `url` where SignalWire can fetch it. The document can play an announcement, + run an AI agent, or connect the recipient to another destination. +- **Forwarded calls** connect the recipient to the original caller. +- **Server SDK Relay calls** follow commands from your running backend, such as playing audio + or connecting the call. +- **Browser SDK calls** carry live audio between the browser user and the recipient. ## Before you dial -You need a [phone number][phone-numbers] purchased in your project, or a -[verified caller ID][caller-id], to call from. Any number you pass as `from` or `caller_id` on a -call to the public telephone network must be one of those, in E.164 format such as `+15551234567`. +### Phone numbers and caller ID + +Use E.164 format for phone numbers, such as `+15551234567`. For Calling API requests, use a +[phone number][phone-numbers] purchased in your project or a [verified caller ID][caller-id] as +the calling number. Forwarding has its own caller ID configuration; the +[forwarding guide][forward-calls] shows how to preserve the original caller's number. + +For inbound forwarding, assign your SWML script or Call Flow Builder flow to the SignalWire +number receiving the call. The [forwarding guide][forward-calls] walks through this setup. A project in [trial mode][trial-mode] can only call purchased and verified numbers, and can't call @@ -60,24 +101,84 @@ internationally at all. Outside trial mode, calls to other countries still need [international dialing enabled][international] for your Space. -## Call from a server +### Server and browser credentials + +For Calling API and Server SDK requests, use your Project ID and an API token with voice +permissions from the Dashboard's [API credentials][api-credentials] page. Keep the API token on +your backend. + +For Browser SDK calls, your backend authenticates the user and issues a +[Subscriber Access Token (SAT)][browser-auth] that allows them to call the destination. Your +frontend uses that SAT with `@signalwire/js`. The Project API Token stays on the backend. + +## Track the call's progress + +| How you placed the call | How to confirm it connected | +|---|---| +| Calling API or Server SDK REST client | A `200` response with a call `id` means the request was accepted. Listen for an `answered` webhook to confirm pickup. | +| Inbound forwarding with SWML | Use `connect`'s `call_state_url` and `call_state_events` to receive outbound call state webhooks. See [the reference][swml-connect]. | +| Server SDK Relay client | `dial` returns a call object when the destination answers, or raises `RelayError` if dialing fails. | +| Browser SDK | Watch for `status$` to reach `connected`. If `dial()` rejects, check the destination and token permissions. | + +For Calling API requests, send a `status_url` and choose `status_events`: `created`, `ringing`, +`answered`, or `ended`. If you omit `status_events`, you receive `ended` only. Forwarding uses the +callback settings of your SWML method or Call Flow Builder node. + +## Troubleshoot outbound calls + +For Calling API and Server SDK REST requests, a `422` means the call was rejected before it was +placed. Check the error code first. For browser setup and token issues, see +[Browser SDK authentication][browser-auth]. -Run these examples on your backend. You need your Project ID and an API token with voice -permissions from the [API credentials][api-credentials] page of your Dashboard. Keep the API -token on the server. + + -There are two ways to control a server-initiated call: +A `422` response carries an [error code][error-codes] that names the problem. `not_purchased_or_verified` +means the `from` number is not purchased or verified for your project. `not_valid_for_caller_id` +means `from` or `caller_id` isn't an E.164 number, caller ID string, or SIP URI. `invalid_destination_number` and +`destination_number_not_supported` point at `to`. `insufficient_balance` means the project can't pay +for the call. A `dial` request that has neither `url` nor `swml` is also rejected. -- **Calling API or Server SDK REST client:** Send SignalWire a SignalWire Markup Language (SWML) - document to run when the rider answers. Start here for an announcement or AI agent. -- **Server SDK Relay client:** Keep a WebSocket connection open and control the call from your - Python or TypeScript code. See [Control the call with Relay](#control-the-call-with-relay). + + -### Let SignalWire run the call +Check the account limits first. A project in [trial mode][trial-mode] can only reach purchased and +verified numbers. International destinations fail until you [enable international +dialing][international]. If those are fine, a `timeout` shorter than the destination's ring time +ends the call early, and a `max_price_per_minute` below the route's price rejects it before it +rings. -Your server sends the dial request. SignalWire queues the call, runs your SWML when someone -answers, and sends progress to your server's webhook endpoint. This diagram shows the flow with -`answered` and `ended` webhooks enabled. + + + +For Calling API requests, check `from` and any `caller_id` override against your purchased or +verified numbers. For forwarded calls, check the caller ID configured in your forwarding flow. +Set the caller name with the [Caller ID and CNAM][caller-id] guide. A "Spam likely" label on your +number is a reputation problem rather than a configuration one. Start with the +[spam labels][spam-labels] guide and confirm your calls carry [STIR/SHAKEN][stir-shaken] +attestation. + + + + +SignalWire requests `url` with `POST` unless you set `url_method`, and the endpoint has to return a +SWML document. Set `fallback_url` so a failed fetch still gets instructions, and secure the endpoint +as described in [SWML webhook security][swml-webhook-security]. An inline `swml` value must be a +JSON object, not a string of escaped JSON. + + + + +## Examples + +These examples use Bayview Taxi: an automated driver-arrival announcement, an incoming call +forwarded to a dispatcher, and a dispatcher speaking to a rider from the browser. + +### Place a call with REST + +Run the curl command from a terminal or the SDK code on your backend, using your Project ID, +API token, and Space hostname. This flow uses SWML for the announcement and requests `answered` +and `ended` webhooks: @@ -192,13 +293,8 @@ The response returns the new call before anyone answers: } ``` -A `200` response with a call `id` means SignalWire accepted the request. It does not mean the -rider answered. Keep the `id` to end or transfer the call with other Calling API commands. - -Progress arrives at `status_url` as webhooks for the events you list in `status_events`: `created`, -`ringing`, `answered`, and `ended`. Omit `status_events` and you get `ended` only. The full -parameter list is on the [Calling API reference][calling-api]. Listen for `answered` to confirm -the rider picked up and `ended` to know the call finished. +Keep the `id` to end or transfer the call with other Calling API commands. The `status_url` +endpoint receives the `answered` and `ended` webhooks requested above. @@ -234,47 +330,27 @@ many calls. -### Run an AI agent with SWML +### Forward an inbound call -On calls where SignalWire runs the logic, swapping the SWML document changes what the rider hears -without changing how you dial. Replace the `play` method with the [`ai` method][swml-ai] to turn -the announcement into a conversation: +Assign this SWML script to the SignalWire number that riders call. When a rider calls in, +SignalWire dials the dispatcher's phone and connects them: ```yaml version: 1.0.0 sections: main: - - ai: - params: - static_greeting: "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice." - static_greeting_no_barge: true - prompt: - text: | - You are calling to tell the rider their driver is about five minutes away. - Answer questions using that estimate. You cannot change bookings or - contact preferences. If asked, explain that the rider needs to contact - Bayview Taxi to make those changes. Do not claim to have made a change. + - connect: + from: "+15551234567" + to: "+15557654321" ``` -This agent only answers questions about the arrival estimate. With `static_greeting_no_barge` -enabled, the greeting plays in full before the agent's first turn, even if the rider starts talking. +Replace `from` with your SignalWire number and `to` with the dispatcher's number. The dispatcher +sees your SignalWire number as caller ID. See the [forwarding guide][forward-calls] for assigning +the script and preserving the original caller's number instead. -To let the agent change a booking or record a request to stop future calls, connect it to your -backend with [tool calling][tool-calling]. Treat those as separate actions: stopping future calls -doesn't cancel the current ride. The [AI quickstart][ai-quickstart] shows how to serve an agent -from your own code. +### Place a call with the Server SDK Relay client - -An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). -Check consent, your do-not-call list, and the local calling hours in your code before you send the -`dial` request, because once the call is placed it has already happened. The [TCPA guide][tcpa] and -the compliance section of [AI best practices][ai-best-practices] cover each obligation. This is -technical guidance, not legal advice. - - -### Control the call with Relay - -These Relay examples play the announcement directly from your server. Your code waits for the +Run these Relay examples on your server to play the announcement. Your code waits for the rider to answer, speaks the message, and ends the call. @@ -349,15 +425,11 @@ fails. Then `play` speaks the announcement and `hangup` ends the call. The [Python][relay-dial-python] and [TypeScript][relay-dial-ts] references cover timeouts and ringing several destinations. -## Call from a browser +### Place a call with the Browser SDK -Run the Browser SDK in your frontend so the dispatcher can speak to the rider using the device's -microphone and speaker. This path has two parts: - -1. **Backend:** Authenticate the user and issue a [Subscriber Access Token (SAT)][browser-auth] - that allows them to call phone numbers. Your Project API Token stays on the backend. -2. **Browser:** Use that SAT with `@signalwire/js` to dial the rider and handle live audio and - call controls. The example below runs here. +The dispatcher speaks to the rider through your web app. First have your backend issue a +[Subscriber Access Token][browser-auth] for the dispatcher with permission to dial phone numbers. +Pass that token to the frontend as `SAT`; the following code runs in the browser. Use an HTTPS page with `` and ``. Run the dialing code from a user action, such as a @@ -382,63 +454,40 @@ Watch for `status$` to reach `connected` to confirm the browser call connected. rejects, check that the token is allowed to reach the destination. The [Browser SDK outbound guide][browser-outbound] includes a complete page with media and call controls. -## Troubleshoot outbound calls - -For Calling API and Server SDK REST requests, a `422` means the call was rejected before it was -placed. Check the error code first. For browser setup and token issues, see -[Browser SDK authentication][browser-auth]. - - - - -A `422` response carries an [error code][error-codes] that names the problem. `not_purchased_or_verified` -means the `from` number isn't yours. `not_valid_for_caller_id` means `from` or `caller_id` isn't an -E.164 number, caller ID string, or SIP URI. `invalid_destination_number` and -`destination_number_not_supported` point at `to`. `insufficient_balance` means the project can't pay -for the call. A `dial` request that has neither `url` nor `swml` is also rejected. - - - - -Check the account limits first. A project in [trial mode][trial-mode] can only reach purchased and -verified numbers. International destinations fail until you [enable international -dialing][international]. If those are fine, a `timeout` shorter than the destination's ring time -ends the call early, and a `max_price_per_minute` below the route's price rejects it before it -rings. - - - +### Run an AI agent with SWML -The displayed number is `from`, or `caller_id` when you set it, and both must be numbers you own. -Set the caller name with the [Caller ID and CNAM][caller-id] guide. A "Spam likely" label on your -number is a reputation problem rather than a configuration one. Start with the -[spam labels][spam-labels] guide and confirm your calls carry [STIR/SHAKEN][stir-shaken] -attestation. +To turn the [REST announcement example](#place-a-call-with-rest) into an AI conversation, +serve this SWML at the URL used in that request. It replaces `play` with the [`ai` method][swml-ai]; +the dial request stays the same: - - +```yaml +version: 1.0.0 +sections: + main: + - ai: + params: + static_greeting: "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice." + static_greeting_no_barge: true + prompt: + text: | + You are calling to tell the rider their driver is about five minutes away. + Answer questions using that estimate. You cannot change bookings or + contact preferences. If asked, explain that the rider needs to contact + Bayview Taxi to make those changes. Do not claim to have made a change. +``` -SignalWire requests `url` with `POST` unless you set `url_method`, and the endpoint has to return a -SWML document. Set `fallback_url` so a failed fetch still gets instructions, and secure the endpoint -as described in [SWML webhook security][swml-webhook-security]. An inline `swml` value must be a -JSON object, not a string of escaped JSON. +This agent only answers questions about the arrival estimate. With `static_greeting_no_barge` +enabled, the greeting plays in full before the agent's first turn, even if the rider starts talking. - - +To let the agent change a booking or record a request to stop future calls, connect it to your +backend with [tool calling][tool-calling]. Treat those as separate actions: stopping future calls +doesn't cancel the current ride. The [AI quickstart][ai-quickstart] shows how to serve an agent +from your own code. -## Next steps - - - - Every `dial` parameter, plus the commands that control a call after it's placed. - - - Stay on the call from your server and react to events as they happen. - - - Destinations, media options, and call controls for click-to-call pages. - - - Handle the inbound side and build the SWML your outbound calls run. - - + +An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). +Check consent, your do-not-call list, and the local calling hours in your code before you send the +`dial` request, because once the call is placed it has already happened. The [TCPA guide][tcpa] and +the compliance section of [AI best practices][ai-best-practices] cover each obligation. This is +technical guidance, not legal advice. + From 83c935544d8af6f4a47199581b96128c6061594b Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 15:20:11 -0400 Subject: [PATCH 008/103] docs: explain outbound destinations and API support --- .../pages/calling/voice/outbound-calling.mdx | 66 ++++++++++++++++--- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 0e02a795c1..a1b6f2e175 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -1,7 +1,7 @@ --- title: Outbound calling slug: /voice/outbound-calling -description: Learn how REST requests, inbound call forwarding, and SDK requests start outbound calls, with server and browser examples. +description: Learn how outbound calls start, which destinations each API and SDK supports, and how to place calls from a server or browser. max-toc-depth: 3 --- @@ -32,9 +32,12 @@ max-toc-depth: 3 [swml-connect]: /docs/swml/reference/calling/connect [forward-calls]: /docs/swml/guides/forward-calls [forward-node]: /docs/call-flow-builder/reference/forward-to-phone +[resource-addresses]: /docs/platform/addresses +[resources]: /docs/platform/resources -An outbound call is a call SignalWire places to a phone number or SIP endpoint. It can start -with a REST API request, an inbound call being forwarded, or a request from an SDK. +An outbound call is a call your application or call flow places to a destination: a phone number, +SIP endpoint, or a user or application in SignalWire. It can start with a REST API request, +an inbound call being forwarded, or a request from an SDK. ## How outbound calls start @@ -71,6 +74,48 @@ call from the browser. A registered SIP phone or phone system can also dial out through SignalWire. See [SIP credentials][sip-credentials] for that setup. +## Destinations you can call + +The destination identifies who or what you want to reach. A call can leave SignalWire over the +public telephone network (PSTN) or SIP, or connect directly to a resource in your Space. + +| Destination | Address format | What you reach | +|---|---|---| +| Phone number | E.164, such as `+15557654321` | A mobile or landline phone over the PSTN. | +| SIP endpoint | SIP URI, such as `sip:dispatcher@example.com` | A SIP phone, PBX, or another provider's SIP service. | +| Call Fabric application or room | Resource address, such as `/public/dispatch-agent` | An AI agent, SWML application, or room in your Space. | +| Call Fabric user | Resource address, such as `/private/dispatcher` | A registered user (Subscriber) in your Space. | + +Applications, rooms, and Subscribers are all [Resources][resources]. Use the actual +[address][resource-addresses] configured for the resource, including its `public` or `private` +context. Calling that address directly does not require a destination phone number. + +### Support by API and SDK + +- **Calling API and Server SDK REST clients:** Accept phone numbers, SIP URIs, and Call Fabric + addresses in `to`. See the [Calling API reference][calling-api]. +- **Server SDK Relay client:** Supports `phone` and `sip` devices in its `devices` list. Phone + devices use `params.to_number`; see the [dial reference][relay-dial-python] for the device format. +- **Browser SDK:** Accepts phone numbers, SIP URIs, and Call Fabric addresses as the destination + passed to `client.dial()`. The user's token must allow access to that destination. See the + [Browser SDK outbound guide][browser-outbound]. +- **Inbound forwarding:** SWML's [`connect`][swml-connect] accepts phone numbers, SIP URIs, and + Call Fabric addresses in `to`. Call Flow Builder's [Forward to Phone node][forward-node] + supports phone numbers and SIP endpoints. + + + +The [Calling API][calling-api] also accepts `sips:`, `verto:`, and `client:` addresses in `to`. +For a destination handled by a SWML script, use `to_script` with an inline document or a URL that +returns one; `to` can then be omitted. This is separate from the `url` or `swml` instructions +that handle the call. + +SWML's [`connect` method][swml-connect] also supports queues (`queue:support`) and WebSocket +media streams (`stream:wss://example.com/audio`). These route an existing call to a queue or +stream its media; they use the connection-specific options in the `connect` reference. + + + ## What happens when someone answers The call's instructions determine what the recipient hears: @@ -87,10 +132,13 @@ The call's instructions determine what the recipient hears: ### Phone numbers and caller ID -Use E.164 format for phone numbers, such as `+15551234567`. For Calling API requests, use a -[phone number][phone-numbers] purchased in your project or a [verified caller ID][caller-id] as -the calling number. Forwarding has its own caller ID configuration; the -[forwarding guide][forward-calls] shows how to preserve the original caller's number. +Use E.164 format for phone numbers, such as `+15551234567`. For Calling API requests to the PSTN, +use a [phone number][phone-numbers] purchased in your project or a [verified caller ID][caller-id] +as the calling number. SIP destinations can use a SIP URI as the calling address; see the +[Calling API reference][calling-api] for `from` and `caller_id`. + +Forwarding has its own caller ID configuration; the [forwarding guide][forward-calls] shows how +to preserve the original caller's number. For inbound forwarding, assign your SWML script or Call Flow Builder flow to the SignalWire number receiving the call. The [forwarding guide][forward-calls] walks through this setup. @@ -151,8 +199,8 @@ rings. -For Calling API requests, check `from` and any `caller_id` override against your purchased or -verified numbers. For forwarded calls, check the caller ID configured in your forwarding flow. +For Calling API requests to phone numbers, check `from` and any `caller_id` override against your +purchased or verified numbers. For forwarded calls, check the caller ID configured in your forwarding flow. Set the caller name with the [Caller ID and CNAM][caller-id] guide. A "Spam likely" label on your number is a reputation problem rather than a configuration one. Start with the [spam labels][spam-labels] guide and confirm your calls carry [STIR/SHAKEN][stir-shaken] From a734584fbf120c29c716ff9c248625972809db9d Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 15:29:02 -0400 Subject: [PATCH 009/103] docs: correct specialized outbound destination requirements --- .../pages/calling/voice/outbound-calling.mdx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index a1b6f2e175..84cc0d3e4a 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -105,14 +105,21 @@ context. Calling that address directly does not require a destination phone numb -The [Calling API][calling-api] also accepts `sips:`, `verto:`, and `client:` addresses in `to`. +The [Calling API][calling-api] also accepts secure SIP URIs such as +`sips:dispatcher@example.com`. For an existing Verto registration, use its full address, such as +`verto:dispatcher@YOUR_SPACE.verto.signalwire.com`; the destination must be registered. + For a destination handled by a SWML script, use `to_script` with an inline document or a URL that -returns one; `to` can then be omitted. This is separate from the `url` or `swml` instructions -that handle the call. +returns one; `to` can then be omitted. The request still requires `from` and either `url` or +`swml` for the call-handling instructions. `to_script` supplies the destination's instructions. SWML's [`connect` method][swml-connect] also supports queues (`queue:support`) and WebSocket -media streams (`stream:wss://example.com/audio`). These route an existing call to a queue or -stream its media; they use the connection-specific options in the `connect` reference. +media streams (`stream:wss://example.com/audio`). A queue connection also requires +`transfer_after_bridge`, pointing to SWML to run after the bridge ends. See the complete +[queue example](/docs/swml/reference/calling/connect#connect-to-a-queue-with-transfer-after-bridge). +For media streams, use a WebSocket endpoint that handles the streamed audio; see the +[stream example](/docs/swml/reference/calling/connect#connect-to-a-websocket-stream) for codec +and callback settings. From 99245c5f0dc0b1813fffd5b9ac08042c730bce00 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 15:35:34 -0400 Subject: [PATCH 010/103] docs: compare REST and Relay calling in topic tabs --- .../outbound-call-relay-lifecycle-themed.svg | 85 ++++++ .../pages/calling/voice/outbound-calling.mdx | 277 ++++++++++++------ 2 files changed, 272 insertions(+), 90 deletions(-) create mode 100644 fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg diff --git a/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg b/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg new file mode 100644 index 0000000000..4ee7c8581d --- /dev/null +++ b/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg @@ -0,0 +1,85 @@ + + Outbound call with the Server SDK Relay client + Your server keeps a WebSocket connection open to SignalWire. It sends dial with devices. After the rider answers, the dial outcome event lets the SDK return a call object. Your server sends play, receives playback finished, sends hangup, and receives call ended. + + + + + + + + + + + + + Your server + + + + + SignalWire + + + + + Rider’s phone + + + WebSocket stays open + + dial: devices + + rings + + answers + + + dial answered + + SDK returns Call + + play + + + Announcement plays + + playback finished + + hangup + + call ended + + + diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 84cc0d3e4a..eb43ae1baa 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -41,12 +41,12 @@ an inbound call being forwarded, or a request from an SDK. ## How outbound calls start -### REST API request +### Server API or SDK request -Your backend sends a `dial` command to the [Calling API][calling-api] with a destination, -a caller ID, and instructions for the call. Use this for scheduled calls, notifications, or other -automation. The [REST example](#place-a-call-with-rest) shows the request directly with curl -and through the Server SDK's REST clients. +Your backend can start a call with an HTTP request to the [Calling API][calling-api] or a +request over a persistent Relay WebSocket connection. The Server SDK provides both a REST +client and a [Relay client][relay-guide]. Use either approach for notifications or other automation; +the tabs below explain how dialing and call control differ. ### Forwarding an inbound call @@ -57,15 +57,10 @@ Configure this with SWML's [`connect` method][swml-connect] or Call Flow Builder [Forward to Phone node][forward-node]. The incoming call triggers the outbound call through that configuration. See the [forwarding example](#forward-an-inbound-call). -### SDK request +### Browser SDK request -Your application calls an SDK method to dial a destination. The SDK you use depends on where -your code runs: - -- **Server SDK:** Your backend uses the REST client to send a Calling API request, or the - [Relay client][relay-guide] to dial and control the call over a WebSocket connection. -- **Browser SDK:** Your frontend dials from the user's device and connects its microphone and - speaker to the call. Use this when a person will speak through your web app. +Your frontend uses the [Browser SDK][browser-outbound] to dial from the user's device and +connect its microphone and speaker to the call. Use this when a person will speak through your web app. A button in a web app can also ask your backend to start an automated call. In that case, the backend makes the Calling API or Server SDK request. The Browser SDK is for participating in the @@ -92,16 +87,31 @@ context. Calling that address directly does not require a destination phone numb ### Support by API and SDK -- **Calling API and Server SDK REST clients:** Accept phone numbers, SIP URIs, and Call Fabric - addresses in `to`. See the [Calling API reference][calling-api]. -- **Server SDK Relay client:** Supports `phone` and `sip` devices in its `devices` list. Phone - devices use `params.to_number`; see the [dial reference][relay-dial-python] for the device format. -- **Browser SDK:** Accepts phone numbers, SIP URIs, and Call Fabric addresses as the destination - passed to `client.dial()`. The user's token must allow access to that destination. See the - [Browser SDK outbound guide][browser-outbound]. -- **Inbound forwarding:** SWML's [`connect`][swml-connect] accepts phone numbers, SIP URIs, and - Call Fabric addresses in `to`. Call Flow Builder's [Forward to Phone node][forward-node] - supports phone numbers and SIP endpoints. + + + +The Calling API and Server SDK REST clients accept phone numbers, SIP URIs, and Call Fabric +addresses in `to`. Use `from` for the calling address. The [Calling API reference][calling-api] +covers the request fields. + + + + +The Server SDK Relay client takes a nested `devices` list. Each device has a `type` and `params`: + +- **Phone:** `type: "phone"`, with `params.to_number` and `params.from_number`. +- **SIP:** `type: "sip"`, with `params.to` and `params.from`. + +Each inner list contains devices to ring together. The outer list gives the order in which to +try those groups. See the [Python][relay-dial-python] and [TypeScript][relay-dial-ts] dial references. + + + + +The **Browser SDK** accepts phone numbers, SIP URIs, and Call Fabric addresses in `client.dial()`; +the user's token must allow access to the destination. For **inbound forwarding**, SWML's +[`connect`][swml-connect] accepts those addresses in `to`, while Call Flow Builder's +[Forward to Phone node][forward-node] supports phone numbers and SIP endpoints. @@ -123,26 +133,101 @@ and callback settings. -## What happens when someone answers +## How the call runs + +Both server approaches ask SignalWire to place the call. The difference is how your backend +supplies the call logic and receives progress. These diagrams trace a call to a rider's phone. + + + + +Send a `dial` request with a SignalWire Markup Language (SWML) document, either inline in `swml` +or hosted at `url`. The response returns a call ID before the rider answers. SignalWire runs the +SWML after answer and sends progress to your webhook endpoint. The document can play an +announcement, run an AI agent, or connect the recipient to another destination. + +This diagram shows `answered` and `ended` webhooks enabled. + + + +Your code sends a dial request with from, to, and SWML. SignalWire returns a call id with status queued and rings the rider's phone. When the rider answers, SignalWire sends an answered webhook and runs your SWML. When the call finishes, SignalWire sends an ended webhook. + + + + + +```mermaid +sequenceDiagram + participant App as Your code + participant SW as SignalWire + participant Rider as Rider's phone + + App->>SW: dial: from, to, SWML + SW-->>App: call id, status queued + SW->>Rider: rings + Rider->>SW: answers + SW-->>App: status webhook answered + Note over SW,Rider: Your SWML runs + Note over SW,Rider: Call finishes + SW-->>App: status webhook ended +``` + + -The call's instructions determine what the recipient hears: + + + +Connect your Server SDK Relay client, then call `dial` with the devices to ring. The SDK waits +for the dial outcome and returns a call object when the rider answers. Your running code then +controls the call with methods such as `play`, `connect`, and `hangup`. + +Commands and call events travel over the same WebSocket connection. In this flow, your code +plays an announcement, waits for playback to finish, and hangs up. + + -- **REST-initiated calls** run a SignalWire Markup Language (SWML) document. Send it inline in - `swml`, or provide a `url` where SignalWire can fetch it. The document can play an announcement, - run an AI agent, or connect the recipient to another destination. -- **Forwarded calls** connect the recipient to the original caller. -- **Server SDK Relay calls** follow commands from your running backend, such as playing audio - or connecting the call. -- **Browser SDK calls** carry live audio between the browser user and the recipient. +Your server keeps a Relay WebSocket connection open to SignalWire. It sends dial with devices, SignalWire rings the rider, and the rider answers. The dial outcome event lets the SDK return a call object. Your server sends play, waits for the playback-finished event, then sends hangup and receives the call-ended event. + + + + + +```mermaid +sequenceDiagram + participant App as Your server + participant SW as SignalWire + participant Rider as Rider's phone + + Note over App,SW: Authenticated WebSocket stays open + App->>SW: dial: devices + SW->>Rider: rings + Rider->>SW: answers + SW-->>App: Dial outcome: answered + Note over App: SDK returns a call object + App->>SW: play announcement + Note over SW,Rider: Announcement plays + SW-->>App: Playback finished + App->>SW: hangup + SW-->>App: Call ended +``` + + + + + + +For **inbound forwarding**, the recipient connects to the original caller. For **Browser SDK +calls**, the browser user exchanges live audio with the destination. ## Before you dial ### Phone numbers and caller ID -Use E.164 format for phone numbers, such as `+15551234567`. For Calling API requests to the PSTN, +Use E.164 format for phone numbers, such as `+15551234567`. For server calls to the PSTN, use a [phone number][phone-numbers] purchased in your project or a [verified caller ID][caller-id] -as the calling number. SIP destinations can use a SIP URI as the calling address; see the -[Calling API reference][calling-api] for `from` and `caller_id`. +as the calling number: `from` in REST, or `params.from_number` for a Relay phone device. SIP +destinations can use a SIP URI as the calling address; see the [Calling API reference][calling-api] +for `from` and `caller_id`. Forwarding has its own caller ID configuration; the [forwarding guide][forward-calls] shows how to preserve the original caller's number. @@ -168,16 +253,33 @@ frontend uses that SAT with `@signalwire/js`. The Project API Token stays on the ## Track the call's progress -| How you placed the call | How to confirm it connected | -|---|---| -| Calling API or Server SDK REST client | A `200` response with a call `id` means the request was accepted. Listen for an `answered` webhook to confirm pickup. | -| Inbound forwarding with SWML | Use `connect`'s `call_state_url` and `call_state_events` to receive outbound call state webhooks. See [the reference][swml-connect]. | -| Server SDK Relay client | `dial` returns a call object when the destination answers, or raises `RelayError` if dialing fails. | -| Browser SDK | Watch for `status$` to reach `connected`. If `dial()` rejects, check the destination and token permissions. | + + + +A `200` response with a call `id` means SignalWire accepted the request. Listen for `answered` +to confirm pickup and `ended` to know the call finished. + +Set `status_url` to your webhook endpoint and choose `status_events`: `created`, `ringing`, +`answered`, or `ended`. If you omit `status_events`, you receive `ended` only. + + + -For Calling API requests, send a `status_url` and choose `status_events`: `created`, `ringing`, -`answered`, or `ended`. If you omit `status_events`, you receive `ended` only. Forwarding uses the -callback settings of your SWML method or Call Flow Builder node. +The Server SDK's `dial` waits for the dial outcome. It returns a call object after answer or +raises `RelayError` if dialing fails. At the protocol level, the initial `calling.dial` response +only acknowledges the request; the SDK waits for a `calling.call.dial` event before returning. + +Register call listeners with `call.on` to receive state and action events over the WebSocket. +For an action such as playback, `action.wait()` waits for it to finish before your next step. +See the [Python](/docs/server-sdks/reference/python/relay/call/on) or +[TypeScript](/docs/server-sdks/reference/typescript/relay/call/on) event reference. + + + + +For **SWML forwarding**, use `connect`'s `call_state_url` and `call_state_events`, or the callback +settings of your Call Flow Builder node. For **Browser SDK calls**, watch for `status$` to reach +`connected`; if `dial()` rejects, check the destination and token permissions. ## Troubleshoot outbound calls @@ -186,7 +288,7 @@ placed. Check the error code first. For browser setup and token issues, see [Browser SDK authentication][browser-auth]. - + A `422` response carries an [error code][error-codes] that names the problem. `not_purchased_or_verified` means the `from` number is not purchased or verified for your project. `not_valid_for_caller_id` @@ -194,6 +296,14 @@ means `from` or `caller_id` isn't an E.164 number, caller ID string, or SIP URI. `destination_number_not_supported` point at `to`. `insufficient_balance` means the project can't pay for the call. A `dial` request that has neither `url` nor `swml` is also rejected. + + + +Confirm the Relay client connected with your Project ID, API token, and Space hostname before +calling `dial`. Check the `RelayError` and the device parameters: phone devices use `to_number` +and `from_number`, while SIP devices use `to` and `from`. Set a device `timeout` long enough for +the destination to answer. The [Relay guide][relay-guide] covers connection setup. + @@ -229,37 +339,18 @@ JSON object, not a string of escaped JSON. These examples use Bayview Taxi: an automated driver-arrival announcement, an incoming call forwarded to a dispatcher, and a dispatcher speaking to a rider from the browser. -### Place a call with REST +### Place a call from a server -Run the curl command from a terminal or the SDK code on your backend, using your Project ID, -API token, and Space hostname. This flow uses SWML for the announcement and requests `answered` -and `ended` webhooks: +Both approaches tell the rider their driver is on the way. Choose the client your backend uses. - + + -Your code sends a dial request with from, to, and SWML. SignalWire returns a call id with status queued and rings the rider's phone. When the rider answers, SignalWire sends an answered webhook and runs your SWML. When the call finishes, SignalWire sends an ended webhook. - - +#### Place a call with REST - - -```mermaid -sequenceDiagram - participant App as Your code - participant SW as SignalWire - participant Rider as Rider's phone - - App->>SW: dial: from, to, SWML - SW-->>App: call id, status queued - SW->>Rider: rings - Rider->>SW: answers - SW-->>App: status webhook answered - Note over SW,Rider: Your SWML runs - Note over SW,Rider: Call finishes - SW-->>App: status webhook ended -``` - - +Run the curl command from a terminal or the SDK code on your backend, using your Project ID, +API token, and Space hostname. This flow uses SWML for the announcement and requests `answered` +and `ended` webhooks. Serve a SWML document at a URL SignalWire can reach. This one plays the announcement: @@ -385,25 +476,10 @@ many calls. -### Forward an inbound call + + -Assign this SWML script to the SignalWire number that riders call. When a rider calls in, -SignalWire dials the dispatcher's phone and connects them: - -```yaml -version: 1.0.0 -sections: - main: - - connect: - from: "+15551234567" - to: "+15557654321" -``` - -Replace `from` with your SignalWire number and `to` with the dispatcher's number. The dispatcher -sees your SignalWire number as caller ID. See the [forwarding guide][forward-calls] for assigning -the script and preserving the original caller's number instead. - -### Place a call with the Server SDK Relay client +#### Place a call with the Server SDK Relay client Run these Relay examples on your server to play the announcement. Your code waits for the rider to answer, speaks the message, and ends the call. @@ -480,6 +556,27 @@ fails. Then `play` speaks the announcement and `hangup` ends the call. The [Python][relay-dial-python] and [TypeScript][relay-dial-ts] references cover timeouts and ringing several destinations. + + + +### Forward an inbound call + +Assign this SWML script to the SignalWire number that riders call. When a rider calls in, +SignalWire dials the dispatcher's phone and connects them: + +```yaml +version: 1.0.0 +sections: + main: + - connect: + from: "+15551234567" + to: "+15557654321" +``` + +Replace `from` with your SignalWire number and `to` with the dispatcher's number. The dispatcher +sees your SignalWire number as caller ID. See the [forwarding guide][forward-calls] for assigning +the script and preserving the original caller's number instead. + ### Place a call with the Browser SDK The dispatcher speaks to the rider through your web app. First have your backend issue a From fa65ec19626cd58cf661122c3b4aad6f2963cc80 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 15:45:12 -0400 Subject: [PATCH 011/103] docs: illustrate Relay bidirectional commands and event reactions --- .../outbound-call-relay-lifecycle-themed.svg | 162 ++++++++++-------- .../pages/calling/voice/outbound-calling.mdx | 44 ++--- 2 files changed, 119 insertions(+), 87 deletions(-) diff --git a/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg b/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg index 4ee7c8581d..31985c6af9 100644 --- a/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg +++ b/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg @@ -1,85 +1,113 @@ - - Outbound call with the Server SDK Relay client - Your server keeps a WebSocket connection open to SignalWire. It sends dial with devices. After the rider answers, the dial outcome event lets the SDK return a call object. Your server sends play, receives playback finished, sends hangup, and receives call ended. - + + Relay: commands and events over one bidirectional WebSocket + Your application and SignalWire share one persistent WebSocket. Your application sends commands to control the call; SignalWire sends live events as the call changes. Your application decides how to react: a call-answered event can trigger play, collected keypad input can trigger connect to a chosen destination, and a playback-finished event can trigger hangup. These are examples of application decisions, not an automatic sequence. Input events require an active collection operation. + - - + + + + + + + + - - - - - - Your server - - - - - SignalWire - - - - - Rider’s phone + + One persistent, bidirectional WebSocket + + + + Your application + Decides what + happens next + + Commands → + dial, play, connect, hangup + + + ← Live events + call state, playback, collected input + + SignalWire + Places and + controls the call - - WebSocket stays open + Examples: your code reacts to an event + Event from SignalWire + Your app decides + Command to SignalWire - dial: devices - - rings - - answers - + + + Call answered + calling.call.dial + + + Start the greeting + + + play(...) + announcement audio - dial answered - - SDK returns Call + + Digits collected + calling.call.collect + + + Route to dispatch + + + connect(...) + chosen destination - play - - - Announcement plays + + Playback finished + calling.call.play + + + End the conversation + + + hangup() + finish this call - playback finished - - hangup - - call ended - + Input events require an active collection operation. diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index eb43ae1baa..32fe80a6f4 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -136,7 +136,7 @@ and callback settings. ## How the call runs Both server approaches ask SignalWire to place the call. The difference is how your backend -supplies the call logic and receives progress. These diagrams trace a call to a rider's phone. +supplies the call logic and receives progress. The diagrams show how each approach controls a call. @@ -181,34 +181,38 @@ Connect your Server SDK Relay client, then call `dial` with the devices to ring. for the dial outcome and returns a call object when the rider answers. Your running code then controls the call with methods such as `play`, `connect`, and `hangup`. -Commands and call events travel over the same WebSocket connection. In this flow, your code -plays an announcement, waits for playback to finish, and hangs up. +The WebSocket is a persistent, bidirectional channel: your app sends commands to control the +call, and SignalWire sends live events as its state, playback, or collected input changes. +Your code uses those events to decide what to do next. + +The diagram shows examples of that feedback: start a greeting after answer, choose a destination +from collected keypad input, or hang up after playback finishes. Input collection must first be +started with a method such as [play and collect](/docs/server-sdks/reference/python/relay/call/play-and-collect). +Your app makes each decision using event listeners or the SDK's action results. -Your server keeps a Relay WebSocket connection open to SignalWire. It sends dial with devices, SignalWire rings the rider, and the rider answers. The dial outcome event lets the SDK return a call object. Your server sends play, waits for the playback-finished event, then sends hangup and receives the call-ended event. +One persistent WebSocket carries commands from your application to SignalWire and live events back to your application. Your code decides how to react: call answered can trigger play, collected keypad input can trigger connect to a chosen destination, and playback finished can trigger hangup. These are independent examples of application decisions; input events require an active collection operation. ```mermaid -sequenceDiagram - participant App as Your server - participant SW as SignalWire - participant Rider as Rider's phone - - Note over App,SW: Authenticated WebSocket stays open - App->>SW: dial: devices - SW->>Rider: rings - Rider->>SW: answers - SW-->>App: Dial outcome: answered - Note over App: SDK returns a call object - App->>SW: play announcement - Note over SW,Rider: Announcement plays - SW-->>App: Playback finished - App->>SW: hangup - SW-->>App: Call ended +flowchart TB + subgraph Channel[One persistent, bidirectional WebSocket] + direction LR + App[Your application: decides what happens next] + SW[SignalWire: places and controls the call] + App -->|Commands: dial, play, connect, hangup| SW + SW -.->|Live events: call state, playback, collected input| App + end + + subgraph Reactions[Examples of application decisions, not an automatic sequence] + Answered[Call answered: calling.call.dial] --> Greeting[Your app: start the greeting] --> Play[Command: play] + Input[Keypad input collected: calling.call.collect] --> Route[Your app: route to dispatch] --> Connect[Command: connect] + Finished[Playback finished: calling.call.play] --> End[Your app: end the conversation] --> Hangup[Command: hangup] + end ``` From 21b03d102d19941a0545a0af49cfb4b0651f4f72 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 15:50:40 -0400 Subject: [PATCH 012/103] docs: streamline outbound calling guide for new readers --- .../pages/calling/voice/outbound-calling.mdx | 292 +++++++----------- 1 file changed, 107 insertions(+), 185 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 32fe80a6f4..3022db7d65 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -35,44 +35,31 @@ max-toc-depth: 3 [resource-addresses]: /docs/platform/addresses [resources]: /docs/platform/resources -An outbound call is a call your application or call flow places to a destination: a phone number, -SIP endpoint, or a user or application in SignalWire. It can start with a REST API request, -an inbound call being forwarded, or a request from an SDK. +Outbound calls let your app reach a customer, connect a caller to a dispatcher, or start an AI +conversation. You choose the destination and what happens on the call. -## How outbound calls start - -### Server API or SDK request - -Your backend can start a call with an HTTP request to the [Calling API][calling-api] or a -request over a persistent Relay WebSocket connection. The Server SDK provides both a REST -client and a [Relay client][relay-guide]. Use either approach for notifications or other automation; -the tabs below explain how dialing and call control differ. - -### Forwarding an inbound call - -Someone calls your SignalWire number, and its call flow dials another destination and connects -the two calls. The call to the second destination is the outbound part of that conversation. +Jump to the [examples](#examples) for working code or +[troubleshooting](#troubleshoot-outbound-calls) to diagnose a failed call. -Configure this with SWML's [`connect` method][swml-connect] or Call Flow Builder's -[Forward to Phone node][forward-node]. The incoming call triggers the outbound call through that -configuration. See the [forwarding example](#forward-an-inbound-call). +## How outbound calls start -### Browser SDK request +Your **server** can send a REST request to the [Calling API][calling-api] or dial over a +persistent Relay WebSocket connection. The Server SDK provides a client for each approach. -Your frontend uses the [Browser SDK][browser-outbound] to dial from the user's device and -connect its microphone and speaker to the call. Use this when a person will speak through your web app. +**Forwarding an inbound call** places an outbound call to a second destination and connects the +two callers. Configure it with [SWML call instructions][swml-connect] or Call Flow Builder's +[Forward to Phone node][forward-node]. -A button in a web app can also ask your backend to start an automated call. In that case, the -backend makes the Calling API or Server SDK request. The Browser SDK is for participating in the -call from the browser. +The **Browser SDK** lets a person dial and speak through your web app's microphone and speaker. +A button that starts an automated call instead sends the request to your backend. -A registered SIP phone or phone system can also dial out through SignalWire. See -[SIP credentials][sip-credentials] for that setup. +Registered SIP phones and phone systems can also dial through SignalWire using +[SIP credentials][sip-credentials]. ## Destinations you can call -The destination identifies who or what you want to reach. A call can leave SignalWire over the -public telephone network (PSTN) or SIP, or connect directly to a resource in your Space. +A destination can be a phone on the public switched telephone network (PSTN), a SIP endpoint, or a +resource in your SignalWire Space. | Destination | Address format | What you reach | |---|---|---| @@ -81,72 +68,62 @@ public telephone network (PSTN) or SIP, or connect directly to a resource in you | Call Fabric application or room | Resource address, such as `/public/dispatch-agent` | An AI agent, SWML application, or room in your Space. | | Call Fabric user | Resource address, such as `/private/dispatcher` | A registered user (Subscriber) in your Space. | -Applications, rooms, and Subscribers are all [Resources][resources]. Use the actual -[address][resource-addresses] configured for the resource, including its `public` or `private` -context. Calling that address directly does not require a destination phone number. +Applications, rooms, and Subscribers are [Resources][resources]. Dial their configured +[address][resource-addresses], including the `public` or `private` context, to reach them directly. ### Support by API and SDK -The Calling API and Server SDK REST clients accept phone numbers, SIP URIs, and Call Fabric -addresses in `to`. Use `from` for the calling address. The [Calling API reference][calling-api] -covers the request fields. +The Calling API and Server SDK REST clients accept all four destination types above in `to`. +Set the calling address in `from`. See the [Calling API reference][calling-api] for all fields. -The Server SDK Relay client takes a nested `devices` list. Each device has a `type` and `params`: +The Relay client accepts phone and SIP devices in a nested `devices` list: - **Phone:** `type: "phone"`, with `params.to_number` and `params.from_number`. - **SIP:** `type: "sip"`, with `params.to` and `params.from`. -Each inner list contains devices to ring together. The outer list gives the order in which to -try those groups. See the [Python][relay-dial-python] and [TypeScript][relay-dial-ts] dial references. +Each inner list rings together; the outer list sets the order of attempts. The +[Python][relay-dial-python] and [TypeScript][relay-dial-ts] references cover device options. -The **Browser SDK** accepts phone numbers, SIP URIs, and Call Fabric addresses in `client.dial()`; -the user's token must allow access to the destination. For **inbound forwarding**, SWML's -[`connect`][swml-connect] accepts those addresses in `to`, while Call Flow Builder's -[Forward to Phone node][forward-node] supports phone numbers and SIP endpoints. +The **Browser SDK** and **SWML `connect`** accept all four types above. Call Flow Builder's +**Forward to Phone** node supports phone numbers and SIP endpoints. - + -The [Calling API][calling-api] also accepts secure SIP URIs such as -`sips:dispatcher@example.com`. For an existing Verto registration, use its full address, such as -`verto:dispatcher@YOUR_SPACE.verto.signalwire.com`; the destination must be registered. +The [Calling API][calling-api] also accepts `sips:` URIs and full, registered Verto addresses +such as `verto:dispatcher@YOUR_SPACE.verto.signalwire.com`. -For a destination handled by a SWML script, use `to_script` with an inline document or a URL that -returns one; `to` can then be omitted. The request still requires `from` and either `url` or -`swml` for the call-handling instructions. `to_script` supplies the destination's instructions. +Use `to_script` to run SWML at the destination, supplying an inline document or its URL. +The request still needs `from` and call-handling instructions in `url` or `swml`; `to` can be omitted. -SWML's [`connect` method][swml-connect] also supports queues (`queue:support`) and WebSocket -media streams (`stream:wss://example.com/audio`). A queue connection also requires -`transfer_after_bridge`, pointing to SWML to run after the bridge ends. See the complete -[queue example](/docs/swml/reference/calling/connect#connect-to-a-queue-with-transfer-after-bridge). -For media streams, use a WebSocket endpoint that handles the streamed audio; see the -[stream example](/docs/swml/reference/calling/connect#connect-to-a-websocket-stream) for codec -and callback settings. +SWML `connect` also supports [queues](/docs/swml/reference/calling/connect#connect-to-a-queue-with-transfer-after-bridge) +(`queue:support`, with the required `transfer_after_bridge`) and +[WebSocket media streams](/docs/swml/reference/calling/connect#connect-to-a-websocket-stream) +(`stream:wss://example.com/audio`). Follow the linked examples for their connection settings. ## How the call runs -Both server approaches ask SignalWire to place the call. The difference is how your backend -supplies the call logic and receives progress. The diagrams show how each approach controls a call. +With REST, SignalWire runs the instructions you supply. With Relay, your running application +controls the call through commands and events. -Send a `dial` request with a SignalWire Markup Language (SWML) document, either inline in `swml` -or hosted at `url`. The response returns a call ID before the rider answers. SignalWire runs the -SWML after answer and sends progress to your webhook endpoint. The document can play an -announcement, run an AI agent, or connect the recipient to another destination. +A `dial` request supplies a SignalWire Markup Language (SWML) document in `swml` or at `url`. +SignalWire places the call and runs the document after answer. The document can play audio, +run an AI agent, or connect another destination. Progress arrives at your webhook endpoint. -This diagram shows `answered` and `ended` webhooks enabled. +This flow uses `answered` and `ended` webhooks: @@ -177,18 +154,9 @@ sequenceDiagram -Connect your Server SDK Relay client, then call `dial` with the devices to ring. The SDK waits -for the dial outcome and returns a call object when the rider answers. Your running code then -controls the call with methods such as `play`, `connect`, and `hangup`. - -The WebSocket is a persistent, bidirectional channel: your app sends commands to control the -call, and SignalWire sends live events as its state, playback, or collected input changes. -Your code uses those events to decide what to do next. - -The diagram shows examples of that feedback: start a greeting after answer, choose a destination -from collected keypad input, or hang up after playback finishes. Input collection must first be -started with a method such as [play and collect](/docs/server-sdks/reference/python/relay/call/play-and-collect). -Your app makes each decision using event listeners or the SDK's action results. +Relay keeps a bidirectional WebSocket open between your server and SignalWire. Your app sends +commands such as `dial`, `play`, and `connect`; SignalWire sends live events back. Your code +uses those events to decide what the call does next. @@ -220,40 +188,26 @@ flowchart TB -For **inbound forwarding**, the recipient connects to the original caller. For **Browser SDK -calls**, the browser user exchanges live audio with the destination. - ## Before you dial -### Phone numbers and caller ID +### Calling number -Use E.164 format for phone numbers, such as `+15551234567`. For server calls to the PSTN, -use a [phone number][phone-numbers] purchased in your project or a [verified caller ID][caller-id] -as the calling number: `from` in REST, or `params.from_number` for a Relay phone device. SIP -destinations can use a SIP URI as the calling address; see the [Calling API reference][calling-api] -for `from` and `caller_id`. - -Forwarding has its own caller ID configuration; the [forwarding guide][forward-calls] shows how -to preserve the original caller's number. - -For inbound forwarding, assign your SWML script or Call Flow Builder flow to the SignalWire -number receiving the call. The [forwarding guide][forward-calls] walks through this setup. +For server calls to phone numbers, use a [purchased number][phone-numbers] or +[verified caller ID][caller-id]. Pass it as `from` in REST or `params.from_number` for a Relay +phone device. SIP caller ID settings are covered in the [Calling API reference][calling-api]. -A project in [trial mode][trial-mode] can only call purchased and verified numbers, and can't call -internationally at all. Outside trial mode, calls to other countries still need -[international dialing enabled][international] for your Space. +[Trial projects][trial-mode] can call only purchased and verified numbers, with no international +calling. Other projects need [international dialing enabled][international] to call other countries. -### Server and browser credentials +### Credentials -For Calling API and Server SDK requests, use your Project ID and an API token with voice -permissions from the Dashboard's [API credentials][api-credentials] page. Keep the API token on -your backend. +Server calls use your Project ID and an API token with voice permissions from the Dashboard's +[API credentials][api-credentials] page. Keep the API token on your backend. -For Browser SDK calls, your backend authenticates the user and issues a -[Subscriber Access Token (SAT)][browser-auth] that allows them to call the destination. Your -frontend uses that SAT with `@signalwire/js`. The Project API Token stays on the backend. +Browser calls use a [Subscriber Access Token (SAT)][browser-auth] that your backend issues for +the user. Its permissions determine which destinations the user can call. ## Track the call's progress @@ -269,94 +223,75 @@ Set `status_url` to your webhook endpoint and choose `status_events`: `created`, -The Server SDK's `dial` waits for the dial outcome. It returns a call object after answer or -raises `RelayError` if dialing fails. At the protocol level, the initial `calling.dial` response -only acknowledges the request; the SDK waits for a `calling.call.dial` event before returning. - -Register call listeners with `call.on` to receive state and action events over the WebSocket. -For an action such as playback, `action.wait()` waits for it to finish before your next step. -See the [Python](/docs/server-sdks/reference/python/relay/call/on) or +The SDK's `dial` returns a call object after answer or raises `RelayError` on failure. +Use `call.on` for state and action events, and `action.wait()` to wait for an action such as +playback to finish. See the [Python](/docs/server-sdks/reference/python/relay/call/on) or [TypeScript](/docs/server-sdks/reference/typescript/relay/call/on) event reference. + +The initial `calling.dial` response acknowledges the request. A later `calling.call.dial` +event carries the dial outcome. The Server SDK waits for this event before returning a call object. + + -For **SWML forwarding**, use `connect`'s `call_state_url` and `call_state_events`, or the callback -settings of your Call Flow Builder node. For **Browser SDK calls**, watch for `status$` to reach -`connected`; if `dial()` rejects, check the destination and token permissions. - ## Troubleshoot outbound calls -For Calling API and Server SDK REST requests, a `422` means the call was rejected before it was -placed. Check the error code first. For browser setup and token issues, see -[Browser SDK authentication][browser-auth]. +Start with the error returned by your API or SDK, then check the relevant case below. +Browser token setup is covered in [authentication][browser-auth]. -A `422` response carries an [error code][error-codes] that names the problem. `not_purchased_or_verified` -means the `from` number is not purchased or verified for your project. `not_valid_for_caller_id` -means `from` or `caller_id` isn't an E.164 number, caller ID string, or SIP URI. `invalid_destination_number` and -`destination_number_not_supported` point at `to`. `insufficient_balance` means the project can't pay -for the call. A `dial` request that has neither `url` nor `swml` is also rejected. +A `422` response includes an [error code][error-codes]. Check the field it identifies: +`not_purchased_or_verified` means that number is not purchased or verified for your project. +Also check the destination (`to`), call instructions (`url` or `swml`), and account balance. -Confirm the Relay client connected with your Project ID, API token, and Space hostname before -calling `dial`. Check the `RelayError` and the device parameters: phone devices use `to_number` -and `from_number`, while SIP devices use `to` and `from`. Set a device `timeout` long enough for -the destination to answer. The [Relay guide][relay-guide] covers connection setup. +Check the `RelayError`, confirm the client is connected, and review the device parameters. +Phone devices use `to_number` and `from_number`; SIP devices use `to` and `from`. +The [Relay guide][relay-guide] covers connection setup. -Check the account limits first. A project in [trial mode][trial-mode] can only reach purchased and -verified numbers. International destinations fail until you [enable international -dialing][international]. If those are fine, a `timeout` shorter than the destination's ring time -ends the call early, and a `max_price_per_minute` below the route's price rejects it before it -rings. +Check the ring `timeout`, [trial limits][trial-mode], and [international dialing settings][international]. +For REST calls, a `max_price_per_minute` below the route's price rejects the call before it rings. -For Calling API requests to phone numbers, check `from` and any `caller_id` override against your -purchased or verified numbers. For forwarded calls, check the caller ID configured in your forwarding flow. -Set the caller name with the [Caller ID and CNAM][caller-id] guide. A "Spam likely" label on your -number is a reputation problem rather than a configuration one. Start with the -[spam labels][spam-labels] guide and confirm your calls carry [STIR/SHAKEN][stir-shaken] -attestation. +Review the calling number and any caller ID override in your request or forwarding flow. +The [Caller ID and CNAM][caller-id] guide covers display settings. For reputation issues, see +[spam labels][spam-labels] and [STIR/SHAKEN][stir-shaken]. -SignalWire requests `url` with `POST` unless you set `url_method`, and the endpoint has to return a -SWML document. Set `fallback_url` so a failed fetch still gets instructions, and secure the endpoint -as described in [SWML webhook security][swml-webhook-security]. An inline `swml` value must be a -JSON object, not a string of escaped JSON. +Your `url` endpoint must return SWML and accept `POST` by default. Use `url_method` to change +the method or `fallback_url` to provide backup instructions. Send inline `swml` as a JSON object. +See [SWML webhook security][swml-webhook-security] for securing the endpoint. ## Examples -These examples use Bayview Taxi: an automated driver-arrival announcement, an incoming call -forwarded to a dispatcher, and a dispatcher speaking to a rider from the browser. +Bayview Taxi uses outbound calls to notify riders and connect them with dispatchers. ### Place a call from a server -Both approaches tell the rider their driver is on the way. Choose the client your backend uses. +Tell a rider their driver is on the way, using REST or Relay. #### Place a call with REST -Run the curl command from a terminal or the SDK code on your backend, using your Project ID, -API token, and Space hostname. This flow uses SWML for the announcement and requests `answered` -and `ended` webhooks. - -Serve a SWML document at a URL SignalWire can reach. This one plays the announcement: +Host this SWML at a URL SignalWire can reach: ```yaml version: 1.0.0 @@ -365,8 +300,8 @@ sections: - play: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." ``` -Then point a `dial` request at that URL. These three examples send the same request, each asking -for an update when the call is answered and when it ends. +Run one of these equivalent requests from your terminal or backend. Replace the credentials, +phone numbers, SWML URL, and status webhook URL with your own values. @@ -428,9 +363,7 @@ console.log(call.id); -The Calling API and Server SDK REST clients ([Python][sdk-rest-dial-python] or -[TypeScript][sdk-rest-dial-ts]) send the same `dial` command, so their parameters match. -The response returns the new call before anyone answers: +The response identifies the queued call: ```json { @@ -443,15 +376,15 @@ The response returns the new call before anyone answers: } ``` -Keep the `id` to end or transfer the call with other Calling API commands. The `status_url` -endpoint receives the `answered` and `ended` webhooks requested above. +Keep the `id` for later call commands. Your `status_url` receives the requested `answered` and +`ended` webhooks. See the REST client reference for [Python][sdk-rest-dial-python] or +[TypeScript][sdk-rest-dial-ts] for the full request schema. -You don't have to host the document. Send it in the `swml` field instead of `url`, as a JSON -object rather than a string of escaped JSON. To make the logic specific to one call, attach your -own values in `custom_variables` and read them in the SWML as `${envs.}` using -[SWML expressions][swml-expressions]. +Send the document as a JSON object in `swml` instead of hosting it at `url`. +Pass per-call values in `custom_variables` and read them with [SWML expressions][swml-expressions] +such as `${envs.driver_name}`: ```bash curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ @@ -475,8 +408,7 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ }' ``` -The same variables are available when you serve SWML from `url`, so one document can personalize -many calls. +`custom_variables` also works with SWML served from `url`. @@ -485,8 +417,8 @@ many calls. #### Place a call with the Server SDK Relay client -Run these Relay examples on your server to play the announcement. Your code waits for the -rider to answer, speaks the message, and ends the call. +Run this on your server with your credentials and phone numbers. It dials the rider, plays +the announcement after answer, waits for playback to finish, and hangs up. @@ -555,10 +487,7 @@ await client.disconnect(); -With Relay, `dial` returns a call object when the rider answers, or raises `RelayError` if dialing -fails. Then `play` speaks the announcement and `hangup` ends the call. -The [Python][relay-dial-python] and [TypeScript][relay-dial-ts] references cover timeouts and -ringing several destinations. +See the [Relay guide][relay-guide] for connection management and more call controls. @@ -577,15 +506,14 @@ sections: to: "+15557654321" ``` -Replace `from` with your SignalWire number and `to` with the dispatcher's number. The dispatcher -sees your SignalWire number as caller ID. See the [forwarding guide][forward-calls] for assigning -the script and preserving the original caller's number instead. +Set `from` to your SignalWire number and `to` to the dispatcher's number. The +[forwarding guide][forward-calls] covers assigning the script and preserving the original caller ID. +Use `connect`'s `call_state_url` and `call_state_events` to track the outbound call. ### Place a call with the Browser SDK -The dispatcher speaks to the rider through your web app. First have your backend issue a -[Subscriber Access Token][browser-auth] for the dispatcher with permission to dial phone numbers. -Pass that token to the frontend as `SAT`; the following code runs in the browser. +Let the dispatcher speak to the rider through your web app. Supply a +[Subscriber Access Token][browser-auth] as `SAT`, with permission to call phone numbers. Use an HTTPS page with `` and ``. Run the dialing code from a user action, such as a @@ -606,15 +534,13 @@ call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); hangupButton.onclick = () => call.hangup(); ``` -Watch for `status$` to reach `connected` to confirm the browser call connected. If `dial()` -rejects, check that the token is allowed to reach the destination. The -[Browser SDK outbound guide][browser-outbound] includes a complete page with media and call controls. +Watch for `status$` to reach `connected`. The [Browser SDK outbound guide][browser-outbound] +includes a complete page with media and call controls. ### Run an AI agent with SWML -To turn the [REST announcement example](#place-a-call-with-rest) into an AI conversation, -serve this SWML at the URL used in that request. It replaces `play` with the [`ai` method][swml-ai]; -the dial request stays the same: +Use this SWML in the [REST example](#place-a-call-with-rest) to replace the announcement with +an AI conversation. The [`ai` method][swml-ai] supplies the agent's greeting and instructions: ```yaml version: 1.0.0 @@ -632,18 +558,14 @@ sections: Bayview Taxi to make those changes. Do not claim to have made a change. ``` -This agent only answers questions about the arrival estimate. With `static_greeting_no_barge` -enabled, the greeting plays in full before the agent's first turn, even if the rider starts talking. - -To let the agent change a booking or record a request to stop future calls, connect it to your -backend with [tool calling][tool-calling]. Treat those as separate actions: stopping future calls -doesn't cancel the current ride. The [AI quickstart][ai-quickstart] shows how to serve an agent -from your own code. +`static_greeting_no_barge` lets the greeting finish before the conversation begins. Add +[tool calling][tool-calling] to let the agent take actions, such as updating a booking. +The [AI quickstart][ai-quickstart] covers serving an agent from your code. An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). Check consent, your do-not-call list, and the local calling hours in your code before you send the -`dial` request, because once the call is placed it has already happened. The [TCPA guide][tcpa] and +`dial` request. The [TCPA guide][tcpa] and the compliance section of [AI best practices][ai-best-practices] cover each obligation. This is technical guidance, not legal advice. From 147a2f81a8eabb0fce3e05a3c89807d6d647fc37 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 16:11:15 -0400 Subject: [PATCH 013/103] docs: inline SWML in the first outbound REST example --- .../pages/calling/voice/outbound-calling.mdx | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 3022db7d65..f3ab8bc24c 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -291,20 +291,11 @@ Tell a rider their driver is on the way, using REST or Relay. #### Place a call with REST -Host this SWML at a URL SignalWire can reach: - -```yaml -version: 1.0.0 -sections: - main: - - play: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." -``` - -Run one of these equivalent requests from your terminal or backend. Replace the credentials, -phone numbers, SWML URL, and status webhook URL with your own values. +This request includes the SWML announcement. Replace the credentials and phone numbers, +then run it from your terminal or backend. - + ```bash curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ @@ -314,9 +305,14 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ "params": { "from": "+15551234567", "to": "+15557654321", - "url": "https://example.com/swml/driver-on-the-way", - "status_url": "https://example.com/call-status", - "status_events": ["answered", "ended"] + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { "play": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } + ] + } + } } }' ``` @@ -334,9 +330,14 @@ client = RestClient( call = client.calling.dial( from_="+15551234567", to="+15557654321", - url="https://example.com/swml/driver-on-the-way", - status_url="https://example.com/call-status", - status_events=["answered", "ended"], + swml={ + "version": "1.0.0", + "sections": { + "main": [ + {"play": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} + ] + }, + }, ) print(call["id"]) ``` @@ -354,9 +355,14 @@ const client = new RestClient({ const call = await client.calling.dial({ from: "+15551234567", to: "+15557654321", - url: "https://example.com/swml/driver-on-the-way", - status_url: "https://example.com/call-status", - status_events: ["answered", "ended"], + swml: { + version: "1.0.0", + sections: { + main: [ + { play: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }, + ], + }, + }, }); console.log(call.id); ``` @@ -376,13 +382,13 @@ The response identifies the queued call: } ``` -Keep the `id` for later call commands. Your `status_url` receives the requested `answered` and -`ended` webhooks. See the REST client reference for [Python][sdk-rest-dial-python] or -[TypeScript][sdk-rest-dial-ts] for the full request schema. +Keep the `id` for later call commands. To receive progress webhooks, add `status_url` and +`status_events` as described in [Track the call's progress](#track-the-calls-progress). +The REST client references for [Python][sdk-rest-dial-python] and [TypeScript][sdk-rest-dial-ts] +cover all request fields. - + -Send the document as a JSON object in `swml` instead of hosting it at `url`. Pass per-call values in `custom_variables` and read them with [SWML expressions][swml-expressions] such as `${envs.driver_name}`: @@ -408,7 +414,8 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ }' ``` -`custom_variables` also works with SWML served from `url`. +To reuse a hosted SWML document, pass its `url` instead of `swml`. +`custom_variables` works with either approach. @@ -539,8 +546,9 @@ includes a complete page with media and call controls. ### Run an AI agent with SWML -Use this SWML in the [REST example](#place-a-call-with-rest) to replace the announcement with -an AI conversation. The [`ai` method][swml-ai] supplies the agent's greeting and instructions: +For an AI conversation, host the following SWML and replace `swml` in the +[REST example](#place-a-call-with-rest) with the document's `url`. +The [`ai` method][swml-ai] supplies the agent's greeting and instructions: ```yaml version: 1.0.0 From a05bfbc5bfdfa97b048324ced7500c9b8cc00a03 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 16:17:38 -0400 Subject: [PATCH 014/103] docs: add Relay SDK tabs to outbound calling examples --- .../pages/calling/voice/outbound-calling.mdx | 137 ++++++++++++++++-- 1 file changed, 121 insertions(+), 16 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index f3ab8bc24c..59c16d58bc 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -287,7 +287,7 @@ Bayview Taxi uses outbound calls to notify riders and connect them with dispatch Tell a rider their driver is on the way, using REST or Relay. - + #### Place a call with REST @@ -317,7 +317,7 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ }' ``` - + ```python from signalwire.rest import RestClient @@ -342,7 +342,7 @@ call = client.calling.dial( print(call["id"]) ``` - + ```typescript import { RestClient } from "@signalwire/sdk"; @@ -420,7 +420,7 @@ To reuse a hosted SWML document, pass its `url` instead of `swml`. - + #### Place a call with the Server SDK Relay client @@ -454,7 +454,7 @@ async def main(): ) action = await call.play([{ "type": "tts", - "params": {"text": "Hi, this is Bayview Taxi. Your driver is on the way."}, + "params": {"text": "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}, }]) await action.wait() await call.hangup() @@ -484,7 +484,7 @@ const call = await client.dial([[{ }, }]]); const action = await call.play([ - { type: "tts", text: "Hi, this is Bayview Taxi. Your driver is on the way." }, + { type: "tts", text: "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }, ]); await action.wait(); await call.hangup(); @@ -501,9 +501,15 @@ See the [Relay guide][relay-guide] for connection management and more call contr ### Forward an inbound call -Assign this SWML script to the SignalWire number that riders call. When a rider calls in, -SignalWire dials the dispatcher's phone and connects them: +Connect an incoming rider call to a dispatcher. Set the calling number to your SignalWire +number and the destination to the dispatcher's number. +- **SWML:** Assign the script to the number riders call, using the [forwarding guide][forward-calls]. +- **Server SDK:** Route that number's inbound calls to the Relay `dispatch` context and run the + handler on your backend. See the [Relay guide][relay-guide] for setup. + + + ```yaml version: 1.0.0 sections: @@ -512,10 +518,61 @@ sections: from: "+15551234567" to: "+15557654321" ``` + + +```python +from signalwire.relay import RelayClient + +client = RelayClient( + project="YOUR_PROJECT_ID", + token="YOUR_API_TOKEN", + host="YOUR_SPACE.signalwire.com", + contexts=["dispatch"], +) + +@client.on_call +async def handle_call(call): + await call.answer() + await call.connect([[{ + "type": "phone", + "params": { + "from_number": "+15551234567", + "to_number": "+15557654321", + }, + }]]) + +client.run() +``` + + +```typescript +import { RelayClient } from "@signalwire/sdk"; -Set `from` to your SignalWire number and `to` to the dispatcher's number. The -[forwarding guide][forward-calls] covers assigning the script and preserving the original caller ID. -Use `connect`'s `call_state_url` and `call_state_events` to track the outbound call. +const client = new RelayClient({ + project: "YOUR_PROJECT_ID", + token: "YOUR_API_TOKEN", + host: "YOUR_SPACE.signalwire.com", + contexts: ["dispatch"], +}); + +client.onCall(async (call) => { + await call.answer(); + await call.connect([[{ + type: "phone", + params: { + from_number: "+15551234567", + to_number: "+15557654321", + }, + }]]); +}); + +await client.run(); +``` + + + +The connect references for [Python](/docs/server-sdks/reference/python/relay/call/connect) and +[TypeScript](/docs/server-sdks/reference/typescript/relay/call/connect) cover additional Relay options. For SWML progress webhooks, use `call_state_url` and `call_state_events`. ### Place a call with the Browser SDK @@ -544,12 +601,17 @@ hangupButton.onclick = () => call.hangup(); Watch for `status$` to reach `connected`. The [Browser SDK outbound guide][browser-outbound] includes a complete page with media and call controls. -### Run an AI agent with SWML +### Run an AI agent -For an AI conversation, host the following SWML and replace `swml` in the -[REST example](#place-a-call-with-rest) with the document's `url`. -The [`ai` method][swml-ai] supplies the agent's greeting and instructions: +Replace the announcement with an AI conversation about the rider's pickup. +- **SWML:** Host the document below and use its `url` in the [REST request](#place-a-call-with-rest) + in place of the inline `swml`. +- **Server SDK:** In the [Relay example](#place-a-call-with-the-server-sdk-relay-client), replace + the playback and hangup steps with the SDK snippet below. `call` is the answered call returned by `dial`. + + + ```yaml version: 1.0.0 sections: @@ -565,10 +627,53 @@ sections: contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change. ``` + + +```python +# After dialing, start the agent on the answered call. +action = await call.ai( + ai_params={ + "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + "static_greeting_no_barge": True, + }, + prompt={ + "text": """You are calling to tell the rider their driver is about five minutes away. +Answer questions using that estimate. You cannot change bookings or +contact preferences. If asked, explain that the rider needs to contact +Bayview Taxi to make those changes. Do not claim to have made a change.""" + }, +) +await action.wait() +await call.hangup() +``` + + +```typescript +// After dialing, start the agent on the answered call. +const action = await call.ai({ + aiParams: { + static_greeting: "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + static_greeting_no_barge: true, + }, + prompt: { + text: `You are calling to tell the rider their driver is about five minutes away. +Answer questions using that estimate. You cannot change bookings or +contact preferences. If asked, explain that the rider needs to contact +Bayview Taxi to make those changes. Do not claim to have made a change.`, + }, +}); +await action.wait(); +await call.hangup(); +``` + + +The [`ai` method][swml-ai] uses the same greeting and prompt in each version. `static_greeting_no_barge` lets the greeting finish before the conversation begins. Add [tool calling][tool-calling] to let the agent take actions, such as updating a booking. -The [AI quickstart][ai-quickstart] covers serving an agent from your code. +See the [AI quickstart][ai-quickstart] or the Relay AI references for +[Python](/docs/server-sdks/reference/python/relay/call/ai) and +[TypeScript](/docs/server-sdks/reference/typescript/relay/call/ai). An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). From 78ac6050d6cf05ee67829bf80e5e19a1ca88bd83 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 16:24:32 -0400 Subject: [PATCH 015/103] docs: order outbound calling guide code-before-troubleshooting Move Examples ahead of progress tracking and troubleshooting so a new reader reaches working code before failure cases, and close the guide with Next steps cards. Replace the bold pseudo-label paragraphs and bullets with prose, drop the H4s nested inside tabs (their anchors were unreachable and absent from the TOC), and use one naming scheme for tab and code block titles so the REST/Relay choice follows the reader down the page. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 226 ++++++++++-------- 1 file changed, 120 insertions(+), 106 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 59c16d58bc..1abc4a4e61 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -43,18 +43,18 @@ Jump to the [examples](#examples) for working code or ## How outbound calls start -Your **server** can send a REST request to the [Calling API][calling-api] or dial over a -persistent Relay WebSocket connection. The Server SDK provides a client for each approach. +Most outbound calls start on your server: send a REST request to the [Calling API][calling-api], or +dial over a persistent Relay WebSocket connection. The Server SDK provides a client for each +approach. -**Forwarding an inbound call** places an outbound call to a second destination and connects the -two callers. Configure it with [SWML call instructions][swml-connect] or Call Flow Builder's -[Forward to Phone node][forward-node]. +Forwarding an inbound call also places an outbound call, dialing a second destination and +connecting the two callers. Configure it with [SWML call instructions][swml-connect] or Call Flow +Builder's [Forward to Phone node][forward-node]. -The **Browser SDK** lets a person dial and speak through your web app's microphone and speaker. -A button that starts an automated call instead sends the request to your backend. - -Registered SIP phones and phone systems can also dial through SignalWire using -[SIP credentials][sip-credentials]. +Calls can start on a device instead. The [Browser SDK][browser-outbound] dials from your web app so +a person speaks through their own microphone and speaker, and registered SIP phones and phone +systems dial through SignalWire using [SIP credentials][sip-credentials]. A browser button that +starts an automated call is a server call: it sends the request to your backend. ## Destinations you can call @@ -209,75 +209,6 @@ Server calls use your Project ID and an API token with voice permissions from th Browser calls use a [Subscriber Access Token (SAT)][browser-auth] that your backend issues for the user. Its permissions determine which destinations the user can call. -## Track the call's progress - - - - -A `200` response with a call `id` means SignalWire accepted the request. Listen for `answered` -to confirm pickup and `ended` to know the call finished. - -Set `status_url` to your webhook endpoint and choose `status_events`: `created`, `ringing`, -`answered`, or `ended`. If you omit `status_events`, you receive `ended` only. - - - - -The SDK's `dial` returns a call object after answer or raises `RelayError` on failure. -Use `call.on` for state and action events, and `action.wait()` to wait for an action such as -playback to finish. See the [Python](/docs/server-sdks/reference/python/relay/call/on) or -[TypeScript](/docs/server-sdks/reference/typescript/relay/call/on) event reference. - - -The initial `calling.dial` response acknowledges the request. A later `calling.call.dial` -event carries the dial outcome. The Server SDK waits for this event before returning a call object. - - - - - -## Troubleshoot outbound calls - -Start with the error returned by your API or SDK, then check the relevant case below. -Browser token setup is covered in [authentication][browser-auth]. - - - - -A `422` response includes an [error code][error-codes]. Check the field it identifies: -`not_purchased_or_verified` means that number is not purchased or verified for your project. -Also check the destination (`to`), call instructions (`url` or `swml`), and account balance. - - - - -Check the `RelayError`, confirm the client is connected, and review the device parameters. -Phone devices use `to_number` and `from_number`; SIP devices use `to` and `from`. -The [Relay guide][relay-guide] covers connection setup. - - - - -Check the ring `timeout`, [trial limits][trial-mode], and [international dialing settings][international]. -For REST calls, a `max_price_per_minute` below the route's price rejects the call before it rings. - - - - -Review the calling number and any caller ID override in your request or forwarding flow. -The [Caller ID and CNAM][caller-id] guide covers display settings. For reputation issues, see -[spam labels][spam-labels] and [STIR/SHAKEN][stir-shaken]. - - - - -Your `url` endpoint must return SWML and accept `POST` by default. Use `url_method` to change -the method or `fallback_url` to provide backup instructions. Send inline `swml` as a JSON object. -See [SWML webhook security][swml-webhook-security] for securing the endpoint. - - - - ## Examples Bayview Taxi uses outbound calls to notify riders and connect them with dispatchers. @@ -287,9 +218,7 @@ Bayview Taxi uses outbound calls to notify riders and connect them with dispatch Tell a rider their driver is on the way, using REST or Relay. - - -#### Place a call with REST + This request includes the SWML announcement. Replace the credentials and phone numbers, then run it from your terminal or backend. @@ -420,15 +349,13 @@ To reuse a hosted SWML document, pass its `url` instead of `swml`. - - -#### Place a call with the Server SDK Relay client + Run this on your server with your credentials and phone numbers. It dials the rider, plays the announcement after answer, waits for playback to finish, and hangs up. - + ```python import asyncio from signalwire.relay import RelayClient @@ -462,7 +389,7 @@ async def main(): asyncio.run(main()) ``` - + ```typescript import { RelayClient } from "@signalwire/sdk"; @@ -504,9 +431,10 @@ See the [Relay guide][relay-guide] for connection management and more call contr Connect an incoming rider call to a dispatcher. Set the calling number to your SignalWire number and the destination to the dispatcher's number. -- **SWML:** Assign the script to the number riders call, using the [forwarding guide][forward-calls]. -- **Server SDK:** Route that number's inbound calls to the Relay `dispatch` context and run the - handler on your backend. See the [Relay guide][relay-guide] for setup. +With SWML, assign the script below to the number riders call, following the +[forwarding guide][forward-calls]. With the Server SDK, route that number's inbound calls to the +Relay `dispatch` context and run the handler on your backend, as the [Relay guide][relay-guide] +describes. @@ -519,7 +447,7 @@ sections: to: "+15557654321" ``` - + ```python from signalwire.relay import RelayClient @@ -544,7 +472,7 @@ async def handle_call(call): client.run() ``` - + ```typescript import { RelayClient } from "@signalwire/sdk"; @@ -603,12 +531,18 @@ includes a complete page with media and call controls. ### Run an AI agent -Replace the announcement with an AI conversation about the rider's pickup. +Replace the announcement with an AI conversation about the rider's pickup. Both versions build on +[Place a call from a server](#place-a-call-from-a-server). With SWML, host the document below and +pass its `url` in place of the inline `swml`. With the Server SDK, replace the playback and hangup +steps with the snippet below, where `call` is the answered call returned by `dial`. -- **SWML:** Host the document below and use its `url` in the [REST request](#place-a-call-with-rest) - in place of the inline `swml`. -- **Server SDK:** In the [Relay example](#place-a-call-with-the-server-sdk-relay-client), replace - the playback and hangup steps with the SDK snippet below. `call` is the answered call returned by `dial`. + +An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). +Check consent, your do-not-call list, and the local calling hours in your code before you send the +`dial` request. The [TCPA guide][tcpa] and +the compliance section of [AI best practices][ai-best-practices] cover each obligation. This is +technical guidance, not legal advice. + @@ -628,7 +562,7 @@ sections: Bayview Taxi to make those changes. Do not claim to have made a change. ``` - + ```python # After dialing, start the agent on the answered call. action = await call.ai( @@ -647,7 +581,7 @@ await action.wait() await call.hangup() ``` - + ```typescript // After dialing, start the agent on the answered call. const action = await call.ai({ @@ -675,10 +609,90 @@ See the [AI quickstart][ai-quickstart] or the Relay AI references for [Python](/docs/server-sdks/reference/python/relay/call/ai) and [TypeScript](/docs/server-sdks/reference/typescript/relay/call/ai). - -An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). -Check consent, your do-not-call list, and the local calling hours in your code before you send the -`dial` request. The [TCPA guide][tcpa] and -the compliance section of [AI best practices][ai-best-practices] cover each obligation. This is -technical guidance, not legal advice. - +## Track the call's progress + +Confirm SignalWire accepted the request, then follow the call to answer and hangup. + + + + +A `200` response with a call `id` means SignalWire accepted the request. Listen for `answered` +to confirm pickup and `ended` to know the call finished. + +Set `status_url` to your webhook endpoint and choose `status_events`: `created`, `ringing`, +`answered`, or `ended`. If you omit `status_events`, you receive `ended` only. + + + + +The SDK's `dial` returns a call object after answer or raises `RelayError` on failure. +Use `call.on` for state and action events, and `action.wait()` to wait for an action such as +playback to finish. See the [Python](/docs/server-sdks/reference/python/relay/call/on) or +[TypeScript](/docs/server-sdks/reference/typescript/relay/call/on) event reference. + + +The initial `calling.dial` response acknowledges the request. A later `calling.call.dial` +event carries the dial outcome. The Server SDK waits for this event before returning a call object. + + + + + +## Troubleshoot outbound calls + +Start with the error returned by your API or SDK, then check the relevant case below. +Browser token setup is covered in [authentication][browser-auth]. + + + + +A `422` response includes an [error code][error-codes]. Check the field it identifies: +`not_purchased_or_verified` means that number is not purchased or verified for your project. +Also check the destination (`to`), call instructions (`url` or `swml`), and account balance. + + + + +Check the `RelayError`, confirm the client is connected, and review the device parameters. +Phone devices use `to_number` and `from_number`; SIP devices use `to` and `from`. +The [Relay guide][relay-guide] covers connection setup. + + + + +Check the ring `timeout`, [trial limits][trial-mode], and [international dialing settings][international]. +For REST calls, a `max_price_per_minute` below the route's price rejects the call before it rings. + + + + +Review the calling number and any caller ID override in your request or forwarding flow. +The [Caller ID and CNAM][caller-id] guide covers display settings. For reputation issues, see +[spam labels][spam-labels] and [STIR/SHAKEN][stir-shaken]. + + + + +Your `url` endpoint must return SWML and accept `POST` by default. Use `url_method` to change +the method or `fallback_url` to provide backup instructions. Send inline `swml` as a JSON object. +See [SWML webhook security][swml-webhook-security] for securing the endpoint. + + + + +## Next steps + + + + Every `dial` field, plus the commands that control a call already in progress. + + + Connection management and event-driven call control from your backend. + + + A complete web page with media handling and call controls. + + + Build the agent that holds the conversation once someone answers. + + From 5a2522b5afebddba2f35cba6ed41be71ec1c5d97 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 16:29:50 -0400 Subject: [PATCH 016/103] docs: drop "Call Fabric" from the outbound calling and Vapi guides "Call Fabric" names the platform, not a kind of destination. The destination table now lists an application or room and a Subscriber, and both pages say "resource address". Co-Authored-By: Claude Opus 5 (1M context) --- .../platform/pages/ai/guides/Integrations/vapi.mdx | 2 +- .../platform/pages/calling/voice/outbound-calling.mdx | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/fern/products/platform/pages/ai/guides/Integrations/vapi.mdx b/fern/products/platform/pages/ai/guides/Integrations/vapi.mdx index 6f52503a0b..5c1edaef91 100644 --- a/fern/products/platform/pages/ai/guides/Integrations/vapi.mdx +++ b/fern/products/platform/pages/ai/guides/Integrations/vapi.mdx @@ -170,7 +170,7 @@ After saving your SWML script, you'll need to create a SIP address that VAPI can After configuration, note down your unique SIP domain app. It will look something like: `test-space-vapi.dapp.signalwire.com` -Learn more about addresses in our [Call Fabric Addresses documentation][addresses-guide]. +The [Addresses documentation][addresses-guide] covers resource addresses in more detail. To complete this setup, you must contact SignalWire Support to generate a password for your SIP domain app. VAPI needs this password for authentication. diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 1abc4a4e61..2fced6f7bd 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -65,11 +65,12 @@ resource in your SignalWire Space. |---|---|---| | Phone number | E.164, such as `+15557654321` | A mobile or landline phone over the PSTN. | | SIP endpoint | SIP URI, such as `sip:dispatcher@example.com` | A SIP phone, PBX, or another provider's SIP service. | -| Call Fabric application or room | Resource address, such as `/public/dispatch-agent` | An AI agent, SWML application, or room in your Space. | -| Call Fabric user | Resource address, such as `/private/dispatcher` | A registered user (Subscriber) in your Space. | +| Application or room | Resource address, such as `/public/dispatch-agent` | An AI agent, SWML application, or room in your Space. | +| Subscriber | Resource address, such as `/private/dispatcher` | A registered user in your Space. | Applications, rooms, and Subscribers are [Resources][resources]. Dial their configured -[address][resource-addresses], including the `public` or `private` context, to reach them directly. +[resource address][resource-addresses], including the `public` or `private` context, to reach them +directly. ### Support by API and SDK From 0c9bb169641d46177eac95ca0213173f4e9ab979 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 16:31:59 -0400 Subject: [PATCH 017/103] docs: remove the outbound calling troubleshooting section Keep the spam-label and STIR/SHAKEN pointers, which are prerequisites for getting answered rather than diagnostics, in the calling-number section, and drop the link definitions left with no home. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 50 ++----------------- 1 file changed, 4 insertions(+), 46 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 2fced6f7bd..e4d336e461 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -21,9 +21,7 @@ max-toc-depth: 3 [international]: /docs/platform/how-to-enable-international-services [api-credentials]: /docs/platform/your-signalwire-api-space [phone-numbers]: /docs/platform/phone-numbers -[error-codes]: /docs/apis/error-codes [swml-ai]: /docs/swml/reference/calling/ai -[swml-webhook-security]: /docs/swml/guides/webhook-security [swml-expressions]: /docs/swml/reference/expressions [ai-quickstart]: /docs/platform/ai/quickstart [tool-calling]: /docs/platform/ai/tool-calling @@ -38,8 +36,7 @@ max-toc-depth: 3 Outbound calls let your app reach a customer, connect a caller to a dispatcher, or start an AI conversation. You choose the destination and what happens on the call. -Jump to the [examples](#examples) for working code or -[troubleshooting](#troubleshoot-outbound-calls) to diagnose a failed call. +Jump to the [examples](#examples) for working code. ## How outbound calls start @@ -197,6 +194,9 @@ For server calls to phone numbers, use a [purchased number][phone-numbers] or [verified caller ID][caller-id]. Pass it as `from` in REST or `params.from_number` for a Relay phone device. SIP caller ID settings are covered in the [Calling API reference][calling-api]. +How that number is labeled decides whether people answer. The [spam labels][spam-labels] and +[STIR/SHAKEN][stir-shaken] guides cover call reputation and attestation. + [Trial projects][trial-mode] can call only purchased and verified numbers, with no international calling. Other projects need [international dialing enabled][international] to call other countries. @@ -639,48 +639,6 @@ event carries the dial outcome. The Server SDK waits for this event before retur -## Troubleshoot outbound calls - -Start with the error returned by your API or SDK, then check the relevant case below. -Browser token setup is covered in [authentication][browser-auth]. - - - - -A `422` response includes an [error code][error-codes]. Check the field it identifies: -`not_purchased_or_verified` means that number is not purchased or verified for your project. -Also check the destination (`to`), call instructions (`url` or `swml`), and account balance. - - - - -Check the `RelayError`, confirm the client is connected, and review the device parameters. -Phone devices use `to_number` and `from_number`; SIP devices use `to` and `from`. -The [Relay guide][relay-guide] covers connection setup. - - - - -Check the ring `timeout`, [trial limits][trial-mode], and [international dialing settings][international]. -For REST calls, a `max_price_per_minute` below the route's price rejects the call before it rings. - - - - -Review the calling number and any caller ID override in your request or forwarding flow. -The [Caller ID and CNAM][caller-id] guide covers display settings. For reputation issues, see -[spam labels][spam-labels] and [STIR/SHAKEN][stir-shaken]. - - - - -Your `url` endpoint must return SWML and accept `POST` by default. Use `url_method` to change -the method or `fallback_url` to provide backup instructions. Send inline `swml` as a JSON object. -See [SWML webhook security][swml-webhook-security] for securing the endpoint. - - - - ## Next steps From c432bd12f98507cff3276fa1392bd025dbf99ba6 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 16:52:07 -0400 Subject: [PATCH 018/103] docs: remove the "Personalize each call" accordion Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index e4d336e461..ad391cf25b 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -22,7 +22,6 @@ max-toc-depth: 3 [api-credentials]: /docs/platform/your-signalwire-api-space [phone-numbers]: /docs/platform/phone-numbers [swml-ai]: /docs/swml/reference/calling/ai -[swml-expressions]: /docs/swml/reference/expressions [ai-quickstart]: /docs/platform/ai/quickstart [tool-calling]: /docs/platform/ai/tool-calling [ai-best-practices]: /docs/platform/ai/best-practices @@ -317,38 +316,6 @@ Keep the `id` for later call commands. To receive progress webhooks, add `status The REST client references for [Python][sdk-rest-dial-python] and [TypeScript][sdk-rest-dial-ts] cover all request fields. - - -Pass per-call values in `custom_variables` and read them with [SWML expressions][swml-expressions] -such as `${envs.driver_name}`: - -```bash -curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ - -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "command": "dial", - "params": { - "from": "+15551234567", - "to": "+15557654321", - "custom_variables": { "driver_name": "Sam", "eta_minutes": "5" }, - "swml": { - "version": "1.0.0", - "sections": { - "main": [ - { "play": "say:Hi, this is Bayview Taxi. ${envs.driver_name} is on the way and will arrive in about ${envs.eta_minutes} minutes." } - ] - } - } - } - }' -``` - -To reuse a hosted SWML document, pass its `url` instead of `swml`. -`custom_variables` works with either approach. - - - From 851fec8314beed9e6f60a4d73350f59c352c0f6f Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 16:59:17 -0400 Subject: [PATCH 019/103] docs: collapse the outbound destination table to one Resource row Every Resource type is dialed the same way, so the table lists Resource once and names the common types it reaches, conference rooms included. Say plainly that queues and media streams are not Resources and only SWML connect reaches them. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index ad391cf25b..66309c3504 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -54,26 +54,24 @@ starts an automated call is a server call: it sends the request to your backend. ## Destinations you can call -A destination can be a phone on the public switched telephone network (PSTN), a SIP endpoint, or a -resource in your SignalWire Space. +A destination is a phone on the public switched telephone network (PSTN), a SIP endpoint, or a +Resource in your SignalWire Space. | Destination | Address format | What you reach | |---|---|---| | Phone number | E.164, such as `+15557654321` | A mobile or landline phone over the PSTN. | | SIP endpoint | SIP URI, such as `sip:dispatcher@example.com` | A SIP phone, PBX, or another provider's SIP service. | -| Application or room | Resource address, such as `/public/dispatch-agent` | An AI agent, SWML application, or room in your Space. | -| Subscriber | Resource address, such as `/private/dispatcher` | A registered user in your Space. | +| Resource | Resource address, such as `/public/dispatch-agent` | An AI agent, SWML or cXML application, Call Flow, Subscriber, or conference room in your Space. | -Applications, rooms, and Subscribers are [Resources][resources]. Dial their configured -[resource address][resource-addresses], including the `public` or `private` context, to reach them -directly. +Every [Resource][resources] is reachable the same way, whatever its type. Dial its configured +[resource address][resource-addresses], including the `public` or `private` context. ### Support by API and SDK -The Calling API and Server SDK REST clients accept all four destination types above in `to`. +The Calling API and Server SDK REST clients accept all three destination types above in `to`. Set the calling address in `from`. See the [Calling API reference][calling-api] for all fields. @@ -90,7 +88,7 @@ Each inner list rings together; the outer list sets the order of attempts. The -The **Browser SDK** and **SWML `connect`** accept all four types above. Call Flow Builder's +The **Browser SDK** and **SWML `connect`** accept all three types above. Call Flow Builder's **Forward to Phone** node supports phone numbers and SIP endpoints. @@ -104,7 +102,8 @@ The request still needs `from` and call-handling instructions in `url` or `swml` SWML `connect` also supports [queues](/docs/swml/reference/calling/connect#connect-to-a-queue-with-transfer-after-bridge) (`queue:support`, with the required `transfer_after_bridge`) and [WebSocket media streams](/docs/swml/reference/calling/connect#connect-to-a-websocket-stream) -(`stream:wss://example.com/audio`). Follow the linked examples for their connection settings. +(`stream:wss://example.com/audio`). Neither is a Resource, so neither has a resource address, and +no other API or SDK dials them. Follow the linked examples for their connection settings. From cba9fcad1fe42135dd0a0c30a80a48102e9e07a0 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 17:08:17 -0400 Subject: [PATCH 020/103] docs: map outbound call statuses to their webhook events The dial response's queued status is not a status_events value, so a reader waiting on a "queued" webhook waits forever. Name the status the response reports, say that created is the first subscribable event, and add the status-to-event table. Flags one gap with NEEDS SOURCE: nothing in the repo says which event, if any, accompanies failed, canceled, or completed. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 66309c3504..c0925dc6dc 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -310,8 +310,10 @@ The response identifies the queued call: } ``` -Keep the `id` for later call commands. To receive progress webhooks, add `status_url` and -`status_events` as described in [Track the call's progress](#track-the-calls-progress). +`queued` means SignalWire accepted the call but has not placed it yet, so no webhook has fired. +The first event you can subscribe to is `created`. Keep the `id` for later call commands, and add +`status_url` and `status_events` as described in +[Track the call's progress](#track-the-calls-progress). The REST client references for [Python][sdk-rest-dial-python] and [TypeScript][sdk-rest-dial-ts] cover all request fields. @@ -589,6 +591,26 @@ to confirm pickup and `ended` to know the call finished. Set `status_url` to your webhook endpoint and choose `status_events`: `created`, `ringing`, `answered`, or `ended`. If you omit `status_events`, you receive `ended` only. +A call reports more statuses than it fires events for. Each of the four event names matches the +`status` the call reports when that event arrives: + +| `status` | Webhook event | +|---|---| +| `queued`, `initiated` | None — the call exists but has not been placed | +| `created` | `created` | +| `ringing` | `ringing` | +| `answered` | `answered` | +| `ending` | None | +| `ended` | `ended` | + +So the `queued` status in a `dial` response is not a missed webhook. Nothing fires until the call +reaches `created`. + +A call also reports `failed`, `canceled`, and `completed`. +[NEEDS SOURCE: whether a call that ends in `failed`, `canceled`, or `completed` delivers the +`ended` webhook, or no webhook at all. The `status_events` enum defines only four event names, and +no spec or reference in this repo maps these three terminal statuses onto them.] + From 29a9f56154e87ff7c69455c4c7ea77c63249dd3e Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 17:14:17 -0400 Subject: [PATCH 021/103] docs: source outbound call statuses from the Calling API implementation Verified against prime-rails at 02473273. The status webhook fires only when the call state string is itself listed in status_events, so the event name and the status are the same value and there is no mapping. Replaces the NEEDS SOURCE with the answer: busy, no-answer, failed, and canceled deliver no callback at all, so a call that never connects is silent. Adds busy and no-answer, which the table was missing. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index c0925dc6dc..5192f8275a 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -591,25 +591,27 @@ to confirm pickup and `ended` to know the call finished. Set `status_url` to your webhook endpoint and choose `status_events`: `created`, `ringing`, `answered`, or `ended`. If you omit `status_events`, you receive `ended` only. -A call reports more statuses than it fires events for. Each of the four event names matches the -`status` the call reports when that event arrives: +Each event name is also a `status` the call reports, so a webhook arrives when the call enters the +state you subscribed to. A call passes through more states than there are events: -| `status` | Webhook event | +| `status` | Fires a webhook | |---|---| -| `queued`, `initiated` | None — the call exists but has not been placed | -| `created` | `created` | -| `ringing` | `ringing` | -| `answered` | `answered` | -| `ending` | None | -| `ended` | `ended` | - -So the `queued` status in a `dial` response is not a missed webhook. Nothing fires until the call -reaches `created`. - -A call also reports `failed`, `canceled`, and `completed`. -[NEEDS SOURCE: whether a call that ends in `failed`, `canceled`, or `completed` delivers the -`ended` webhook, or no webhook at all. The `status_events` enum defines only four event names, and -no spec or reference in this repo maps these three terminal statuses onto them.] +| `queued` | No — accepted, not yet placed | +| `initiated` | No — handed off to be placed | +| `created` | Yes, as `created` | +| `ringing` | Yes, as `ringing` | +| `answered` | Yes, as `answered` | +| `ending` | No | +| `ended` | Yes, as `ended` | +| `busy`, `no-answer`, `failed`, `canceled` | No | + + +`busy`, `no-answer`, `failed`, and `canceled` are not `status_events` values, so none of them +delivers a callback — not even under the default `status_events` of `["ended"]`. Subscribe to +`answered` and pair it with the ring `timeout` you set, so a call that never connects shows up as +an `answered` webhook that never arrives. To confirm what happened, read the call's +[voice log](/docs/apis/rest/voice-logs/list-voice-logs). + From 0d2ada042f7ae592a8250951822feb0abc37d5b9 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 17:23:51 -0400 Subject: [PATCH 022/103] docs: remove the "Additional destination options" accordion Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 5192f8275a..f093d990e2 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -91,22 +91,6 @@ Each inner list rings together; the outer list sets the order of attempts. The The **Browser SDK** and **SWML `connect`** accept all three types above. Call Flow Builder's **Forward to Phone** node supports phone numbers and SIP endpoints. - - -The [Calling API][calling-api] also accepts `sips:` URIs and full, registered Verto addresses -such as `verto:dispatcher@YOUR_SPACE.verto.signalwire.com`. - -Use `to_script` to run SWML at the destination, supplying an inline document or its URL. -The request still needs `from` and call-handling instructions in `url` or `swml`; `to` can be omitted. - -SWML `connect` also supports [queues](/docs/swml/reference/calling/connect#connect-to-a-queue-with-transfer-after-bridge) -(`queue:support`, with the required `transfer_after_bridge`) and -[WebSocket media streams](/docs/swml/reference/calling/connect#connect-to-a-websocket-stream) -(`stream:wss://example.com/audio`). Neither is a Resource, so neither has a resource address, and -no other API or SDK dials them. Follow the linked examples for their connection settings. - - - ## How the call runs With REST, SignalWire runs the instructions you supply. With Relay, your running application From 3d9495a6c903c31fc213b462fd5f2c18ea4886ee Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 17:25:27 -0400 Subject: [PATCH 023/103] docs: cut per-interface field plumbing from the destinations section Which parameter carries the destination in each client belongs in the reference, not in a starter guide. Keeps the one fact a beginner can trip over: the Relay client dials phone and SIP only, not resource addresses. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 28 ++----------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index f093d990e2..920255dab6 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -8,8 +8,6 @@ max-toc-depth: 3 [calling-api]: /docs/apis/rest/calls/call-commands [sdk-rest-dial-python]: /docs/server-sdks/reference/python/rest/calling/dial [sdk-rest-dial-ts]: /docs/server-sdks/reference/typescript/rest/calling/dial -[relay-dial-python]: /docs/server-sdks/reference/python/relay/client/dial -[relay-dial-ts]: /docs/server-sdks/reference/typescript/relay/client/dial [relay-guide]: /docs/server-sdks/guides/relay-client [browser-outbound]: /docs/browser-sdk/v4/guides/outbound-calls [browser-auth]: /docs/browser-sdk/v4/guides/authentication @@ -66,30 +64,8 @@ Resource in your SignalWire Space. Every [Resource][resources] is reachable the same way, whatever its type. Dial its configured [resource address][resource-addresses], including the `public` or `private` context. -### Support by API and SDK - - - - -The Calling API and Server SDK REST clients accept all three destination types above in `to`. -Set the calling address in `from`. See the [Calling API reference][calling-api] for all fields. - - - - -The Relay client accepts phone and SIP devices in a nested `devices` list: - -- **Phone:** `type: "phone"`, with `params.to_number` and `params.from_number`. -- **SIP:** `type: "sip"`, with `params.to` and `params.from`. - -Each inner list rings together; the outer list sets the order of attempts. The -[Python][relay-dial-python] and [TypeScript][relay-dial-ts] references cover device options. - - - - -The **Browser SDK** and **SWML `connect`** accept all three types above. Call Flow Builder's -**Forward to Phone** node supports phone numbers and SIP endpoints. +The Relay client is the one exception: it dials phone numbers and SIP endpoints only, not +resource addresses. ## How the call runs From f0aeae6866369c750dab25c1db33176d891d19af Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 17:32:05 -0400 Subject: [PATCH 024/103] docs: drop the running scenario from the outbound examples prose Each example now says what it does rather than what a fictional taxi company does with it, so a reader matches an example to their own task without carrying the story. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 920255dab6..57bf5b375b 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -83,7 +83,7 @@ This flow uses `answered` and `ended` webhooks: -Your code sends a dial request with from, to, and SWML. SignalWire returns a call id with status queued and rings the rider's phone. When the rider answers, SignalWire sends an answered webhook and runs your SWML. When the call finishes, SignalWire sends an ended webhook. +Your code sends a dial request with from, to, and SWML. SignalWire returns a call id with status queued and rings the destination. When the person answers, SignalWire sends an answered webhook and runs your SWML. When the call finishes, SignalWire sends an ended webhook. @@ -170,11 +170,11 @@ the user. Its permissions determine which destinations the user can call. ## Examples -Bayview Taxi uses outbound calls to notify riders and connect them with dispatchers. +Working code for the four things people build first. ### Place a call from a server -Tell a rider their driver is on the way, using REST or Relay. +Place a call that plays a spoken announcement when someone answers, using REST or Relay. @@ -280,7 +280,7 @@ cover all request fields. -Run this on your server with your credentials and phone numbers. It dials the rider, plays +Run this on your server with your credentials and phone numbers. It dials the destination, plays the announcement after answer, waits for playback to finish, and hangs up. @@ -357,10 +357,10 @@ See the [Relay guide][relay-guide] for connection management and more call contr ### Forward an inbound call -Connect an incoming rider call to a dispatcher. Set the calling number to your SignalWire -number and the destination to the dispatcher's number. +Answer a call to one of your numbers and connect it to a second destination. Set the calling +number to your SignalWire number and the destination to whoever should take the call. -With SWML, assign the script below to the number riders call, following the +With SWML, assign the script below to the number people call, following the [forwarding guide][forward-calls]. With the Server SDK, route that number's inbound calls to the Relay `dispatch` context and run the handler on your backend, as the [Relay guide][relay-guide] describes. @@ -433,12 +433,12 @@ The connect references for [Python](/docs/server-sdks/reference/python/relay/cal ### Place a call with the Browser SDK -Let the dispatcher speak to the rider through your web app. Supply a +Let someone place and speak on a call from your web app. Supply a [Subscriber Access Token][browser-auth] as `SAT`, with permission to call phone numbers. Use an HTTPS page with `` and ``. Run the dialing code from a user action, such as a -**Call rider** button. +**Call** button. ```typescript import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; @@ -460,7 +460,7 @@ includes a complete page with media and call controls. ### Run an AI agent -Replace the announcement with an AI conversation about the rider's pickup. Both versions build on +Replace the announcement with an AI agent that holds a conversation. Both versions build on [Place a call from a server](#place-a-call-from-a-server). With SWML, host the document below and pass its `url` in place of the inline `swml`. With the Server SDK, replace the playback and hangup steps with the snippet below, where `call` is the answered call returned by `dial`. From 1a924a9b7453122944a4eedac4eb72215269409f Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 17:36:10 -0400 Subject: [PATCH 025/103] docs: restructure "Before you dial" as steps with decision tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three ordered steps — calling address, credential, project limits — each answering "for what I'm dialing, what do I need" in a table instead of prose a reader has to filter. Drops the last of the per-client field plumbing. Calling-address rules verified against the Calling API's own destination validation: E.164 for PSTN, E.164 or SIP URI or a short token for SIP, unchecked for a resource address. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 57bf5b375b..072a207b8e 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -146,27 +146,46 @@ flowchart TB ## Before you dial -### Calling number + -For server calls to phone numbers, use a [purchased number][phone-numbers] or -[verified caller ID][caller-id]. Pass it as `from` in REST or `params.from_number` for a Relay -phone device. SIP caller ID settings are covered in the [Calling API reference][calling-api]. + -How that number is labeled decides whether people answer. The [spam labels][spam-labels] and -[STIR/SHAKEN][stir-shaken] guides cover call reputation and attestation. +What the call can present as its caller depends on where you are calling. - -[Trial projects][trial-mode] can call only purchased and verified numbers, with no international -calling. Other projects need [international dialing enabled][international] to call other countries. - +| Dialing | Your calling address must be | +|---|---| +| A phone number | A [purchased number][phone-numbers] or [verified caller ID][caller-id], in E.164, with voice enabled | +| A SIP endpoint | An E.164 number, a SIP URI, or a short caller ID token of up to 11 letters, digits, or underscores | +| A Resource | Anything — the format is not checked, though one of your own numbers is the safe choice | + +Whichever you use, how it is labeled decides whether people answer. The [spam labels][spam-labels] +and [STIR/SHAKEN][stir-shaken] guides cover call reputation and attestation. + + + + + +Which credential you need depends on where the dialing code runs. + +| Dialing from | Use | +|---|---| +| Your server | Your Project ID and an API token with voice permissions, from the Dashboard's [API credentials][api-credentials] page | +| A browser | A [Subscriber Access Token (SAT)][browser-auth] that your backend issues for the signed-in user | + +Keep the API token on your backend. A Subscriber Access Token's permissions decide which +destinations that user can call, so scope it to what the person should reach. + + + + -### Credentials +[Trial projects][trial-mode] can call only purchased and verified numbers, and cannot call +internationally at all. Every other project needs +[international dialing enabled][international] before it can call another country. -Server calls use your Project ID and an API token with voice permissions from the Dashboard's -[API credentials][api-credentials] page. Keep the API token on your backend. + -Browser calls use a [Subscriber Access Token (SAT)][browser-auth] that your backend issues for -the user. Its permissions determine which destinations the user can call. + ## Examples From 70374b93fa9217dce4e0de767690576fded99ce0 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 17:38:29 -0400 Subject: [PATCH 026/103] docs: keep "address" for destinations, "caller ID" for the caller An address is one of the ways a destination is identified, so calling the from side an "address" made the two read as one thing. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 072a207b8e..96224e3f50 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -148,18 +148,20 @@ flowchart TB - + -What the call can present as its caller depends on where you are calling. +Every call presents a caller ID to whoever it reaches. What that can be depends on the +destination you dial. -| Dialing | Your calling address must be | +| Dialing | Your caller ID must be | |---|---| -| A phone number | A [purchased number][phone-numbers] or [verified caller ID][caller-id], in E.164, with voice enabled | -| A SIP endpoint | An E.164 number, a SIP URI, or a short caller ID token of up to 11 letters, digits, or underscores | +| A phone number | A [purchased number][phone-numbers] or [verified caller ID][caller-id] in E.164, with voice enabled | +| A SIP endpoint | An E.164 number, a SIP URI, or a short token of up to 11 letters, digits, or underscores | | A Resource | Anything — the format is not checked, though one of your own numbers is the safe choice | -Whichever you use, how it is labeled decides whether people answer. The [spam labels][spam-labels] -and [STIR/SHAKEN][stir-shaken] guides cover call reputation and attestation. +However you set it, how that caller ID is labeled decides whether people answer. The +[spam labels][spam-labels] and [STIR/SHAKEN][stir-shaken] guides cover call reputation and +attestation. From f2f202e39ba587ecfee2a9c183aa94a5f1a95fb6 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 17:50:26 -0400 Subject: [PATCH 027/103] docs: key the first outbound step on the destination, not the caller ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The step is a decision about what you're dialing; the caller ID rule is what follows from it. Also fixes the phone-number row, which read as if you needed to own a number in order to dial one — the requirement is on the caller ID the call presents. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 96224e3f50..f661e7c26e 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -148,20 +148,22 @@ flowchart TB - + -Every call presents a caller ID to whoever it reaches. What that can be depends on the -destination you dial. +Pick the destination first. It decides what the call has to present as its caller ID. -| Dialing | Your caller ID must be | +| Destination | Caller ID it requires | |---|---| -| A phone number | A [purchased number][phone-numbers] or [verified caller ID][caller-id] in E.164, with voice enabled | +| A phone number | A voice-capable PSTN number in your project, either one you [bought from SignalWire][phone-numbers] or one you [verified as a caller ID][caller-id], in E.164 | | A SIP endpoint | An E.164 number, a SIP URI, or a short token of up to 11 letters, digits, or underscores | -| A Resource | Anything — the format is not checked, though one of your own numbers is the safe choice | +| A Resource | Not checked — any value is accepted, though a number in your project is still the safe choice | -However you set it, how that caller ID is labeled decides whether people answer. The -[spam labels][spam-labels] and [STIR/SHAKEN][stir-shaken] guides cover call reputation and -attestation. +Calling a phone number is the strict case, because SignalWire originates that call on the PSTN as +your number and has to be entitled to use it. The other destinations aren't PSTN originations, so +they accept more. + +How that caller ID is labeled decides whether people answer. The [spam labels][spam-labels] and +[STIR/SHAKEN][stir-shaken] guides cover call reputation and attestation. From e518430d4967a34e910f0ce7fac827c3f3bdc002 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 18:13:39 -0400 Subject: [PATCH 028/103] docs: name outbound destinations by what they are, not by "Resource" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the single Resource row into the things people recognize — a registered SIP phone, a Subscriber, an application, a conference room — with the platform term alongside and a real address in each row, so the Resource concept lands from the table instead of ahead of it. Keys the caller ID table on the address form dialed, which is what the API actually branches on. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index f661e7c26e..a025cd2a2a 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -52,17 +52,21 @@ starts an automated call is a server call: it sends the request to your backend. ## Destinations you can call -A destination is a phone on the public switched telephone network (PSTN), a SIP endpoint, or a -Resource in your SignalWire Space. +A call can reach a phone anywhere on the public switched telephone network (PSTN), a SIP endpoint +anywhere on the internet, or anything you have set up inside your own SignalWire Space. -| Destination | Address format | What you reach | +| What you want to reach | Dial | Example | |---|---|---| -| Phone number | E.164, such as `+15557654321` | A mobile or landline phone over the PSTN. | -| SIP endpoint | SIP URI, such as `sip:dispatcher@example.com` | A SIP phone, PBX, or another provider's SIP service. | -| Resource | Resource address, such as `/public/dispatch-agent` | An AI agent, SWML or cXML application, Call Flow, Subscriber, or conference room in your Space. | +| A mobile or landline phone | Its number in E.164 | `+15557654321` | +| A SIP phone, PBX, or another provider's SIP service | Its SIP URI | `sip:support@example.com` | +| A SIP phone registered to your Space with [SIP credentials][sip-credentials] | The endpoint's resource address | `/private/desk-phone` | +| One of your own users, called a Subscriber | The Subscriber's resource address | `/private/support-rep` | +| An AI agent, a SWML or cXML script, or a Call Flow | The application's resource address | `/public/support-agent` | +| A conference room | The room's resource address | `/public/team-standup` | -Every [Resource][resources] is reachable the same way, whatever its type. Dial its configured -[resource address][resource-addresses], including the `public` or `private` context. +The last four rows are all [Resources][resources]: things you create in your Space, each reachable +at a [resource address][resource-addresses] shaped `/context/name`, where the context is `public` +or `private`. Whatever the type, you dial it the same way. The Relay client is the one exception: it dials phone numbers and SIP endpoints only, not resource addresses. @@ -155,8 +159,8 @@ Pick the destination first. It decides what the call has to present as its calle | Destination | Caller ID it requires | |---|---| | A phone number | A voice-capable PSTN number in your project, either one you [bought from SignalWire][phone-numbers] or one you [verified as a caller ID][caller-id], in E.164 | -| A SIP endpoint | An E.164 number, a SIP URI, or a short token of up to 11 letters, digits, or underscores | -| A Resource | Not checked — any value is accepted, though a number in your project is still the safe choice | +| A SIP URI | An E.164 number, a SIP URI, or a short token of up to 11 letters, digits, or underscores | +| A resource address | Not checked — any value is accepted, though a number in your project is still the safe choice | Calling a phone number is the strict case, because SignalWire originates that call on the PSTN as your number and has to be entitled to use it. The other destinations aren't PSTN originations, so From 61cf222b6bf3fbca6ce98890ef5662cf048ad070 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 18:18:49 -0400 Subject: [PATCH 029/103] docs: state the two ways an outbound call starts, without the plumbing The section named the Calling API, Relay WebSockets, Server SDK clients, the Browser SDK, SIP credentials, and a Call Flow Builder node before a reader knows what any of them are. There are two ways a call starts, so say that; the interfaces arrive later where they matter. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index a025cd2a2a..c01bfddf56 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -5,7 +5,6 @@ description: Learn how outbound calls start, which destinations each API and SDK max-toc-depth: 3 --- -[calling-api]: /docs/apis/rest/calls/call-commands [sdk-rest-dial-python]: /docs/server-sdks/reference/python/rest/calling/dial [sdk-rest-dial-ts]: /docs/server-sdks/reference/typescript/rest/calling/dial [relay-guide]: /docs/server-sdks/guides/relay-client @@ -24,9 +23,7 @@ max-toc-depth: 3 [tool-calling]: /docs/platform/ai/tool-calling [ai-best-practices]: /docs/platform/ai/best-practices [tcpa]: /docs/platform/compliance/tcpa -[swml-connect]: /docs/swml/reference/calling/connect [forward-calls]: /docs/swml/guides/forward-calls -[forward-node]: /docs/call-flow-builder/reference/forward-to-phone [resource-addresses]: /docs/platform/addresses [resources]: /docs/platform/resources @@ -37,18 +34,15 @@ Jump to the [examples](#examples) for working code. ## How outbound calls start -Most outbound calls start on your server: send a REST request to the [Calling API][calling-api], or -dial over a persistent Relay WebSocket connection. The Server SDK provides a client for each -approach. +An outbound call starts one of two ways: from a request you make, or from an inbound call you +forward on. -Forwarding an inbound call also places an outbound call, dialing a second destination and -connecting the two callers. Configure it with [SWML call instructions][swml-connect] or Call Flow -Builder's [Forward to Phone node][forward-node]. +When you make the request, you name a destination and say what should happen once someone +answers. SignalWire places the call and runs those instructions. -Calls can start on a device instead. The [Browser SDK][browser-outbound] dials from your web app so -a person speaks through their own microphone and speaker, and registered SIP phones and phone -systems dial through SignalWire using [SIP credentials][sip-credentials]. A browser button that -starts an automated call is a server call: it sends the request to your backend. +Forwarding is the same thing prompted by someone else. A call arrives on one of your numbers, you +send it on to a second destination, and SignalWire places that call and connects the two people +once it is answered. ## Destinations you can call From 2e93287afa5a56f19b8eb7badee7072dbfe72687 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 18:29:56 -0400 Subject: [PATCH 030/103] docs: shorten the destination labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sentence-long labels overlapped — two rows both opened "A SIP phone" — so the column no longer told you which row was which. One short noun per row, with the concepts explained underneath instead. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index c01bfddf56..f6bf91927a 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -49,18 +49,22 @@ once it is answered. A call can reach a phone anywhere on the public switched telephone network (PSTN), a SIP endpoint anywhere on the internet, or anything you have set up inside your own SignalWire Space. -| What you want to reach | Dial | Example | +| Destination | Dial | Example | |---|---|---| -| A mobile or landline phone | Its number in E.164 | `+15557654321` | -| A SIP phone, PBX, or another provider's SIP service | Its SIP URI | `sip:support@example.com` | -| A SIP phone registered to your Space with [SIP credentials][sip-credentials] | The endpoint's resource address | `/private/desk-phone` | -| One of your own users, called a Subscriber | The Subscriber's resource address | `/private/support-rep` | -| An AI agent, a SWML or cXML script, or a Call Flow | The application's resource address | `/public/support-agent` | -| A conference room | The room's resource address | `/public/team-standup` | - -The last four rows are all [Resources][resources]: things you create in your Space, each reachable -at a [resource address][resource-addresses] shaped `/context/name`, where the context is `public` -or `private`. Whatever the type, you dial it the same way. +| Phone | Its number in E.164 | `+15557654321` | +| SIP endpoint | Its SIP URI | `sip:support@example.com` | +| SIP endpoint in your Space | Its resource address | `/private/desk-phone` | +| Subscriber | Their resource address | `/private/support-rep` | +| Application | Its resource address | `/public/support-agent` | +| Conference room | Its resource address | `/public/team-standup` | + +A Subscriber is one of your own users. An application is an AI agent, a SWML or cXML script, or a +Call Flow. A SIP endpoint in your Space is a phone or softphone you registered with +[SIP credentials][sip-credentials]. + +Those last four are all [Resources][resources]: things you create in your Space, each reachable at +a [resource address][resource-addresses] shaped `/context/name`, where the context is `public` or +`private`. Whatever the type, you dial it the same way. The Relay client is the one exception: it dials phone numbers and SIP endpoints only, not resource addresses. From d4dee05174eb344a1305fbc40dfd1463cd63d741 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 18:33:08 -0400 Subject: [PATCH 031/103] docs: stop the caller ID table reading as a limit on what you can dial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two real errors. The table was keyed on the destination with a column of caller ID rules, so the phone row read as "you can only dial numbers you own" — the destination is unrestricted; the from is what's held to numbers your project owns. And the resource row's "any value is accepted" read as though any address were dialable, when a resource address has to resolve to a Resource in your Space. Now states outright that any PSTN number, any SIP endpoint, and any existing resource address are dialable, with trial projects called out as the one narrowing case. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index f6bf91927a..55f2dbc0d7 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -150,19 +150,22 @@ flowchart TB - + -Pick the destination first. It decides what the call has to present as its caller ID. +You can dial any phone number on the PSTN, any SIP endpoint on the internet, and any resource +address that exists in your Space. Nothing about the destination is restricted to numbers you own. -| Destination | Caller ID it requires | +What the destination does change is the caller ID your call is allowed to present: + +| Dialing | Caller ID your call can present | |---|---| -| A phone number | A voice-capable PSTN number in your project, either one you [bought from SignalWire][phone-numbers] or one you [verified as a caller ID][caller-id], in E.164 | +| A phone number | Only a number your project holds, in E.164 and voice-capable: one you [bought from SignalWire][phone-numbers] or [verified as a caller ID][caller-id] | | A SIP URI | An E.164 number, a SIP URI, or a short token of up to 11 letters, digits, or underscores | -| A resource address | Not checked — any value is accepted, though a number in your project is still the safe choice | +| A resource address | Any of those — the caller ID format isn't checked for calls that stay inside your Space | -Calling a phone number is the strict case, because SignalWire originates that call on the PSTN as -your number and has to be entitled to use it. The other destinations aren't PSTN originations, so -they accept more. +Calling a phone number is the strict case, because SignalWire originates that call on the PSTN +under your caller ID and has to be entitled to use it. The other destinations aren't PSTN +originations, so they accept more. How that caller ID is labeled decides whether people answer. The [spam labels][spam-labels] and [STIR/SHAKEN][stir-shaken] guides cover call reputation and attestation. @@ -185,9 +188,10 @@ destinations that user can call, so scope it to what the person should reach. -[Trial projects][trial-mode] can call only purchased and verified numbers, and cannot call -internationally at all. Every other project needs -[international dialing enabled][international] before it can call another country. +One case does narrow the destination. A [trial project][trial-mode] can dial only numbers it has +purchased or verified, and cannot call internationally at all. Every other project can dial any +number, but needs [international dialing enabled][international] before it can reach another +country. From 2c334106bd8d3e23cd888ce11ce7d18e8383517a Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 18:39:17 -0400 Subject: [PATCH 032/103] docs: one SIP row in the destinations table Co-Authored-By: Claude Opus 5 (1M context) --- .../platform/pages/calling/voice/outbound-calling.mdx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 55f2dbc0d7..6a2db1c561 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -10,7 +10,6 @@ max-toc-depth: 3 [relay-guide]: /docs/server-sdks/guides/relay-client [browser-outbound]: /docs/browser-sdk/v4/guides/outbound-calls [browser-auth]: /docs/browser-sdk/v4/guides/authentication -[sip-credentials]: /docs/platform/voice/sip/sip-credentials [caller-id]: /docs/platform/voice/how-to-set-caller-id-or-cnam [stir-shaken]: /docs/platform/voice/stir-shaken [spam-labels]: /docs/platform/voice/resolving-spam-labels @@ -53,18 +52,16 @@ anywhere on the internet, or anything you have set up inside your own SignalWire |---|---|---| | Phone | Its number in E.164 | `+15557654321` | | SIP endpoint | Its SIP URI | `sip:support@example.com` | -| SIP endpoint in your Space | Its resource address | `/private/desk-phone` | | Subscriber | Their resource address | `/private/support-rep` | | Application | Its resource address | `/public/support-agent` | | Conference room | Its resource address | `/public/team-standup` | A Subscriber is one of your own users. An application is an AI agent, a SWML or cXML script, or a -Call Flow. A SIP endpoint in your Space is a phone or softphone you registered with -[SIP credentials][sip-credentials]. +Call Flow. -Those last four are all [Resources][resources]: things you create in your Space, each reachable at -a [resource address][resource-addresses] shaped `/context/name`, where the context is `public` or -`private`. Whatever the type, you dial it the same way. +Those last three are all [Resources][resources]: things you create in your Space, each reachable +at a [resource address][resource-addresses] shaped `/context/name`, where the context is `public` +or `private`. Whatever the type, you dial it the same way. The Relay client is the one exception: it dials phone numbers and SIP endpoints only, not resource addresses. From e55c9b02bf1bf05d3983546e4da214d5fa238487 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 18:42:38 -0400 Subject: [PATCH 033/103] docs: key the first step on what each destination needs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reciting caller ID rules against destination rows left the reader working out what to actually go get. The column is now "What you need", so each row is a checklist for dialing that destination — including the Resource having to exist at that address, and optional SIP auth. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 6a2db1c561..ac4e64f49b 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -147,25 +147,24 @@ flowchart TB - + -You can dial any phone number on the PSTN, any SIP endpoint on the internet, and any resource -address that exists in your Space. Nothing about the destination is restricted to numbers you own. +You can dial any phone number, any SIP endpoint, and any resource address that exists in your +Space. What each one needs from you before it will go through differs. -What the destination does change is the caller ID your call is allowed to present: - -| Dialing | Caller ID your call can present | +| Dialing | What you need | |---|---| -| A phone number | Only a number your project holds, in E.164 and voice-capable: one you [bought from SignalWire][phone-numbers] or [verified as a caller ID][caller-id] | -| A SIP URI | An E.164 number, a SIP URI, or a short token of up to 11 letters, digits, or underscores | -| A resource address | Any of those — the caller ID format isn't checked for calls that stay inside your Space | +| A phone number | A voice-capable PSTN number in your project to present as the caller ID, either one you [bought from SignalWire][phone-numbers] or one you [verified as a caller ID][caller-id] | +| A SIP URI | A caller ID, which here can be an E.164 number, a SIP URI, or a short token of up to 11 letters, digits, or underscores, plus a SIP username and password if the far end authenticates | +| A resource address | A Resource in your Space already reachable at that address. Its caller ID isn't format-checked | -Calling a phone number is the strict case, because SignalWire originates that call on the PSTN -under your caller ID and has to be entitled to use it. The other destinations aren't PSTN -originations, so they accept more. +The phone number is the demanding case, because SignalWire originates that call on the PSTN under +your caller ID and has to be entitled to use it. The other two aren't PSTN originations, so they +ask less of you. -How that caller ID is labeled decides whether people answer. The [spam labels][spam-labels] and -[STIR/SHAKEN][stir-shaken] guides cover call reputation and attestation. +For calls to phones, how your caller ID is labeled decides whether people answer. The +[spam labels][spam-labels] and [STIR/SHAKEN][stir-shaken] guides cover call reputation and +attestation. From cacbf0d7f7201d8a4db73c50cc2b83dc264f9fe9 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 18:47:53 -0400 Subject: [PATCH 034/103] docs: state what a credential has to be permitted to dial Dialing a Resource needs a credential allowed to reach it, which the guide never said. A project API token covers every Resource in its own project; a SAT reaches what its Subscriber reaches; a guest token only the allowed_addresses set at creation. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index ac4e64f49b..a01a39321c 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -156,7 +156,7 @@ Space. What each one needs from you before it will go through differs. |---|---| | A phone number | A voice-capable PSTN number in your project to present as the caller ID, either one you [bought from SignalWire][phone-numbers] or one you [verified as a caller ID][caller-id] | | A SIP URI | A caller ID, which here can be an E.164 number, a SIP URI, or a short token of up to 11 letters, digits, or underscores, plus a SIP username and password if the far end authenticates | -| A resource address | A Resource in your Space already reachable at that address. Its caller ID isn't format-checked | +| A resource address | A Resource in your Space already reachable at that address, and a credential permitted to dial it. Its caller ID isn't format-checked | The phone number is the demanding case, because SignalWire originates that call on the PSTN under your caller ID and has to be entitled to use it. The other two aren't PSTN originations, so they @@ -177,8 +177,14 @@ Which credential you need depends on where the dialing code runs. | Your server | Your Project ID and an API token with voice permissions, from the Dashboard's [API credentials][api-credentials] page | | A browser | A [Subscriber Access Token (SAT)][browser-auth] that your backend issues for the signed-in user | -Keep the API token on your backend. A Subscriber Access Token's permissions decide which -destinations that user can call, so scope it to what the person should reach. +Keep the API token on your backend. + +Which Resources a credential can dial follows from the credential itself. A project API token +reaches every Resource in its own project, so nothing extra is needed there. A Subscriber Access +Token reaches whatever its Subscriber can reach, and a +[guest token](/docs/apis/rest/subscribers/tokens/create-subscriber-guest-token) reaches only the +addresses you list in `allowed_addresses` when you create it, up to ten. When a browser call to a +Resource fails, that scope is the first thing to check. From b7398b87f6fdb2335c231539ed65f508d63a838e Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 18:50:48 -0400 Subject: [PATCH 035/103] docs: credential row covers any client app, not just a browser A SAT authenticates a subscriber client, so the row is "Your web or mobile app". Also refreshes the page description, which still named a section removed earlier. Co-Authored-By: Claude Opus 5 (1M context) --- .../platform/pages/calling/voice/outbound-calling.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index a01a39321c..ade12ccd11 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -1,7 +1,7 @@ --- title: Outbound calling slug: /voice/outbound-calling -description: Learn how outbound calls start, which destinations each API and SDK supports, and how to place calls from a server or browser. +description: Learn how outbound calls start, what you can dial, and how to place a call from your server or your own app. max-toc-depth: 3 --- @@ -175,7 +175,7 @@ Which credential you need depends on where the dialing code runs. | Dialing from | Use | |---|---| | Your server | Your Project ID and an API token with voice permissions, from the Dashboard's [API credentials][api-credentials] page | -| A browser | A [Subscriber Access Token (SAT)][browser-auth] that your backend issues for the signed-in user | +| Your web or mobile app | A [Subscriber Access Token (SAT)][browser-auth] that your backend issues for the signed-in user | Keep the API token on your backend. @@ -183,8 +183,8 @@ Which Resources a credential can dial follows from the credential itself. A proj reaches every Resource in its own project, so nothing extra is needed there. A Subscriber Access Token reaches whatever its Subscriber can reach, and a [guest token](/docs/apis/rest/subscribers/tokens/create-subscriber-guest-token) reaches only the -addresses you list in `allowed_addresses` when you create it, up to ten. When a browser call to a -Resource fails, that scope is the first thing to check. +addresses you list in `allowed_addresses` when you create it, up to ten. When a call from your app +to a Resource fails, that scope is the first thing to check. From 0e0dbd99d28d2058958fc6a12d1c416e7f50f6a5 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 18:57:12 -0400 Subject: [PATCH 036/103] docs: make it a Getting started section ending in the call Three steps: what the destination needs, which credential, place the call. The trial and international limits move into step one, where the other destination constraints already live. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index ade12ccd11..d2081f5ae4 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -143,7 +143,7 @@ flowchart TB -## Before you dial +## Getting started @@ -162,6 +162,11 @@ The phone number is the demanding case, because SignalWire originates that call your caller ID and has to be entitled to use it. The other two aren't PSTN originations, so they ask less of you. +One case narrows the destination itself. A [trial project][trial-mode] can dial only numbers it +has purchased or verified, and cannot call internationally at all. Every other project can dial +any number, but needs [international dialing enabled][international] before it reaches another +country. + For calls to phones, how your caller ID is labeled decides whether people answer. The [spam labels][spam-labels] and [STIR/SHAKEN][stir-shaken] guides cover call reputation and attestation. @@ -188,12 +193,14 @@ to a Resource fails, that scope is the first thing to check. - + -One case does narrow the destination. A [trial project][trial-mode] can dial only numbers it has -purchased or verified, and cannot call internationally at all. Every other project can dial any -number, but needs [international dialing enabled][international] before it can reach another -country. +Send a `dial` request carrying three things: the caller ID from step one, the destination, and +what should happen once someone answers. SignalWire answers with a call `id` and a `queued` +status, then places the call. + +[Place a call from a server](#place-a-call-from-a-server) has that request in cURL, Python, and +TypeScript, with the SWML announcement included inline so it runs as written. From 5ef67c15afe39695b47d9d5b699a4344999334de Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 19:01:47 -0400 Subject: [PATCH 037/103] docs: trial and international limits become a warning Neither is a step you take, so both move into a titled Warning ahead of the steps, where a trial user hits it before working through them. Co-Authored-By: Claude Opus 5 (1M context) --- .../platform/pages/calling/voice/outbound-calling.mdx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index d2081f5ae4..db9fb0537b 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -145,6 +145,12 @@ flowchart TB ## Getting started + +A [trial project][trial-mode] can dial only numbers it has purchased or verified, and cannot call +internationally at all. Any other project can dial any number, but needs +[international dialing enabled][international] before it reaches another country. + + @@ -162,11 +168,6 @@ The phone number is the demanding case, because SignalWire originates that call your caller ID and has to be entitled to use it. The other two aren't PSTN originations, so they ask less of you. -One case narrows the destination itself. A [trial project][trial-mode] can dial only numbers it -has purchased or verified, and cannot call internationally at all. Every other project can dial -any number, but needs [international dialing enabled][international] before it reaches another -country. - For calls to phones, how your caller ID is labeled decides whether people answer. The [spam labels][spam-labels] and [STIR/SHAKEN][stir-shaken] guides cover call reputation and attestation. From 23074bd22b47886b2938a73b422a7692a40ab9e8 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 19:02:46 -0400 Subject: [PATCH 038/103] docs: drop cXML from the outbound calling guide Co-Authored-By: Claude Opus 5 (1M context) --- .../platform/pages/calling/voice/outbound-calling.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index db9fb0537b..9e3f26c63e 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -56,8 +56,8 @@ anywhere on the internet, or anything you have set up inside your own SignalWire | Application | Its resource address | `/public/support-agent` | | Conference room | Its resource address | `/public/team-standup` | -A Subscriber is one of your own users. An application is an AI agent, a SWML or cXML script, or a -Call Flow. +A Subscriber is one of your own users. An application is an AI agent, a SWML script, or a Call +Flow. Those last three are all [Resources][resources]: things you create in your Space, each reachable at a [resource address][resource-addresses] shaped `/context/name`, where the context is `public` From 09beb50ba68c33469123498e13b03ad411725029 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Tue, 8 Sep 2026 19:05:46 -0400 Subject: [PATCH 039/103] docs: remove call forwarding from the outbound calling guide Drops the "Forward an inbound call" example and its three code tabs, and narrows "How outbound calls start" to the request path. Fixes the counts and the intro line that promised bridging two callers. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/calling/voice/outbound-calling.mdx | 94 +------------------ 1 file changed, 5 insertions(+), 89 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 9e3f26c63e..992a8f62b3 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -22,26 +22,18 @@ max-toc-depth: 3 [tool-calling]: /docs/platform/ai/tool-calling [ai-best-practices]: /docs/platform/ai/best-practices [tcpa]: /docs/platform/compliance/tcpa -[forward-calls]: /docs/swml/guides/forward-calls [resource-addresses]: /docs/platform/addresses [resources]: /docs/platform/resources -Outbound calls let your app reach a customer, connect a caller to a dispatcher, or start an AI -conversation. You choose the destination and what happens on the call. +Outbound calls let your app reach a customer, play them a message, or start an AI conversation. +You choose the destination and what happens on the call. Jump to the [examples](#examples) for working code. ## How outbound calls start -An outbound call starts one of two ways: from a request you make, or from an inbound call you -forward on. - -When you make the request, you name a destination and say what should happen once someone -answers. SignalWire places the call and runs those instructions. - -Forwarding is the same thing prompted by someone else. A call arrives on one of your numbers, you -send it on to a second destination, and SignalWire places that call and connects the two people -once it is answered. +An outbound call starts from a request you make. You name a destination and say what should happen +once someone answers, and SignalWire places the call and runs those instructions. ## Destinations you can call @@ -209,7 +201,7 @@ TypeScript, with the SWML announcement included inline so it runs as written. ## Examples -Working code for the four things people build first. +Working code for the three things people build first. ### Place a call from a server @@ -394,82 +386,6 @@ See the [Relay guide][relay-guide] for connection management and more call contr -### Forward an inbound call - -Answer a call to one of your numbers and connect it to a second destination. Set the calling -number to your SignalWire number and the destination to whoever should take the call. - -With SWML, assign the script below to the number people call, following the -[forwarding guide][forward-calls]. With the Server SDK, route that number's inbound calls to the -Relay `dispatch` context and run the handler on your backend, as the [Relay guide][relay-guide] -describes. - - - -```yaml -version: 1.0.0 -sections: - main: - - connect: - from: "+15551234567" - to: "+15557654321" -``` - - -```python -from signalwire.relay import RelayClient - -client = RelayClient( - project="YOUR_PROJECT_ID", - token="YOUR_API_TOKEN", - host="YOUR_SPACE.signalwire.com", - contexts=["dispatch"], -) - -@client.on_call -async def handle_call(call): - await call.answer() - await call.connect([[{ - "type": "phone", - "params": { - "from_number": "+15551234567", - "to_number": "+15557654321", - }, - }]]) - -client.run() -``` - - -```typescript -import { RelayClient } from "@signalwire/sdk"; - -const client = new RelayClient({ - project: "YOUR_PROJECT_ID", - token: "YOUR_API_TOKEN", - host: "YOUR_SPACE.signalwire.com", - contexts: ["dispatch"], -}); - -client.onCall(async (call) => { - await call.answer(); - await call.connect([[{ - type: "phone", - params: { - from_number: "+15551234567", - to_number: "+15557654321", - }, - }]]); -}); - -await client.run(); -``` - - - -The connect references for [Python](/docs/server-sdks/reference/python/relay/call/connect) and -[TypeScript](/docs/server-sdks/reference/typescript/relay/call/connect) cover additional Relay options. For SWML progress webhooks, use `call_state_url` and `call_state_events`. - ### Place a call with the Browser SDK Let someone place and speak on a call from your web app. Supply a From a8461de5bfd551510000e4525e4b5601f889469c Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 9 Sep 2026 07:22:53 -0400 Subject: [PATCH 040/103] docs: simplify outbound calling guide --- .../pages/calling/voice/outbound-calling.mdx | 250 +++++++----------- 1 file changed, 93 insertions(+), 157 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 992a8f62b3..73e385b91d 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -25,20 +25,14 @@ max-toc-depth: 3 [resource-addresses]: /docs/platform/addresses [resources]: /docs/platform/resources -Outbound calls let your app reach a customer, play them a message, or start an AI conversation. -You choose the destination and what happens on the call. +Place outbound calls to phone numbers, SIP endpoints, or SignalWire resources. +[Call from your server](#place-a-call-from-a-server), +[let a user call from your web app](#place-a-call-with-the-browser-sdk), or +[connect someone to an AI agent](#run-an-ai-agent). -Jump to the [examples](#examples) for working code. +## Prerequisites -## How outbound calls start - -An outbound call starts from a request you make. You name a destination and say what should happen -once someone answers, and SignalWire places the call and runs those instructions. - -## Destinations you can call - -A call can reach a phone anywhere on the public switched telephone network (PSTN), a SIP endpoint -anywhere on the internet, or anything you have set up inside your own SignalWire Space. +### Choose a destination | Destination | Dial | Example | |---|---|---| @@ -48,94 +42,13 @@ anywhere on the internet, or anything you have set up inside your own SignalWire | Application | Its resource address | `/public/support-agent` | | Conference room | Its resource address | `/public/team-standup` | -A Subscriber is one of your own users. An application is an AI agent, a SWML script, or a Call -Flow. - -Those last three are all [Resources][resources]: things you create in your Space, each reachable -at a [resource address][resource-addresses] shaped `/context/name`, where the context is `public` -or `private`. Whatever the type, you dial it the same way. - -The Relay client is the one exception: it dials phone numbers and SIP endpoints only, not -resource addresses. - -## How the call runs - -With REST, SignalWire runs the instructions you supply. With Relay, your running application -controls the call through commands and events. - - - - -A `dial` request supplies a SignalWire Markup Language (SWML) document in `swml` or at `url`. -SignalWire places the call and runs the document after answer. The document can play audio, -run an AI agent, or connect another destination. Progress arrives at your webhook endpoint. - -This flow uses `answered` and `ended` webhooks: - - - -Your code sends a dial request with from, to, and SWML. SignalWire returns a call id with status queued and rings the destination. When the person answers, SignalWire sends an answered webhook and runs your SWML. When the call finishes, SignalWire sends an ended webhook. - - - - - -```mermaid -sequenceDiagram - participant App as Your code - participant SW as SignalWire - participant Rider as Rider's phone - - App->>SW: dial: from, to, SWML - SW-->>App: call id, status queued - SW->>Rider: rings - Rider->>SW: answers - SW-->>App: status webhook answered - Note over SW,Rider: Your SWML runs - Note over SW,Rider: Call finishes - SW-->>App: status webhook ended -``` +Subscribers (your users), applications (AI agents, SWML scripts, or Call Flows), and conference +rooms are [Resources][resources]. Each has a [resource address][resource-addresses] in the form +`/context/name`, where the context is `public` or `private`. - - - - - -Relay keeps a bidirectional WebSocket open between your server and SignalWire. Your app sends -commands such as `dial`, `play`, and `connect`; SignalWire sends live events back. Your code -uses those events to decide what the call does next. - - - -One persistent WebSocket carries commands from your application to SignalWire and live events back to your application. Your code decides how to react: call answered can trigger play, collected keypad input can trigger connect to a chosen destination, and playback finished can trigger hangup. These are independent examples of application decisions; input events require an active collection operation. - - - - - -```mermaid -flowchart TB - subgraph Channel[One persistent, bidirectional WebSocket] - direction LR - App[Your application: decides what happens next] - SW[SignalWire: places and controls the call] - App -->|Commands: dial, play, connect, hangup| SW - SW -.->|Live events: call state, playback, collected input| App - end - - subgraph Reactions[Examples of application decisions, not an automatic sequence] - Answered[Call answered: calling.call.dial] --> Greeting[Your app: start the greeting] --> Play[Command: play] - Input[Keypad input collected: calling.call.collect] --> Route[Your app: route to dispatch] --> Connect[Command: connect] - Finished[Playback finished: calling.call.play] --> End[Your app: end the conversation] --> Hangup[Command: hangup] - end -``` - - - - - +The Relay client supports phone numbers and SIP endpoints only. -## Getting started +### Set up your caller ID A [trial project][trial-mode] can dial only numbers it has purchased or verified, and cannot call @@ -143,32 +56,16 @@ internationally at all. Any other project can dial any number, but needs [international dialing enabled][international] before it reaches another country. - - - - -You can dial any phone number, any SIP endpoint, and any resource address that exists in your -Space. What each one needs from you before it will go through differs. - | Dialing | What you need | |---|---| | A phone number | A voice-capable PSTN number in your project to present as the caller ID, either one you [bought from SignalWire][phone-numbers] or one you [verified as a caller ID][caller-id] | | A SIP URI | A caller ID, which here can be an E.164 number, a SIP URI, or a short token of up to 11 letters, digits, or underscores, plus a SIP username and password if the far end authenticates | | A resource address | A Resource in your Space already reachable at that address, and a credential permitted to dial it. Its caller ID isn't format-checked | -The phone number is the demanding case, because SignalWire originates that call on the PSTN under -your caller ID and has to be entitled to use it. The other two aren't PSTN originations, so they -ask less of you. - -For calls to phones, how your caller ID is labeled decides whether people answer. The -[spam labels][spam-labels] and [STIR/SHAKEN][stir-shaken] guides cover call reputation and -attestation. - - +For phone calls, see [spam labels][spam-labels] and [STIR/SHAKEN][stir-shaken] for caller ID +reputation and attestation. - - -Which credential you need depends on where the dialing code runs. +### Get credentials | Dialing from | Use | |---|---| @@ -177,40 +74,27 @@ Which credential you need depends on where the dialing code runs. Keep the API token on your backend. -Which Resources a credential can dial follows from the credential itself. A project API token -reaches every Resource in its own project, so nothing extra is needed there. A Subscriber Access -Token reaches whatever its Subscriber can reach, and a -[guest token](/docs/apis/rest/subscribers/tokens/create-subscriber-guest-token) reaches only the -addresses you list in `allowed_addresses` when you create it, up to ten. When a call from your app -to a Resource fails, that scope is the first thing to check. - - - - - -Send a `dial` request carrying three things: the caller ID from step one, the destination, and -what should happen once someone answers. SignalWire answers with a call `id` and a `queued` -status, then places the call. - -[Place a call from a server](#place-a-call-from-a-server) has that request in cURL, Python, and -TypeScript, with the SWML announcement included inline so it runs as written. - - - - + +A project API token can dial every Resource in its project. A Subscriber Access Token has its +Subscriber's access. A [guest token](/docs/apis/rest/subscribers/tokens/create-subscriber-guest-token) +can dial only the addresses in its `allowed_addresses` list, up to ten. If a call to a Resource +fails, check the token's access. + ## Examples -Working code for the three things people build first. - ### Place a call from a server -Place a call that plays a spoken announcement when someone answers, using REST or Relay. +Place a call that plays a spoken announcement when someone answers. Choose REST to have +SignalWire run supplied instructions, or Relay to control the call through commands and events. -This request includes the SWML announcement. Replace the credentials and phone numbers, +Supply a SignalWire Markup Language (SWML) document in `swml` or at `url` to define what happens +after answer: play audio, run an AI agent, or connect another destination. + +This example includes the announcement in `swml`. Replace the credentials and phone numbers, then run it from your terminal or backend. @@ -301,9 +185,7 @@ The response identifies the queued call: } ``` -`queued` means SignalWire accepted the call but has not placed it yet, so no webhook has fired. -The first event you can subscribe to is `created`. Keep the `id` for later call commands, and add -`status_url` and `status_events` as described in +Keep the `id` for later call commands. To receive webhooks, see [Track the call's progress](#track-the-calls-progress). The REST client references for [Python][sdk-rest-dial-python] and [TypeScript][sdk-rest-dial-ts] cover all request fields. @@ -415,10 +297,11 @@ includes a complete page with media and call controls. ### Run an AI agent -Replace the announcement with an AI agent that holds a conversation. Both versions build on -[Place a call from a server](#place-a-call-from-a-server). With SWML, host the document below and -pass its `url` in place of the inline `swml`. With the Server SDK, replace the playback and hangup -steps with the snippet below, where `call` is the answered call returned by `dial`. +Replace the announcement in the [server example](#place-a-call-from-a-server) with an AI agent: + +- **SWML:** Host the document below and pass its `url` in place of the inline `swml`. +- **Relay:** Replace the playback and hangup steps with the Python or TypeScript snippet below. + Here, `call` is the answered call returned by `dial`. An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). @@ -495,8 +378,6 @@ See the [AI quickstart][ai-quickstart] or the Relay AI references for ## Track the call's progress -Confirm SignalWire accepted the request, then follow the call to answer and hangup. - @@ -506,8 +387,35 @@ to confirm pickup and `ended` to know the call finished. Set `status_url` to your webhook endpoint and choose `status_events`: `created`, `ringing`, `answered`, or `ended`. If you omit `status_events`, you receive `ended` only. -Each event name is also a `status` the call reports, so a webhook arrives when the call enters the -state you subscribed to. A call passes through more states than there are events: +This flow shows a call with `answered` and `ended` webhooks enabled: + + + +Your code sends a dial request with from, to, and SWML. SignalWire returns a call id with status queued and rings the destination. When the person answers, SignalWire sends an answered webhook and runs your SWML. When the call finishes, SignalWire sends an ended webhook. + + + + + +```mermaid +sequenceDiagram + participant App as Your code + participant SW as SignalWire + participant Rider as Rider's phone + + App->>SW: dial: from, to, SWML + SW-->>App: call id, status queued + SW->>Rider: rings + Rider->>SW: answers + SW-->>App: status webhook answered + Note over SW,Rider: Your SWML runs + Note over SW,Rider: Call finishes + SW-->>App: status webhook ended +``` + + + +Only some call states trigger webhooks: | `status` | Fires a webhook | |---|---| @@ -522,20 +430,48 @@ state you subscribed to. A call passes through more states than there are events `busy`, `no-answer`, `failed`, and `canceled` are not `status_events` values, so none of them -delivers a callback — not even under the default `status_events` of `["ended"]`. Subscribe to -`answered` and pair it with the ring `timeout` you set, so a call that never connects shows up as -an `answered` webhook that never arrives. To confirm what happened, read the call's -[voice log](/docs/apis/rest/voice-logs/list-voice-logs). +delivers a callback, even with the default `status_events` of `["ended"]`. Subscribe to `answered` +and set an application deadline based on the call's ring `timeout`, allowing for webhook delivery +delay. If no `answered` webhook arrives by that deadline, check the call's +[voice log](/docs/apis/rest/voice-logs/list-voice-logs) to confirm the outcome. +Relay carries commands and live events over a persistent WebSocket. The SDK's `dial` returns a call object after answer or raises `RelayError` on failure. Use `call.on` for state and action events, and `action.wait()` to wait for an action such as playback to finish. See the [Python](/docs/server-sdks/reference/python/relay/call/on) or [TypeScript](/docs/server-sdks/reference/typescript/relay/call/on) event reference. + + +One persistent WebSocket carries commands from your application to SignalWire and live events back to your application. Your code decides how to react: call answered can trigger play, collected keypad input can trigger connect to a chosen destination, and playback finished can trigger hangup. These are independent examples of application decisions; input events require an active collection operation. + + + + + +```mermaid +flowchart TB + subgraph Channel[One persistent, bidirectional WebSocket] + direction LR + App[Your application: decides what happens next] + SW[SignalWire: places and controls the call] + App -->|Commands: dial, play, connect, hangup| SW + SW -.->|Live events: call state, playback, collected input| App + end + + subgraph Reactions[Examples of application decisions, not an automatic sequence] + Answered[Call answered: calling.call.dial] --> Greeting[Your app: start the greeting] --> Play[Command: play] + Input[Keypad input collected: calling.call.collect] --> Route[Your app: route to dispatch] --> Connect[Command: connect] + Finished[Playback finished: calling.call.play] --> End[Your app: end the conversation] --> Hangup[Command: hangup] + end +``` + + + The initial `calling.dial` response acknowledges the request. A later `calling.call.dial` event carries the dial outcome. The Server SDK waits for this event before returning a call object. From ed37e05a445f00edf0218f6c26b19937a455589a Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 9 Sep 2026 09:22:50 -0400 Subject: [PATCH 041/103] docs: fix status webhook claims and tighten the outbound calling guide Remove the one-off remarks that sat between the examples, and move progress tracking above the examples so readers know how a call is followed before they place one. Drop the webhook table and the "never connects" warning. Verified against prime-rails 02473273 and mod_infrastructure: the status webhook fires when the incoming call_state is in status_events, and busy, no-answer, decline, and cancel are end_reason values on the ended state, not states of their own. A busy call therefore delivers ended with end_reason busy. Spec follow-ups are recorded on issue #669. Rewrite the REST tracking intro to explain status_url before the details, add end_reason, and update the lifecycle diagram: label the SignalWire-to-app arrows as statuses, add the ringing status, and rename the third participant to Destination since it may be a browser or SIP endpoint rather than a phone. Co-Authored-By: Claude Fable 5.1 --- .../img/outbound-call-lifecycle-themed.svg | 43 ++-- .../pages/calling/voice/outbound-calling.mdx | 228 +++++++----------- 2 files changed, 113 insertions(+), 158 deletions(-) diff --git a/fern/assets/images/img/outbound-call-lifecycle-themed.svg b/fern/assets/images/img/outbound-call-lifecycle-themed.svg index f2facb907a..1605a53487 100644 --- a/fern/assets/images/img/outbound-call-lifecycle-themed.svg +++ b/fern/assets/images/img/outbound-call-lifecycle-themed.svg @@ -1,6 +1,6 @@ - + Outbound call with the Calling API or Server SDK REST client - Your code sends dial with from, to, and SWML. SignalWire returns a call id with status queued and rings the rider's phone. The rider answers, SignalWire sends an answered webhook, and your SWML runs. When the call finishes, SignalWire sends an ended webhook. + Your code sends dial with from, to, and SWML. SignalWire returns the call id with status queued and rings the destination and reports status ringing. The destination answers, SignalWire reports status answered, and your SWML runs. When the call finishes, SignalWire reports status ended. - - + + Your code - + SignalWire - + - Rider’s phone + Destination dial from, to, SWML - call id, queued + queued + status, call id rings - answers - + ringing + status + + + answers + - answered - webhook - + answered + status + - - Your SWML runs - Call finishes + + Your SWML runs + Call finishes - ended - webhook - + ended + status + diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 73e385b91d..8e2bc80fc9 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -5,21 +5,13 @@ description: Learn how outbound calls start, what you can dial, and how to place max-toc-depth: 3 --- -[sdk-rest-dial-python]: /docs/server-sdks/reference/python/rest/calling/dial -[sdk-rest-dial-ts]: /docs/server-sdks/reference/typescript/rest/calling/dial -[relay-guide]: /docs/server-sdks/guides/relay-client -[browser-outbound]: /docs/browser-sdk/v4/guides/outbound-calls [browser-auth]: /docs/browser-sdk/v4/guides/authentication [caller-id]: /docs/platform/voice/how-to-set-caller-id-or-cnam -[stir-shaken]: /docs/platform/voice/stir-shaken -[spam-labels]: /docs/platform/voice/resolving-spam-labels [trial-mode]: /docs/platform/trial-mode [international]: /docs/platform/how-to-enable-international-services [api-credentials]: /docs/platform/your-signalwire-api-space [phone-numbers]: /docs/platform/phone-numbers [swml-ai]: /docs/swml/reference/calling/ai -[ai-quickstart]: /docs/platform/ai/quickstart -[tool-calling]: /docs/platform/ai/tool-calling [ai-best-practices]: /docs/platform/ai/best-practices [tcpa]: /docs/platform/compliance/tcpa [resource-addresses]: /docs/platform/addresses @@ -46,8 +38,6 @@ Subscribers (your users), applications (AI agents, SWML scripts, or Call Flows), rooms are [Resources][resources]. Each has a [resource address][resource-addresses] in the form `/context/name`, where the context is `public` or `private`. -The Relay client supports phone numbers and SIP endpoints only. - ### Set up your caller ID @@ -62,9 +52,6 @@ internationally at all. Any other project can dial any number, but needs | A SIP URI | A caller ID, which here can be an E.164 number, a SIP URI, or a short token of up to 11 letters, digits, or underscores, plus a SIP username and password if the far end authenticates | | A resource address | A Resource in your Space already reachable at that address, and a credential permitted to dial it. Its caller ID isn't format-checked | -For phone calls, see [spam labels][spam-labels] and [STIR/SHAKEN][stir-shaken] for caller ID -reputation and attestation. - ### Get credentials | Dialing from | Use | @@ -72,14 +59,92 @@ reputation and attestation. | Your server | Your Project ID and an API token with voice permissions, from the Dashboard's [API credentials][api-credentials] page | | Your web or mobile app | A [Subscriber Access Token (SAT)][browser-auth] that your backend issues for the signed-in user | -Keep the API token on your backend. +## Track the call's progress + + + + +Once you create a call, SignalWire tracks its status through the lifecycle: `queued`, `created`, +`ringing`, `answered`, and `ended`. To follow that lifecycle from your application, add a +`status_url` to the `dial` request. SignalWire sends a POST to that URL each time the call reaches +a status you asked about. + +The `dial` response itself gives you the first status. It returns the call `id` with status +`queued`, meaning SignalWire accepted the request and is about to place the call. Everything after +that arrives at your `status_url`. + +Choose which statuses to receive with `status_events`: any of `created`, `ringing`, `answered`, +and `ended`. If you omit it, you receive `ended` only. Most applications ask for `answered`, to +confirm someone picked up, and `ended`, to know the call is over. The `ended` payload includes an +`end_reason`, such as `hangup`, `busy`, or `noAnswer`, so you can tell how it ended. - -A project API token can dial every Resource in its project. A Subscriber Access Token has its -Subscriber's access. A [guest token](/docs/apis/rest/subscribers/tokens/create-subscriber-guest-token) -can dial only the addresses in its `allowed_addresses` list, up to ten. If a call to a Resource -fails, check the token's access. - +This flow shows a call with `ringing`, `answered`, and `ended` status events enabled: + + + +Your code sends a dial request with from, to, and SWML. SignalWire returns the call id with status queued and rings the destination and reports status ringing. When the person answers, SignalWire reports status answered and runs your SWML. When the call finishes, SignalWire reports status ended. + + + + + +```mermaid +sequenceDiagram + participant App as Your code + participant SW as SignalWire + participant Dest as Destination + + App->>SW: dial: from, to, SWML + SW-->>App: call id, status queued + SW->>Dest: rings + SW-->>App: status ringing + Dest->>SW: answers + SW-->>App: status answered + Note over SW,Dest: Your SWML runs + Note over SW,Dest: Call finishes + SW-->>App: status ended +``` + + + + + + +Relay carries commands and live events over a persistent WebSocket. +The SDK's `dial` returns a call object after answer or raises `RelayError` on failure. +Use `call.on` for state and action events, and `action.wait()` to wait for an action such as +playback to finish. See the [Python](/docs/server-sdks/reference/python/relay/call/on) or +[TypeScript](/docs/server-sdks/reference/typescript/relay/call/on) event reference. + + + +One persistent WebSocket carries commands from your application to SignalWire and live events back to your application. Your code decides how to react: call answered can trigger play, collected keypad input can trigger connect to a chosen destination, and playback finished can trigger hangup. These are independent examples of application decisions; input events require an active collection operation. + + + + + +```mermaid +flowchart TB + subgraph Channel[One persistent, bidirectional WebSocket] + direction LR + App[Your application: decides what happens next] + SW[SignalWire: places and controls the call] + App -->|Commands: dial, play, connect, hangup| SW + SW -.->|Live events: call state, playback, collected input| App + end + + subgraph Reactions[Examples of application decisions, not an automatic sequence] + Answered[Call answered: calling.call.dial] --> Greeting[Your app: start the greeting] --> Play[Command: play] + Input[Keypad input collected: calling.call.collect] --> Route[Your app: route to dispatch] --> Connect[Command: connect] + Finished[Playback finished: calling.call.play] --> End[Your app: end the conversation] --> Hangup[Command: hangup] + end +``` + + + + + ## Examples @@ -185,10 +250,7 @@ The response identifies the queued call: } ``` -Keep the `id` for later call commands. To receive webhooks, see -[Track the call's progress](#track-the-calls-progress). -The REST client references for [Python][sdk-rest-dial-python] and [TypeScript][sdk-rest-dial-ts] -cover all request fields. +To receive status webhooks, see [Track the call's progress](#track-the-calls-progress). @@ -263,8 +325,6 @@ await client.disconnect(); -See the [Relay guide][relay-guide] for connection management and more call controls. - @@ -285,16 +345,12 @@ const client = new SignalWire(new StaticCredentialProvider({ token: SAT })); const remoteAudio = document.querySelector("#remote-audio")!; const hangupButton = document.querySelector("#hangup")!; -// Audio only, the default for a phone-style call. const call = await client.dial("+15557654321"); call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); hangupButton.onclick = () => call.hangup(); ``` -Watch for `status$` to reach `connected`. The [Browser SDK outbound guide][browser-outbound] -includes a complete page with media and call controls. - ### Run an AI agent Replace the announcement in the [server example](#place-a-call-from-a-server) with an AI agent: @@ -369,116 +425,10 @@ await call.hangup(); -The [`ai` method][swml-ai] uses the same greeting and prompt in each version. -`static_greeting_no_barge` lets the greeting finish before the conversation begins. Add -[tool calling][tool-calling] to let the agent take actions, such as updating a booking. -See the [AI quickstart][ai-quickstart] or the Relay AI references for +`static_greeting_no_barge` lets the greeting finish before the conversation begins. The SWML +[`ai` method][swml-ai] and the Relay `ai` references for [Python](/docs/server-sdks/reference/python/relay/call/ai) and -[TypeScript](/docs/server-sdks/reference/typescript/relay/call/ai). - -## Track the call's progress - - - - -A `200` response with a call `id` means SignalWire accepted the request. Listen for `answered` -to confirm pickup and `ended` to know the call finished. - -Set `status_url` to your webhook endpoint and choose `status_events`: `created`, `ringing`, -`answered`, or `ended`. If you omit `status_events`, you receive `ended` only. - -This flow shows a call with `answered` and `ended` webhooks enabled: - - - -Your code sends a dial request with from, to, and SWML. SignalWire returns a call id with status queued and rings the destination. When the person answers, SignalWire sends an answered webhook and runs your SWML. When the call finishes, SignalWire sends an ended webhook. - - - - - -```mermaid -sequenceDiagram - participant App as Your code - participant SW as SignalWire - participant Rider as Rider's phone - - App->>SW: dial: from, to, SWML - SW-->>App: call id, status queued - SW->>Rider: rings - Rider->>SW: answers - SW-->>App: status webhook answered - Note over SW,Rider: Your SWML runs - Note over SW,Rider: Call finishes - SW-->>App: status webhook ended -``` - - - -Only some call states trigger webhooks: - -| `status` | Fires a webhook | -|---|---| -| `queued` | No — accepted, not yet placed | -| `initiated` | No — handed off to be placed | -| `created` | Yes, as `created` | -| `ringing` | Yes, as `ringing` | -| `answered` | Yes, as `answered` | -| `ending` | No | -| `ended` | Yes, as `ended` | -| `busy`, `no-answer`, `failed`, `canceled` | No | - - -`busy`, `no-answer`, `failed`, and `canceled` are not `status_events` values, so none of them -delivers a callback, even with the default `status_events` of `["ended"]`. Subscribe to `answered` -and set an application deadline based on the call's ring `timeout`, allowing for webhook delivery -delay. If no `answered` webhook arrives by that deadline, check the call's -[voice log](/docs/apis/rest/voice-logs/list-voice-logs) to confirm the outcome. - - - - - -Relay carries commands and live events over a persistent WebSocket. -The SDK's `dial` returns a call object after answer or raises `RelayError` on failure. -Use `call.on` for state and action events, and `action.wait()` to wait for an action such as -playback to finish. See the [Python](/docs/server-sdks/reference/python/relay/call/on) or -[TypeScript](/docs/server-sdks/reference/typescript/relay/call/on) event reference. - - - -One persistent WebSocket carries commands from your application to SignalWire and live events back to your application. Your code decides how to react: call answered can trigger play, collected keypad input can trigger connect to a chosen destination, and playback finished can trigger hangup. These are independent examples of application decisions; input events require an active collection operation. - - - - - -```mermaid -flowchart TB - subgraph Channel[One persistent, bidirectional WebSocket] - direction LR - App[Your application: decides what happens next] - SW[SignalWire: places and controls the call] - App -->|Commands: dial, play, connect, hangup| SW - SW -.->|Live events: call state, playback, collected input| App - end - - subgraph Reactions[Examples of application decisions, not an automatic sequence] - Answered[Call answered: calling.call.dial] --> Greeting[Your app: start the greeting] --> Play[Command: play] - Input[Keypad input collected: calling.call.collect] --> Route[Your app: route to dispatch] --> Connect[Command: connect] - Finished[Playback finished: calling.call.play] --> End[Your app: end the conversation] --> Hangup[Command: hangup] - end -``` - - - - -The initial `calling.dial` response acknowledges the request. A later `calling.call.dial` -event carries the dial outcome. The Server SDK waits for this event before returning a call object. - - - - +[TypeScript](/docs/server-sdks/reference/typescript/relay/call/ai) cover every parameter. ## Next steps From a00684de6eb6be4651ebea152e5f55cb0346aff3 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 9 Sep 2026 09:38:44 -0400 Subject: [PATCH 042/103] docs: redraw the Relay diagram as a command and event sequence Replace the concept diagram with a sequence diagram that follows the guide's own Relay example: dial, play the announcement, hang up. Each command your code sends is paired with the events SignalWire emits in return, so the bidirectional WebSocket and the cause of each event are visible. Method and event names come from the Python SDK and the Relay events reference. Rewrite the Relay tab prose to introduce the same ideas: every command is acknowledged at once, some events are the direct result of a command and others report what happened on the call, and the SDK resolves dial() and action.wait() from those events. Co-Authored-By: Claude Fable 5.1 --- .../outbound-call-relay-lifecycle-themed.svg | 168 ++++++++---------- .../pages/calling/voice/outbound-calling.mdx | 54 ++++-- 2 files changed, 113 insertions(+), 109 deletions(-) diff --git a/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg b/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg index 31985c6af9..f7a55a2fff 100644 --- a/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg +++ b/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg @@ -1,6 +1,6 @@ - - Relay: commands and events over one bidirectional WebSocket - Your application and SignalWire share one persistent WebSocket. Your application sends commands to control the call; SignalWire sends live events as the call changes. Your application decides how to react: a call-answered event can trigger play, collected keypad input can trigger connect to a chosen destination, and a playback-finished event can trigger hangup. These are examples of application decisions, not an automatic sequence. Input events require an active collection operation. + + Relay: commands and events over one WebSocket + Your code and SignalWire share one persistent WebSocket. Dial: your code sends calling.dial, which SignalWire acknowledges at once; as the call progresses SignalWire sends calling.call.state events created, ringing, and answered, then calling.call.dial answered, which makes dial() return the call. Play: your code sends calling.play; SignalWire sends calling.call.play playing, then finished, which makes action.wait() return. Hang up: your code sends calling.end; SignalWire sends calling.call.state ending, then ended with end_reason hangup. - - - - - - - - - + + + - - One persistent, bidirectional WebSocket - - - - Your application - Decides what - happens next - - Commands → - dial, play, connect, hangup - - - ← Live events - call state, playback, collected input - - SignalWire - Places and - controls the call - - Examples: your code reacts to an event - Event from SignalWire - Your app decides - Command to SignalWire - - - - Call answered - calling.call.dial - - - Start the greeting - - - play(...) - announcement audio - - - Digits collected - calling.call.collect - - - Route to dispatch - - - connect(...) - chosen destination - - - Playback finished - calling.call.play - - - End the conversation - - - hangup() - finish this call - - Input events require an active collection operation. + + + + + + Your code + + + + + SignalWire + + one persistent WebSocket, both directions + Dial + + dial + calling.dial + + acknowledged at once + created → ringing → answered + calling.call.state + + as the call progresses + answered + calling.call.dial + + dial() returns the call + Play the announcement + + play + calling.play + + playing → finished + calling.call.play + + action.wait() returns + Hang up + + hangup + calling.end + + ending → ended + calling.call.state + + end_reason: hangup + + Command you send + + Event from SignalWire diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 8e2bc80fc9..23407e473b 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -110,34 +110,52 @@ sequenceDiagram -Relay carries commands and live events over a persistent WebSocket. -The SDK's `dial` returns a call object after answer or raises `RelayError` on failure. -Use `call.on` for state and action events, and `action.wait()` to wait for an action such as -playback to finish. See the [Python](/docs/server-sdks/reference/python/relay/call/on) or -[TypeScript](/docs/server-sdks/reference/typescript/relay/call/on) event reference. +Relay keeps one WebSocket open between your application and SignalWire. Your application sends +commands over it, such as `calling.dial`, `calling.play`, and `calling.end`. SignalWire acknowledges +each command at once, then sends events back over the same connection as the call progresses. + +Some events are the direct result of a command: `calling.dial` produces the `created` state, +`calling.play` produces `playing`, and `calling.end` produces `ending`. Others report what happened +on the call: `ringing` and `answered` as the destination responds, `finished` when playback +completes, and `ended` with an `end_reason` once the call is over. + +The SDK turns this exchange into ordinary calls. `dial()` returns the call once the +`calling.call.dial` event reports `answered`, and `action.wait()` returns when the play event +reports `finished`. Use `call.on` to handle any event yourself, as described in the +[Python](/docs/server-sdks/reference/python/relay/events) or +[TypeScript](/docs/server-sdks/reference/typescript/relay/events) event reference. + +This flow follows the Relay example above: dial, play the announcement, hang up. -One persistent WebSocket carries commands from your application to SignalWire and live events back to your application. Your code decides how to react: call answered can trigger play, collected keypad input can trigger connect to a chosen destination, and playback finished can trigger hangup. These are independent examples of application decisions; input events require an active collection operation. +Your code and SignalWire share one persistent WebSocket. Dial: your code sends calling.dial, which SignalWire acknowledges at once; as the call progresses SignalWire sends calling.call.state events created, ringing, and answered, then calling.call.dial answered, which makes dial() return the call. Play: your code sends calling.play; SignalWire sends calling.call.play playing, then finished, which makes action.wait() return. Hang up: your code sends calling.end; SignalWire sends calling.call.state ending, then ended with end_reason hangup. ```mermaid -flowchart TB - subgraph Channel[One persistent, bidirectional WebSocket] - direction LR - App[Your application: decides what happens next] - SW[SignalWire: places and controls the call] - App -->|Commands: dial, play, connect, hangup| SW - SW -.->|Live events: call state, playback, collected input| App - end +sequenceDiagram + participant App as Your code + participant SW as SignalWire - subgraph Reactions[Examples of application decisions, not an automatic sequence] - Answered[Call answered: calling.call.dial] --> Greeting[Your app: start the greeting] --> Play[Command: play] - Input[Keypad input collected: calling.call.collect] --> Route[Your app: route to dispatch] --> Connect[Command: connect] - Finished[Playback finished: calling.call.play] --> End[Your app: end the conversation] --> Hangup[Command: hangup] + Note over App,SW: One persistent WebSocket, both directions + rect rgba(0,0,0,0.04) + Note over App,SW: Dial + App->>SW: calling.dial (acknowledged at once) + SW-->>App: calling.call.state: created, ringing, answered + SW-->>App: calling.call.dial: answered (dial returns the call) + end + rect rgba(0,0,0,0.04) + Note over App,SW: Play the announcement + App->>SW: calling.play + SW-->>App: calling.call.play: playing, then finished (action.wait returns) + end + rect rgba(0,0,0,0.04) + Note over App,SW: Hang up + App->>SW: calling.end + SW-->>App: calling.call.state: ending, then ended (end_reason hangup) end ``` From f55f399fa88b5d01cea9d2fcaddcc28cea72d2f8 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 9 Sep 2026 10:40:00 -0400 Subject: [PATCH 043/103] docs: rewrite the outbound calling overview and prerequisites Open with what you can dial, including web clients and Resources, and the two APIs you can dial with: REST and the real-time WebSocket API. Note that the Server SDKs cover both and the Browser SDK uses the real-time API. Cut Prerequisites to what a first call needs: Project ID and API token (or a SAT minted from them), a client such as cURL or an SDK, and a number to call from when dialing a phone number. The destination, caller ID, and credential tables are gone. Add REST and WebSocket code to the tracking section, and give both Tabs blocks a shared groupId so a reader's API choice follows them down the page. Co-Authored-By: Claude Fable 5.1 --- .../pages/calling/voice/outbound-calling.mdx | 138 +++++++++++++----- 1 file changed, 103 insertions(+), 35 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 23407e473b..63be766f85 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -14,31 +14,45 @@ max-toc-depth: 3 [swml-ai]: /docs/swml/reference/calling/ai [ai-best-practices]: /docs/platform/ai/best-practices [tcpa]: /docs/platform/compliance/tcpa -[resource-addresses]: /docs/platform/addresses [resources]: /docs/platform/resources +[sdk-install]: /docs/server-sdks/guides/installation +[browser-sdk]: /docs/browser-sdk/v4/guides/overview -Place outbound calls to phone numbers, SIP endpoints, or SignalWire resources. -[Call from your server](#place-a-call-from-a-server), -[let a user call from your web app](#place-a-call-with-the-browser-sdk), or -[connect someone to an AI agent](#run-an-ai-agent). +Place outbound calls to phone numbers, SIP endpoints, web clients, or SignalWire Resources from +your own code. SignalWire places the call and, once someone answers, runs whatever you asked for: +a spoken announcement, a conversation with an AI agent, or a connection to another destination. -## Prerequisites +You can dial: + +- **Phone numbers**, in E.164 format such as `+15557654321`. +- **SIP endpoints**, by SIP URI such as `sip:support@example.com`. +- **Web clients**, meaning users signed in to your app with the [Browser SDK][browser-auth]. +- **[SignalWire Resources][resources]**, by resource address: subscribers, AI agents, SWML scripts, + Call Flows, and conference rooms. -### Choose a destination +And you can place the call with either of two APIs: -| Destination | Dial | Example | -|---|---|---| -| Phone | Its number in E.164 | `+15557654321` | -| SIP endpoint | Its SIP URI | `sip:support@example.com` | -| Subscriber | Their resource address | `/private/support-rep` | -| Application | Its resource address | `/public/support-agent` | -| Conference room | Its resource address | `/public/team-standup` | +- **The REST API.** Send one request and SignalWire handles the call from there, following the + instructions you include. +- **The real-time WebSocket API (Relay).** Keep a connection open, send commands, and receive + events as the call progresses. -Subscribers (your users), applications (AI agents, SWML scripts, or Call Flows), and conference -rooms are [Resources][resources]. Each has a [resource address][resource-addresses] in the form -`/context/name`, where the context is `public` or `private`. +The Server SDKs for Python and TypeScript support both. The Browser SDK uses the real-time API +to place calls from a web page. The examples below cover +[a call from your server](#place-a-call-from-a-server), +[a call from your web app](#place-a-call-with-the-browser-sdk), and +[a call into an AI agent](#run-an-ai-agent). + +## Prerequisites -### Set up your caller ID +- **Your Project ID and an API token** from the Dashboard's [API credentials][api-credentials] + page. Server-side calls use them directly. The Browser SDK uses a + [Subscriber Access Token (SAT)][browser-auth] instead, which your backend creates with those + same credentials. +- **A client to send the request**: cURL, the [Python or TypeScript Server SDK][sdk-install], or + the [Browser SDK][browser-sdk] for calls from a web page. +- **A phone number to call from**, if you're dialing a phone number. Use a voice-capable number + [in your project][phone-numbers] or one you've [verified as a caller ID][caller-id]. A [trial project][trial-mode] can dial only numbers it has purchased or verified, and cannot call @@ -46,22 +60,9 @@ internationally at all. Any other project can dial any number, but needs [international dialing enabled][international] before it reaches another country. -| Dialing | What you need | -|---|---| -| A phone number | A voice-capable PSTN number in your project to present as the caller ID, either one you [bought from SignalWire][phone-numbers] or one you [verified as a caller ID][caller-id] | -| A SIP URI | A caller ID, which here can be an E.164 number, a SIP URI, or a short token of up to 11 letters, digits, or underscores, plus a SIP username and password if the far end authenticates | -| A resource address | A Resource in your Space already reachable at that address, and a credential permitted to dial it. Its caller ID isn't format-checked | - -### Get credentials - -| Dialing from | Use | -|---|---| -| Your server | Your Project ID and an API token with voice permissions, from the Dashboard's [API credentials][api-credentials] page | -| Your web or mobile app | A [Subscriber Access Token (SAT)][browser-auth] that your backend issues for the signed-in user | - ## Track the call's progress - + Once you create a call, SignalWire tracks its status through the lifecycle: `queued`, `created`, @@ -78,6 +79,48 @@ and `ended`. If you omit it, you receive `ended` only. Most applications ask for confirm someone picked up, and `ended`, to know the call is over. The `ended` payload includes an `end_reason`, such as `hangup`, `busy`, or `noAnswer`, so you can tell how it ended. + + +```bash +curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ + -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "+15551234567", + "to": "+15557654321", + "url": "https://example.com/swml", + "status_url": "https://example.com/call-status", + "status_events": ["ringing", "answered", "ended"] + } + }' +``` + + +```python +call = client.calling.dial( + from_="+15551234567", + to="+15557654321", + url="https://example.com/swml", + status_url="https://example.com/call-status", + status_events=["ringing", "answered", "ended"], +) +``` + + +```typescript +const call = await client.calling.dial({ + from: "+15551234567", + to: "+15557654321", + url: "https://example.com/swml", + status_url: "https://example.com/call-status", + status_events: ["ringing", "answered", "ended"], +}); +``` + + + This flow shows a call with `ringing`, `answered`, and `ended` status events enabled: @@ -108,7 +151,7 @@ sequenceDiagram - + Relay keeps one WebSocket open between your application and SignalWire. Your application sends commands over it, such as `calling.dial`, `calling.play`, and `calling.end`. SignalWire acknowledges @@ -125,6 +168,31 @@ reports `finished`. Use `call.on` to handle any event yourself, as described in [Python](/docs/server-sdks/reference/python/relay/events) or [TypeScript](/docs/server-sdks/reference/typescript/relay/events) event reference. +Because `dial()` returns once the call is answered, handlers you attach to the returned call see +the states that follow, such as `ending` and `ended`: + + + +```python +from signalwire.relay.event import CallStateEvent + +def handle_state(event: CallStateEvent): + print(f"State: {event.call_state}, reason: {event.end_reason}") + +call.on("calling.call.state", handle_state) +``` + + +```typescript +import { CallStateEvent } from "@signalwire/sdk"; + +call.on("calling.call.state", (event: CallStateEvent) => { + console.log(`State: ${event.callState}, reason: ${event.endReason}`); +}); +``` + + + This flow follows the Relay example above: dial, play the announcement, hang up. @@ -171,7 +239,7 @@ sequenceDiagram Place a call that plays a spoken announcement when someone answers. Choose REST to have SignalWire run supplied instructions, or Relay to control the call through commands and events. - + Supply a SignalWire Markup Language (SWML) document in `swml` or at `url` to define what happens @@ -271,7 +339,7 @@ The response identifies the queued call: To receive status webhooks, see [Track the call's progress](#track-the-calls-progress). - + Run this on your server with your credentials and phone numbers. It dials the destination, plays the announcement after answer, waits for playback to finish, and hangs up. From 51a53ecf5fb23d19f8902a4a28589fbd08643dd4 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 9 Sep 2026 11:20:53 -0400 Subject: [PATCH 044/103] docs: clarify outbound calling overview and setup --- .../pages/calling/voice/outbound-calling.mdx | 61 ++++++++----------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 63be766f85..3e1f3ceafe 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -16,43 +16,31 @@ max-toc-depth: 3 [tcpa]: /docs/platform/compliance/tcpa [resources]: /docs/platform/resources [sdk-install]: /docs/server-sdks/guides/installation +[server-sdks]: /docs/server-sdks [browser-sdk]: /docs/browser-sdk/v4/guides/overview -Place outbound calls to phone numbers, SIP endpoints, web clients, or SignalWire Resources from -your own code. SignalWire places the call and, once someone answers, runs whatever you asked for: -a spoken announcement, a conversation with an AI agent, or a connection to another destination. +SignalWire lets your application place outbound calls to phone numbers, SIP endpoints, +web clients, and [SignalWire Resources][resources]. Use outbound calls to play an announcement, +start a conversation with an AI agent, or connect someone to another destination. -You can dial: +## Choose an API -- **Phone numbers**, in E.164 format such as `+15557654321`. -- **SIP endpoints**, by SIP URI such as `sip:support@example.com`. -- **Web clients**, meaning users signed in to your app with the [Browser SDK][browser-auth]. -- **[SignalWire Resources][resources]**, by resource address: subscribers, AI agents, SWML scripts, - Call Flows, and conference rooms. +- **REST:** Send a request with instructions for SignalWire to run when the call is answered. +- **WebSocket (Relay):** Control the call by sending commands and receiving events over a + persistent connection. -And you can place the call with either of two APIs: - -- **The REST API.** Send one request and SignalWire handles the call from there, following the - instructions you include. -- **The real-time WebSocket API (Relay).** Keep a connection open, send commands, and receive - events as the call progresses. - -The Server SDKs for Python and TypeScript support both. The Browser SDK uses the real-time API -to place calls from a web page. The examples below cover -[a call from your server](#place-a-call-from-a-server), -[a call from your web app](#place-a-call-with-the-browser-sdk), and -[a call into an AI agent](#run-an-ai-agent). +The [Server SDK][server-sdks] supports both APIs. The [Browser SDK][browser-sdk] uses the real-time +API for calls from a web page. ## Prerequisites -- **Your Project ID and an API token** from the Dashboard's [API credentials][api-credentials] - page. Server-side calls use them directly. The Browser SDK uses a - [Subscriber Access Token (SAT)][browser-auth] instead, which your backend creates with those - same credentials. -- **A client to send the request**: cURL, the [Python or TypeScript Server SDK][sdk-install], or - the [Browser SDK][browser-sdk] for calls from a web page. -- **A phone number to call from**, if you're dialing a phone number. Use a voice-capable number - [in your project][phone-numbers] or one you've [verified as a caller ID][caller-id]. +- **Credentials:** Get your Project ID and API token from the Dashboard's + [API credentials][api-credentials] page. For browser calls, use them on your backend to create a + [Subscriber Access Token (SAT)][browser-auth]. +- **Tools:** Use cURL for REST requests, or install the [Server SDK][sdk-install] or + [Browser SDK][browser-sdk]. +- **Caller ID for phone calls:** Use a voice-capable [number in your project][phone-numbers] or a + [verified caller ID][caller-id]. A [trial project][trial-mode] can dial only numbers it has purchased or verified, and cannot call @@ -60,6 +48,10 @@ internationally at all. Any other project can dial any number, but needs [international dialing enabled][international] before it reaches another country. +Start with an example to [call from your server](#place-a-call-from-a-server), +[call from your web app](#place-a-call-with-the-browser-sdk), or +[run an AI agent](#run-an-ai-agent). The next section explains how to track the call's progress. + ## Track the call's progress @@ -166,7 +158,8 @@ The SDK turns this exchange into ordinary calls. `dial()` returns the call once `calling.call.dial` event reports `answered`, and `action.wait()` returns when the play event reports `finished`. Use `call.on` to handle any event yourself, as described in the [Python](/docs/server-sdks/reference/python/relay/events) or -[TypeScript](/docs/server-sdks/reference/typescript/relay/events) event reference. +[TypeScript](/docs/server-sdks/reference/typescript/relay/events) event reference, or the +equivalent for your language. Because `dial()` returns once the call is answered, handlers you attach to the returned call see the states that follow, such as `ending` and `ended`: @@ -442,7 +435,7 @@ hangupButton.onclick = () => call.hangup(); Replace the announcement in the [server example](#place-a-call-from-a-server) with an AI agent: - **SWML:** Host the document below and pass its `url` in place of the inline `swml`. -- **Relay:** Replace the playback and hangup steps with the Python or TypeScript snippet below. +- **Relay:** Replace the playback and hangup steps with the Relay snippet below. Here, `call` is the answered call returned by `dial`. @@ -512,9 +505,9 @@ await call.hangup(); `static_greeting_no_barge` lets the greeting finish before the conversation begins. The SWML -[`ai` method][swml-ai] and the Relay `ai` references for -[Python](/docs/server-sdks/reference/python/relay/call/ai) and -[TypeScript](/docs/server-sdks/reference/typescript/relay/call/ai) cover every parameter. +[`ai` method][swml-ai] reference covers every parameter, as does the Relay `ai` reference for +[Python](/docs/server-sdks/reference/python/relay/call/ai), +[TypeScript](/docs/server-sdks/reference/typescript/relay/call/ai), or your language's SDK. ## Next steps From 0e208c8d94f1c4ba70670492d42cc6f8e8a99618 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 9 Sep 2026 11:39:04 -0400 Subject: [PATCH 045/103] docs: remove outbound guide SDK summary and navigation prose --- .../platform/pages/calling/voice/outbound-calling.mdx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 3e1f3ceafe..03f92bfae6 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -16,7 +16,6 @@ max-toc-depth: 3 [tcpa]: /docs/platform/compliance/tcpa [resources]: /docs/platform/resources [sdk-install]: /docs/server-sdks/guides/installation -[server-sdks]: /docs/server-sdks [browser-sdk]: /docs/browser-sdk/v4/guides/overview SignalWire lets your application place outbound calls to phone numbers, SIP endpoints, @@ -29,9 +28,6 @@ start a conversation with an AI agent, or connect someone to another destination - **WebSocket (Relay):** Control the call by sending commands and receiving events over a persistent connection. -The [Server SDK][server-sdks] supports both APIs. The [Browser SDK][browser-sdk] uses the real-time -API for calls from a web page. - ## Prerequisites - **Credentials:** Get your Project ID and API token from the Dashboard's @@ -48,10 +44,6 @@ internationally at all. Any other project can dial any number, but needs [international dialing enabled][international] before it reaches another country. -Start with an example to [call from your server](#place-a-call-from-a-server), -[call from your web app](#place-a-call-with-the-browser-sdk), or -[run an AI agent](#run-an-ai-agent). The next section explains how to track the call's progress. - ## Track the call's progress From ffda15f40dfb3324a80220d15eaa3570e7fcea13 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 9 Sep 2026 11:46:57 -0400 Subject: [PATCH 046/103] docs: add first outbound call walkthrough --- .../pages/calling/voice/outbound-calling.mdx | 382 +++++++++--------- 1 file changed, 202 insertions(+), 180 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 03f92bfae6..8a7e4dae60 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -44,194 +44,36 @@ internationally at all. Any other project can dial any number, but needs [international dialing enabled][international] before it reaches another country. -## Track the call's progress +## Make your first call - - +Call a phone you can answer and play a short announcement from your server. -Once you create a call, SignalWire tracks its status through the lifecycle: `queued`, `created`, -`ringing`, `answered`, and `ended`. To follow that lifecycle from your application, add a -`status_url` to the `dial` request. SignalWire sends a POST to that URL each time the call reaches -a status you asked about. + -The `dial` response itself gives you the first status. It returns the call `id` with status -`queued`, meaning SignalWire accepted the request and is about to place the call. Everything after -that arrives at your `status_url`. +### Set your credentials and phone numbers -Choose which statuses to receive with `status_events`: any of `created`, `ringing`, `answered`, -and `ended`. If you omit it, you receive `ended` only. Most applications ask for `answered`, to -confirm someone picked up, and `ended`, to know the call is over. The `ended` payload includes an -`end_reason`, such as `hangup`, `busy`, or `noAnswer`, so you can tell how it ended. +Replace these values in the code sample you choose: - - -```bash -curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ - -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "command": "dial", - "params": { - "from": "+15551234567", - "to": "+15557654321", - "url": "https://example.com/swml", - "status_url": "https://example.com/call-status", - "status_events": ["ringing", "answered", "ended"] - } - }' -``` - - -```python -call = client.calling.dial( - from_="+15551234567", - to="+15557654321", - url="https://example.com/swml", - status_url="https://example.com/call-status", - status_events=["ringing", "answered", "ended"], -) -``` - - -```typescript -const call = await client.calling.dial({ - from: "+15551234567", - to: "+15557654321", - url: "https://example.com/swml", - status_url: "https://example.com/call-status", - status_events: ["ringing", "answered", "ended"], -}); -``` - - +| Value | Replace with | +|---|---| +| `YOUR_SPACE` | Your Space's subdomain in `YOUR_SPACE.signalwire.com` | +| `YOUR_PROJECT_ID` | Your Project ID | +| `YOUR_API_TOKEN` | Your API token | +| `+15551234567` | Your caller ID number | +| `+15557654321` | A phone number you can answer | -This flow shows a call with `ringing`, `answered`, and `ended` status events enabled: - - - -Your code sends a dial request with from, to, and SWML. SignalWire returns the call id with status queued and rings the destination and reports status ringing. When the person answers, SignalWire reports status answered and runs your SWML. When the call finishes, SignalWire reports status ended. - - - - - -```mermaid -sequenceDiagram - participant App as Your code - participant SW as SignalWire - participant Dest as Destination - - App->>SW: dial: from, to, SWML - SW-->>App: call id, status queued - SW->>Dest: rings - SW-->>App: status ringing - Dest->>SW: answers - SW-->>App: status answered - Note over SW,Dest: Your SWML runs - Note over SW,Dest: Call finishes - SW-->>App: status ended -``` - - - - - - -Relay keeps one WebSocket open between your application and SignalWire. Your application sends -commands over it, such as `calling.dial`, `calling.play`, and `calling.end`. SignalWire acknowledges -each command at once, then sends events back over the same connection as the call progresses. - -Some events are the direct result of a command: `calling.dial` produces the `created` state, -`calling.play` produces `playing`, and `calling.end` produces `ending`. Others report what happened -on the call: `ringing` and `answered` as the destination responds, `finished` when playback -completes, and `ended` with an `end_reason` once the call is over. - -The SDK turns this exchange into ordinary calls. `dial()` returns the call once the -`calling.call.dial` event reports `answered`, and `action.wait()` returns when the play event -reports `finished`. Use `call.on` to handle any event yourself, as described in the -[Python](/docs/server-sdks/reference/python/relay/events) or -[TypeScript](/docs/server-sdks/reference/typescript/relay/events) event reference, or the -equivalent for your language. - -Because `dial()` returns once the call is answered, handlers you attach to the returned call see -the states that follow, such as `ending` and `ended`: - - - -```python -from signalwire.relay.event import CallStateEvent - -def handle_state(event: CallStateEvent): - print(f"State: {event.call_state}, reason: {event.end_reason}") - -call.on("calling.call.state", handle_state) -``` - - -```typescript -import { CallStateEvent } from "@signalwire/sdk"; - -call.on("calling.call.state", (event: CallStateEvent) => { - console.log(`State: ${event.callState}, reason: ${event.endReason}`); -}); -``` - - - -This flow follows the Relay example above: dial, play the announcement, hang up. - - - -Your code and SignalWire share one persistent WebSocket. Dial: your code sends calling.dial, which SignalWire acknowledges at once; as the call progresses SignalWire sends calling.call.state events created, ringing, and answered, then calling.call.dial answered, which makes dial() return the call. Play: your code sends calling.play; SignalWire sends calling.call.play playing, then finished, which makes action.wait() return. Hang up: your code sends calling.end; SignalWire sends calling.call.state ending, then ended with end_reason hangup. - - - - - -```mermaid -sequenceDiagram - participant App as Your code - participant SW as SignalWire - - Note over App,SW: One persistent WebSocket, both directions - rect rgba(0,0,0,0.04) - Note over App,SW: Dial - App->>SW: calling.dial (acknowledged at once) - SW-->>App: calling.call.state: created, ringing, answered - SW-->>App: calling.call.dial: answered (dial returns the call) - end - rect rgba(0,0,0,0.04) - Note over App,SW: Play the announcement - App->>SW: calling.play - SW-->>App: calling.call.play: playing, then finished (action.wait returns) - end - rect rgba(0,0,0,0.04) - Note over App,SW: Hang up - App->>SW: calling.end - SW-->>App: calling.call.state: ending, then ended (end_reason hangup) - end -``` - - - - - - -## Examples +Use E.164 format for both numbers, including `+` and the country code. ### Place a call from a server -Place a call that plays a spoken announcement when someone answers. Choose REST to have -SignalWire run supplied instructions, or Relay to control the call through commands and events. +Choose REST or WebSocket, then select a code sample. Run the cURL request in your terminal or +the SDK code in your server environment. -Supply a SignalWire Markup Language (SWML) document in `swml` or at `url` to define what happens -after answer: play audio, run an AI agent, or connect another destination. - -This example includes the announcement in `swml`. Replace the credentials and phone numbers, -then run it from your terminal or backend. +The request includes a SignalWire Markup Language (SWML) document in `swml`. SignalWire runs +it when the destination answers, playing the announcement below. @@ -308,7 +150,7 @@ console.log(call.id); -The response identifies the queued call: +The REST API returns a call `id` and status `queued`, confirming that it accepted the request: ```json { @@ -321,13 +163,11 @@ The response identifies the queued call: } ``` -To receive status webhooks, see [Track the call's progress](#track-the-calls-progress). - -Run this on your server with your credentials and phone numbers. It dials the destination, plays -the announcement after answer, waits for playback to finish, and hangs up. +The SDK opens a WebSocket connection, dials the destination, and plays the announcement after +answer. It waits for playback to finish, then hangs up and closes the connection. @@ -399,6 +239,188 @@ await client.disconnect(); +### Answer the call + +Answer the destination phone. You should hear “Hi, this is Bayview Taxi” followed by the driver +arrival message. The call ends when the announcement finishes. + + + +## Track the call's progress + + + + +Once you create a call, SignalWire tracks its status through the lifecycle: `queued`, `created`, +`ringing`, `answered`, and `ended`. To follow that lifecycle from your application, add a +`status_url` to the `dial` request. SignalWire sends a POST to that URL each time the call reaches +a status you asked about. + +The `dial` response itself gives you the first status. It returns the call `id` with status +`queued`, meaning SignalWire accepted the request and is about to place the call. Everything after +that arrives at your `status_url`. + +Choose which statuses to receive with `status_events`: any of `created`, `ringing`, `answered`, +and `ended`. If you omit it, you receive `ended` only. Most applications ask for `answered`, to +confirm someone picked up, and `ended`, to know the call is over. The `ended` payload includes an +`end_reason`, such as `hangup`, `busy`, or `noAnswer`, so you can tell how it ended. + + + +```bash +curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ + -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "+15551234567", + "to": "+15557654321", + "url": "https://example.com/swml", + "status_url": "https://example.com/call-status", + "status_events": ["ringing", "answered", "ended"] + } + }' +``` + + +```python +call = client.calling.dial( + from_="+15551234567", + to="+15557654321", + url="https://example.com/swml", + status_url="https://example.com/call-status", + status_events=["ringing", "answered", "ended"], +) +``` + + +```typescript +const call = await client.calling.dial({ + from: "+15551234567", + to: "+15557654321", + url: "https://example.com/swml", + status_url: "https://example.com/call-status", + status_events: ["ringing", "answered", "ended"], +}); +``` + + + +This flow shows a call with `ringing`, `answered`, and `ended` status events enabled: + + + +Your code sends a dial request with from, to, and SWML. SignalWire returns the call id with status queued and rings the destination and reports status ringing. When the person answers, SignalWire reports status answered and runs your SWML. When the call finishes, SignalWire reports status ended. + + + + + +```mermaid +sequenceDiagram + participant App as Your code + participant SW as SignalWire + participant Dest as Destination + + App->>SW: dial: from, to, SWML + SW-->>App: call id, status queued + SW->>Dest: rings + SW-->>App: status ringing + Dest->>SW: answers + SW-->>App: status answered + Note over SW,Dest: Your SWML runs + Note over SW,Dest: Call finishes + SW-->>App: status ended +``` + + + + + + +Relay keeps one WebSocket open between your application and SignalWire. Your application sends +commands over it, such as `calling.dial`, `calling.play`, and `calling.end`. SignalWire acknowledges +each command at once, then sends events back over the same connection as the call progresses. + +Some events are the direct result of a command: `calling.dial` produces the `created` state, +`calling.play` produces `playing`, and `calling.end` produces `ending`. Others report what happened +on the call: `ringing` and `answered` as the destination responds, `finished` when playback +completes, and `ended` with an `end_reason` once the call is over. + +The SDK turns this exchange into ordinary calls. `dial()` returns the call once the +`calling.call.dial` event reports `answered`, and `action.wait()` returns when the play event +reports `finished`. Use `call.on` to handle any event yourself, as described in the +[Python](/docs/server-sdks/reference/python/relay/events) or +[TypeScript](/docs/server-sdks/reference/typescript/relay/events) event reference, or the +equivalent for your language. + +Because `dial()` returns once the call is answered, handlers you attach to the returned call see +the states that follow, such as `ending` and `ended`: + + + +```python +from signalwire.relay.event import CallStateEvent + +def handle_state(event: CallStateEvent): + print(f"State: {event.call_state}, reason: {event.end_reason}") + +call.on("calling.call.state", handle_state) +``` + + +```typescript +import { CallStateEvent } from "@signalwire/sdk"; + +call.on("calling.call.state", (event: CallStateEvent) => { + console.log(`State: ${event.callState}, reason: ${event.endReason}`); +}); +``` + + + +This flow follows your first call: dial, play the announcement, hang up. + + + +Your code and SignalWire share one persistent WebSocket. Dial: your code sends calling.dial, which SignalWire acknowledges at once; as the call progresses SignalWire sends calling.call.state events created, ringing, and answered, then calling.call.dial answered, which makes dial() return the call. Play: your code sends calling.play; SignalWire sends calling.call.play playing, then finished, which makes action.wait() return. Hang up: your code sends calling.end; SignalWire sends calling.call.state ending, then ended with end_reason hangup. + + + + + +```mermaid +sequenceDiagram + participant App as Your code + participant SW as SignalWire + + Note over App,SW: One persistent WebSocket, both directions + rect rgba(0,0,0,0.04) + Note over App,SW: Dial + App->>SW: calling.dial (acknowledged at once) + SW-->>App: calling.call.state: created, ringing, answered + SW-->>App: calling.call.dial: answered (dial returns the call) + end + rect rgba(0,0,0,0.04) + Note over App,SW: Play the announcement + App->>SW: calling.play + SW-->>App: calling.call.play: playing, then finished (action.wait returns) + end + rect rgba(0,0,0,0.04) + Note over App,SW: Hang up + App->>SW: calling.end + SW-->>App: calling.call.state: ending, then ended (end_reason hangup) + end +``` + + + + + + +## Examples + ### Place a call with the Browser SDK Let someone place and speak on a call from your web app. Supply a From cf864271432253f0757013b446116dc8a72d2fb2 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 9 Sep 2026 12:15:49 -0400 Subject: [PATCH 047/103] docs: improve outbound calling guide flow --- .../pages/calling/voice/outbound-calling.mdx | 306 +++++++++++++----- 1 file changed, 226 insertions(+), 80 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 8a7e4dae60..0ac99864d8 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -1,7 +1,7 @@ --- title: Outbound calling slug: /voice/outbound-calling -description: Learn how outbound calls start, what you can dial, and how to place a call from your server or your own app. +description: Place your first outbound phone call, track its progress, and add AI or browser calling to your application. max-toc-depth: 3 --- @@ -14,29 +14,41 @@ max-toc-depth: 3 [swml-ai]: /docs/swml/reference/calling/ai [ai-best-practices]: /docs/platform/ai/best-practices [tcpa]: /docs/platform/compliance/tcpa -[resources]: /docs/platform/resources [sdk-install]: /docs/server-sdks/guides/installation [browser-sdk]: /docs/browser-sdk/v4/guides/overview +[compatibility-api]: /docs/compatibility-api/rest +[webhooks]: /docs/platform/webhooks -SignalWire lets your application place outbound calls to phone numbers, SIP endpoints, -web clients, and [SignalWire Resources][resources]. Use outbound calls to play an announcement, -start a conversation with an AI agent, or connect someone to another destination. +Place an outbound phone call with SignalWire and choose what happens when the destination answers. +Start by calling your own phone and playing a short announcement, then track the call's progress, +run an AI agent, or let users call from your web app. -## Choose an API +## Choose how to place your call -- **REST:** Send a request with instructions for SignalWire to run when the call is answered. -- **WebSocket (Relay):** Control the call by sending commands and receiving events over a - persistent connection. +For your first call, start with the REST cURL example below. If you're adding outbound calling to +an existing application, choose the approach that fits how you want to control the call. -## Prerequisites +| What you want to do | Where to start | +|---|---| +| Have your server start a call with instructions to run when it's answered | [REST Calling API](#place-a-call-from-a-server), using cURL or a Server SDK | +| Send commands and receive call events over a persistent connection | [WebSocket (Relay)](#place-a-call-from-a-server), using a Server SDK | +| Let someone place and speak on a call from your web app | [Browser SDK](#place-a-call-with-the-browser-sdk) | +| Add outbound calls to an existing Compatibility API application | [Compatibility API reference][compatibility-api] | + +## Prepare for your first call -- **Credentials:** Get your Project ID and API token from the Dashboard's - [API credentials][api-credentials] page. For browser calls, use them on your backend to create a - [Subscriber Access Token (SAT)][browser-auth]. -- **Tools:** Use cURL for REST requests, or install the [Server SDK][sdk-install] or - [Browser SDK][browser-sdk]. -- **Caller ID for phone calls:** Use a voice-capable [number in your project][phone-numbers] or a +For the server walkthrough, have these values ready: + +- Your Space URL, such as `YOUR_SPACE.signalwire.com`. +- Your Project ID and API token from the Dashboard's [API credentials][api-credentials] page. + Enable the token's **Voice** permission for the Calling API. +- A caller ID: a voice-capable [number in your project][phone-numbers] or a [verified caller ID][caller-id]. +- A destination phone you can answer. + +Use cURL in your terminal to make the first REST request. If you choose Python or TypeScript, +follow the setup comments in that sample; the [Server SDK installation guide][sdk-install] +covers runtime requirements and environment setup. A [trial project][trial-mode] can dial only numbers it has purchased or verified, and cannot call @@ -66,8 +78,8 @@ Use E.164 format for both numbers, including `+` and the country code. ### Place a call from a server -Choose REST or WebSocket, then select a code sample. Run the cURL request in your terminal or -the SDK code in your server environment. +Select **REST** and run the **cURL — Calling API** request in your terminal. To use a Server SDK, +select a Python or TypeScript sample under **REST** or **WebSocket (Relay)**. @@ -100,6 +112,8 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ ```python +# Install: python -m pip install signalwire-sdk +# Save as outbound_call.py and run: python outbound_call.py from signalwire.rest import RestClient client = RestClient( @@ -125,6 +139,9 @@ print(call["id"]) ```typescript +// Install: npm install @signalwire/sdk +// This sample also runs as JavaScript: save as outbound-call.mjs, +// then run: node outbound-call.mjs import { RestClient } from "@signalwire/sdk"; const client = new RestClient({ @@ -150,7 +167,8 @@ console.log(call.id); -The REST API returns a call `id` and status `queued`, confirming that it accepted the request: +The REST API returns a call `id` and status `queued`. This confirms that SignalWire accepted the +request; the call hasn't necessarily rung or been answered yet. Save the `id` to identify this call. ```json { @@ -172,6 +190,8 @@ answer. It waits for playback to finish, then hangs up and closes the connection ```python +# Install: python -m pip install signalwire-sdk +# Save as outbound_call.py and run: python outbound_call.py import asyncio from signalwire.relay import RelayClient @@ -206,6 +226,9 @@ asyncio.run(main()) ```typescript +// Install: npm install @signalwire/sdk +// This sample also runs as JavaScript: save as outbound-call.mjs, +// then run: node outbound-call.mjs import { RelayClient } from "@signalwire/sdk"; const client = new RelayClient({ @@ -248,22 +271,23 @@ arrival message. The call ends when the announcement finishes. ## Track the call's progress +Use callbacks with REST or event handlers with Relay to follow what happens after you dial. +Select the same approach you used for your first call. + -Once you create a call, SignalWire tracks its status through the lifecycle: `queued`, `created`, -`ringing`, `answered`, and `ended`. To follow that lifecycle from your application, add a -`status_url` to the `dial` request. SignalWire sends a POST to that URL each time the call reaches -a status you asked about. +Add `status_url` and `status_events` to your first request to receive call progress callbacks. +Keep the inline `swml` announcement. The examples below place another call with notifications +for `ringing`, `answered`, and `ended`. -The `dial` response itself gives you the first status. It returns the call `id` with status -`queued`, meaning SignalWire accepted the request and is about to place the call. Everything after -that arrives at your `status_url`. +Before running the request, replace `https://example.com/call-status` with a webhook endpoint +you control that SignalWire can reach. Your endpoint receives HTTP POST requests as the call +reaches the selected states. See the [webhooks guide][webhooks] for endpoint setup and local testing. +For the SDK snippets, reuse the `client` you created in the first example. -Choose which statuses to receive with `status_events`: any of `created`, `ringing`, `answered`, -and `ended`. If you omit it, you receive `ended` only. Most applications ask for `answered`, to -confirm someone picked up, and `ended`, to know the call is over. The `ended` payload includes an -`end_reason`, such as `hangup`, `busy`, or `noAnswer`, so you can tell how it ended. +`status_events` accepts `created`, `ringing`, `answered`, and `ended`; if omitted, it defaults +to `ended`. The `ended` payload includes an `end_reason` so you can tell how the call finished. @@ -276,7 +300,14 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ "params": { "from": "+15551234567", "to": "+15557654321", - "url": "https://example.com/swml", + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { "play": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } + ] + } + }, "status_url": "https://example.com/call-status", "status_events": ["ringing", "answered", "ended"] } @@ -288,7 +319,14 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ call = client.calling.dial( from_="+15551234567", to="+15557654321", - url="https://example.com/swml", + swml={ + "version": "1.0.0", + "sections": { + "main": [ + {"play": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} + ] + }, + }, status_url="https://example.com/call-status", status_events=["ringing", "answered", "ended"], ) @@ -299,7 +337,14 @@ call = client.calling.dial( const call = await client.calling.dial({ from: "+15551234567", to: "+15557654321", - url: "https://example.com/swml", + swml: { + version: "1.0.0", + sections: { + main: [ + { play: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }, + ], + }, + }, status_url: "https://example.com/call-status", status_events: ["ringing", "answered", "ended"], }); @@ -339,47 +384,109 @@ sequenceDiagram -Relay keeps one WebSocket open between your application and SignalWire. Your application sends -commands over it, such as `calling.dial`, `calling.play`, and `calling.end`. SignalWire acknowledges -each command at once, then sends events back over the same connection as the call progresses. - -Some events are the direct result of a command: `calling.dial` produces the `created` state, -`calling.play` produces `playing`, and `calling.end` produces `ending`. Others report what happened -on the call: `ringing` and `answered` as the destination responds, `finished` when playback -completes, and `ended` with an `end_reason` once the call is over. +The Relay SDK lets you follow the call in your code: `dial()` returns when the destination +answers, and `action.wait()` waits for the announcement to finish. Register a handler with +`call.on` to observe later state changes, including when the call ends. -The SDK turns this exchange into ordinary calls. `dial()` returns the call once the -`calling.call.dial` event reports `answered`, and `action.wait()` returns when the play event -reports `finished`. Use `call.on` to handle any event yourself, as described in the -[Python](/docs/server-sdks/reference/python/relay/events) or -[TypeScript](/docs/server-sdks/reference/typescript/relay/events) event reference, or the -equivalent for your language. - -Because `dial()` returns once the call is answered, handlers you attach to the returned call see -the states that follow, such as `ending` and `ended`: +Replace your first Relay script with the version below. It registers a state handler immediately +after `dial()` returns, before playback starts, and keeps the same announcement and connection setup. ```python +# Install: python -m pip install signalwire-sdk +# Save as outbound_call.py and run: python outbound_call.py +import asyncio +from signalwire.relay import RelayClient from signalwire.relay.event import CallStateEvent -def handle_state(event: CallStateEvent): - print(f"State: {event.call_state}, reason: {event.end_reason}") +client = RelayClient( + project="YOUR_PROJECT_ID", + token="YOUR_API_TOKEN", + host="YOUR_SPACE.signalwire.com", + contexts=["default"], +) + +async def main(): + async with client: + call = await client.dial( + devices=[[{ + "type": "phone", + "params": { + "from_number": "+15551234567", + "to_number": "+15557654321", + "timeout": 30, + }, + }]], + ) + def handle_state(event: CallStateEvent): + print(f"State: {event.call_state}, reason: {event.end_reason}") + + call.on("calling.call.state", handle_state) + action = await call.play([{ + "type": "tts", + "params": {"text": "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}, + }]) + await action.wait() + await call.hangup() -call.on("calling.call.state", handle_state) +asyncio.run(main()) ``` ```typescript -import { CallStateEvent } from "@signalwire/sdk"; +// Install: npm install @signalwire/sdk +// This sample also runs as JavaScript: save as outbound-call.mjs, +// then run: node outbound-call.mjs +import { RelayClient } from "@signalwire/sdk"; -call.on("calling.call.state", (event: CallStateEvent) => { +const client = new RelayClient({ + project: "YOUR_PROJECT_ID", + token: "YOUR_API_TOKEN", + host: "YOUR_SPACE.signalwire.com", + contexts: ["default"], +}); + +await client.connect(); + +const call = await client.dial([[{ + type: "phone", + params: { + from_number: "+15551234567", + to_number: "+15557654321", + timeout: 30, + }, +}]]); +call.on("calling.call.state", (event) => { console.log(`State: ${event.callState}, reason: ${event.endReason}`); }); +const action = await call.play([ + { type: "tts", text: "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }, +]); +await action.wait(); +await call.hangup(); + +await client.disconnect(); ``` +Because you attach the handler after `dial()` returns, it observes later states such as `ending` +and `ended`. For more event handlers, see the +[Python events reference](/docs/server-sdks/reference/python/relay/events) or +[TypeScript events reference](/docs/server-sdks/reference/typescript/relay/events). + + + +Relay uses one persistent WebSocket connection for commands and events. SignalWire acknowledges +each command, then sends events as the call or action progresses. The SDK uses those events to +resolve the operations you await: + +| Operation in your code | Event that completes it | +|---|---| +| `dial()` | `calling.call.dial` reports `answered` | +| `action.wait()` for playback | `calling.call.play` reports `finished` | + This flow follows your first call: dial, play the announcement, hang up. @@ -416,15 +523,26 @@ sequenceDiagram + + -## Examples +## Build on your first call + +Choose what to add next: let a user speak from your web app, or have an AI agent handle the +conversation when the destination answers. ### Place a call with the Browser SDK -Let someone place and speak on a call from your web app. Supply a -[Subscriber Access Token][browser-auth] as `SAT`, with permission to call phone numbers. +Let someone place and speak on a call from your web app using the [Browser SDK][browser-sdk]. +Before running the snippet: + +- Install `@signalwire/js` in your web application. +- Use your Project ID and API token on your backend to create a + [Subscriber Access Token (SAT)][browser-auth] with permission to call phone numbers. +- Supply that subscriber token to the browser code as `SAT`. +- Replace `+15557654321` with a phone number you can answer. Use an HTTPS page with `` and ``. Run the dialing code from a user action, such as a @@ -444,13 +562,15 @@ call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); hangupButton.onclick = () => call.hangup(); ``` -### Run an AI agent +Allow microphone access when prompted, then answer the destination phone to speak with the +browser user. The [Browser SDK outbound calls guide](/docs/browser-sdk/v4/guides/outbound-calls) +includes a complete page with media handling and call controls. -Replace the announcement in the [server example](#place-a-call-from-a-server) with an AI agent: +### Run an AI agent -- **SWML:** Host the document below and pass its `url` in place of the inline `swml`. -- **Relay:** Replace the playback and hangup steps with the Relay snippet below. - Here, `call` is the answered call returned by `dial`. +Replace the announcement in the [server example](#place-a-call-from-a-server) with an AI agent +that tells the rider when their taxi will arrive and answers follow-up questions. Keep the same +credentials, caller ID, and destination number. An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). @@ -460,24 +580,44 @@ the compliance section of [AI best practices][ai-best-practices] cover each obli technical guidance, not legal advice. - - -```yaml -version: 1.0.0 -sections: - main: - - ai: - params: - static_greeting: "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice." - static_greeting_no_barge: true - prompt: - text: | - You are calling to tell the rider their driver is about five minutes away. - Answer questions using that estimate. You cannot change bookings or - contact preferences. If asked, explain that the rider needs to contact - Bayview Taxi to make those changes. Do not claim to have made a change. + + + +Replace the entire inline `swml` value in your first request with the document below, then place +another call. For the Python REST client, use `True` in place of JSON's `true`. + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { + "ai": { + "params": { + "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + "static_greeting_no_barge": true + }, + "prompt": { + "text": "You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change." + } + } + } + ] + } +} ``` - + +To reuse a hosted SWML document, you can instead serve it from a URL reachable by SignalWire +and pass that URL as `url` in place of `swml`. The +[Calling API reference](/docs/apis/rest/calls/call-commands) describes both forms. + + + + +Replace the playback and hangup steps in your first Relay example with the snippet below. +Here, `call` is the answered call returned by `dial`. Keep the connection open while the agent runs. + + ```python # After dialing, start the agent on the answered call. @@ -518,6 +658,12 @@ await call.hangup(); + + + +Answer the call to hear the automated greeting, then ask when your driver will arrive. +The agent should respond using the five-minute estimate. + `static_greeting_no_barge` lets the greeting finish before the conversation begins. The SWML [`ai` method][swml-ai] reference covers every parameter, as does the Relay `ai` reference for [Python](/docs/server-sdks/reference/python/relay/call/ai), From 12a0a44a95db5e2e60c7987c54b11171105575b2 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 9 Sep 2026 12:55:28 -0400 Subject: [PATCH 048/103] docs: simplify outbound Relay lifecycle diagram --- .../outbound-call-relay-lifecycle-themed.svg | 90 ++++++++----------- .../pages/calling/voice/outbound-calling.mdx | 18 +--- 2 files changed, 41 insertions(+), 67 deletions(-) diff --git a/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg b/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg index f7a55a2fff..c28432b8a4 100644 --- a/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg +++ b/fern/assets/images/img/outbound-call-relay-lifecycle-themed.svg @@ -1,6 +1,6 @@ - + Relay: commands and events over one WebSocket - Your code and SignalWire share one persistent WebSocket. Dial: your code sends calling.dial, which SignalWire acknowledges at once; as the call progresses SignalWire sends calling.call.state events created, ringing, and answered, then calling.call.dial answered, which makes dial() return the call. Play: your code sends calling.play; SignalWire sends calling.call.play playing, then finished, which makes action.wait() return. Hang up: your code sends calling.end; SignalWire sends calling.call.state ending, then ended with end_reason hangup. + Your code and SignalWire share one persistent WebSocket. Your code sends calling.dial; SignalWire reports created, ringing, and answered through calling.call.state. Your code sends calling.play; SignalWire reports playing and finished through calling.call.play. Your code sends calling.end; SignalWire reports ending and ended through calling.call.state. - - - - - - Your code - - - - - SignalWire - - one persistent WebSocket, both directions - Dial - - dial - calling.dial - - acknowledged at once - created → ringing → answered - calling.call.state - - as the call progresses - answered - calling.call.dial - - dial() returns the call - Play the announcement - - play - calling.play - - playing → finished - calling.call.play - - action.wait() returns - Hang up - - hangup - calling.end - - ending → ended - calling.call.state - - end_reason: hangup - - Command you send - - Event from SignalWire + + + + + + Your code + + + + + SignalWire + + one persistent WebSocket, both directions + dial + calling.dial + + created → ringing → answered + calling.call.state + + play + calling.play + + playing → finished + calling.call.play + + hangup + calling.end + + ending → ended + calling.call.state + + + Command you send + + Event from SignalWire diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 0ac99864d8..79b9617eb5 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -491,7 +491,7 @@ This flow follows your first call: dial, play the announcement, hang up. -Your code and SignalWire share one persistent WebSocket. Dial: your code sends calling.dial, which SignalWire acknowledges at once; as the call progresses SignalWire sends calling.call.state events created, ringing, and answered, then calling.call.dial answered, which makes dial() return the call. Play: your code sends calling.play; SignalWire sends calling.call.play playing, then finished, which makes action.wait() return. Hang up: your code sends calling.end; SignalWire sends calling.call.state ending, then ended with end_reason hangup. +Your code and SignalWire share one persistent WebSocket. Your code sends calling.dial; SignalWire reports created, ringing, and answered through calling.call.state. Your code sends calling.play; SignalWire reports playing and finished through calling.call.play. Your code sends calling.end; SignalWire reports ending and ended through calling.call.state. @@ -503,22 +503,12 @@ sequenceDiagram participant SW as SignalWire Note over App,SW: One persistent WebSocket, both directions - rect rgba(0,0,0,0.04) - Note over App,SW: Dial - App->>SW: calling.dial (acknowledged at once) + App->>SW: calling.dial SW-->>App: calling.call.state: created, ringing, answered - SW-->>App: calling.call.dial: answered (dial returns the call) - end - rect rgba(0,0,0,0.04) - Note over App,SW: Play the announcement App->>SW: calling.play - SW-->>App: calling.call.play: playing, then finished (action.wait returns) - end - rect rgba(0,0,0,0.04) - Note over App,SW: Hang up + SW-->>App: calling.call.play: playing, then finished App->>SW: calling.end - SW-->>App: calling.call.state: ending, then ended (end_reason hangup) - end + SW-->>App: calling.call.state: ending, then ended ``` From 609a4149b07e095150e94ce60d9fe75a3e5bf74d Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 9 Sep 2026 13:11:34 -0400 Subject: [PATCH 049/103] docs: verify and complete outbound AI calling examples --- .../pages/calling/voice/outbound-calling.mdx | 259 ++++++++++++++---- 1 file changed, 202 insertions(+), 57 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 79b9617eb5..f1e3eb6780 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -570,89 +570,234 @@ the compliance section of [AI best practices][ai-best-practices] cover each obli technical guidance, not legal advice. - - +The examples below use Python `signalwire-sdk` 3.4.1 and TypeScript `@signalwire/sdk` 2.0.5. +Replace the credentials and phone numbers as you did for your first call, then choose REST or +WebSocket (Relay). -Replace the entire inline `swml` value in your first request with the document below, then place -another call. For the Python REST client, use `True` in place of JSON's `true`. +#### REST -```json -{ - "version": "1.0.0", - "sections": { - "main": [ - { - "ai": { - "params": { - "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", - "static_greeting_no_barge": true - }, - "prompt": { - "text": "You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change." - } +Send a `dial` request with an inline SWML `ai` instruction. SignalWire calls the destination +and starts the agent when the call is answered. Run one of these complete examples: + + + +```bash +curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ + -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "+15551234567", + "to": "+15557654321", + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { + "ai": { + "params": { + "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + "static_greeting_no_barge": true + }, + "prompt": { + "text": "You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change." + } + } + } + ] } } - ] - } -} + } + }' ``` + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as outbound_call.py and run: python outbound_call.py +from signalwire.rest import RestClient -To reuse a hosted SWML document, you can instead serve it from a URL reachable by SignalWire -and pass that URL as `url` in place of `swml`. The -[Calling API reference](/docs/apis/rest/calls/call-commands) describes both forms. +client = RestClient( + project="YOUR_PROJECT_ID", + token="YOUR_API_TOKEN", + host="YOUR_SPACE.signalwire.com", +) - - +call = client.calling.dial( + from_="+15551234567", + to="+15557654321", + swml={ + "version": "1.0.0", + "sections": { + "main": [ + { + "ai": { + "params": { + "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + "static_greeting_no_barge": True + }, + "prompt": { + "text": "You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change." + } + } + } + ] + } + }, +) +print(call["id"]) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as outbound-call.mjs, +// then run: node outbound-call.mjs +import { RestClient } from "@signalwire/sdk"; -Replace the playback and hangup steps in your first Relay example with the snippet below. -Here, `call` is the answered call returned by `dial`. Keep the connection open while the agent runs. +const client = new RestClient({ + project: "YOUR_PROJECT_ID", + token: "YOUR_API_TOKEN", + host: "YOUR_SPACE.signalwire.com", +}); + +const call = await client.calling.dial({ + from: "+15551234567", + to: "+15557654321", + swml: { + "version": "1.0.0", + "sections": { + "main": [ + { + "ai": { + "params": { + "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + "static_greeting_no_barge": true + }, + "prompt": { + "text": "You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change." + } + } + } + ] + } + }, +}); +console.log(call.id); +``` + + + +The response returns a call `id`; the conversation runs on SignalWire. To reuse a hosted SWML +document, pass its URL as `url` in place of `swml`, as described in the +[Calling API reference](/docs/apis/rest/calls/call-commands). + +#### WebSocket (Relay) + +Connect a Relay client, dial the destination, then start the agent with `call.ai()` on the +answered call. Python uses `ai_params`; TypeScript uses `aiParams` for the same AI settings. +These scripts keep the WebSocket open until the destination hangs up, then disconnect. ```python -# After dialing, start the agent on the answered call. -action = await call.ai( - ai_params={ - "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", - "static_greeting_no_barge": True, - }, - prompt={ - "text": """You are calling to tell the rider their driver is about five minutes away. +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as outbound_call.py and run: python outbound_call.py +import asyncio +from signalwire.relay import RelayClient + +client = RelayClient( + project="YOUR_PROJECT_ID", + token="YOUR_API_TOKEN", + host="YOUR_SPACE.signalwire.com", + contexts=["default"], +) + +async def main(): + async with client: + call = await client.dial( + devices=[[{ + "type": "phone", + "params": { + "from_number": "+15551234567", + "to_number": "+15557654321", + "timeout": 30, + }, + }]], + ) + await call.ai( + ai_params={ + "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + "static_greeting_no_barge": True, + }, + prompt={ + "text": """You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change.""" - }, -) -await action.wait() -await call.hangup() + }, + ) + # Keep the connection open until the destination hangs up. + await call.wait_for_ended() + +asyncio.run(main()) ``` ```typescript -// After dialing, start the agent on the answered call. -const action = await call.ai({ - aiParams: { - static_greeting: "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", - static_greeting_no_barge: true, - }, - prompt: { - text: `You are calling to tell the rider their driver is about five minutes away. -Answer questions using that estimate. You cannot change bookings or -contact preferences. If asked, explain that the rider needs to contact -Bayview Taxi to make those changes. Do not claim to have made a change.`, - }, +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as outbound-call.mjs, +// then run: node outbound-call.mjs +import { RelayClient } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "YOUR_PROJECT_ID", + token: "YOUR_API_TOKEN", + host: "YOUR_SPACE.signalwire.com", + contexts: ["default"], }); -await action.wait(); -await call.hangup(); + +await client.connect(); + +try { + const call = await client.dial([[{ + type: "phone", + params: { + from_number: "+15551234567", + to_number: "+15557654321", + timeout: 30, + }, + }]]); + await call.ai({ + aiParams: { + static_greeting: "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + static_greeting_no_barge: true, + }, + prompt: { + text: `You are calling to tell the rider their driver is about five minutes away. + Answer questions using that estimate. You cannot change bookings or + contact preferences. If asked, explain that the rider needs to contact + Bayview Taxi to make those changes. Do not claim to have made a change.`, + }, + }); + // Keep the connection open until the destination hangs up. + await call.waitForEnded(); +} finally { + await client.disconnect(); +} ``` - - +Use [Python `call.wait_for_ended()`](/docs/server-sdks/reference/python/relay/call/wait-for-ended) +or [TypeScript `call.waitForEnded()`](/docs/server-sdks/reference/typescript/relay/call/wait-for-ended) +to wait for the call to end. The AI action's `wait()` method waits for an AI action completion +event, which is separate from the call-ended event. Answer the call to hear the automated greeting, then ask when your driver will arrive. -The agent should respond using the five-minute estimate. +The agent should respond using the five-minute estimate. Hang up the destination phone when +you finish; the Relay script then exits. `static_greeting_no_barge` lets the greeting finish before the conversation begins. The SWML [`ai` method][swml-ai] reference covers every parameter, as does the Relay `ai` reference for From d71500b71c8cc5f6a5f84e89114f7d1a7e3c4966 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 9 Sep 2026 13:23:10 -0400 Subject: [PATCH 050/103] docs: validate and fix outbound calling code examples --- .../pages/calling/voice/outbound-calling.mdx | 189 +++++++++++------- 1 file changed, 117 insertions(+), 72 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index f1e3eb6780..6aed97fdf0 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -48,7 +48,8 @@ For the server walkthrough, have these values ready: Use cURL in your terminal to make the first REST request. If you choose Python or TypeScript, follow the setup comments in that sample; the [Server SDK installation guide][sdk-install] -covers runtime requirements and environment setup. +covers runtime requirements and environment setup. The server examples use Python +`signalwire-sdk` 3.4.1 and TypeScript `@signalwire/sdk` 2.0.5. A [trial project][trial-mode] can dial only numbers it has purchased or verified, and cannot call @@ -102,7 +103,7 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ "version": "1.0.0", "sections": { "main": [ - { "play": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } + { "play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} } ] } } @@ -112,7 +113,7 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ ```python -# Install: python -m pip install signalwire-sdk +# Install: python -m pip install signalwire-sdk==3.4.1 # Save as outbound_call.py and run: python outbound_call.py from signalwire.rest import RestClient @@ -129,7 +130,7 @@ call = client.calling.dial( "version": "1.0.0", "sections": { "main": [ - {"play": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} + {"play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}} ] }, }, @@ -139,7 +140,7 @@ print(call["id"]) ```typescript -// Install: npm install @signalwire/sdk +// Install: npm install @signalwire/sdk@2.0.5 // This sample also runs as JavaScript: save as outbound-call.mjs, // then run: node outbound-call.mjs import { RestClient } from "@signalwire/sdk"; @@ -157,7 +158,7 @@ const call = await client.calling.dial({ version: "1.0.0", sections: { main: [ - { play: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }, + { play: { url: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } }, ], }, }, @@ -169,6 +170,7 @@ console.log(call.id); The REST API returns a call `id` and status `queued`. This confirms that SignalWire accepted the request; the call hasn't necessarily rung or been answered yet. Save the `id` to identify this call. +This response excerpt shows the fields used in the walkthrough. ```json { @@ -185,12 +187,13 @@ request; the call hasn't necessarily rung or been answered yet. Save the `id` to The SDK opens a WebSocket connection, dials the destination, and plays the announcement after -answer. It waits for playback to finish, then hangs up and closes the connection. +answer. A playback completion callback hangs up the call. The script keeps the connection open +until the call ends, including when the destination hangs up during the announcement. ```python -# Install: python -m pip install signalwire-sdk +# Install: python -m pip install signalwire-sdk==3.4.1 # Save as outbound_call.py and run: python outbound_call.py import asyncio from signalwire.relay import RelayClient @@ -214,19 +217,22 @@ async def main(): }, }]], ) - action = await call.play([{ + async def hang_up_after_playback(_event): + if call.state != "ended": + await call.hangup() + + await call.play([{ "type": "tts", "params": {"text": "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}, - }]) - await action.wait() - await call.hangup() + }], on_completed=hang_up_after_playback) + await call.wait_for_ended() asyncio.run(main()) ``` ```typescript -// Install: npm install @signalwire/sdk +// Install: npm install @signalwire/sdk@2.0.5 // This sample also runs as JavaScript: save as outbound-call.mjs, // then run: node outbound-call.mjs import { RelayClient } from "@signalwire/sdk"; @@ -240,21 +246,26 @@ const client = new RelayClient({ await client.connect(); -const call = await client.dial([[{ - type: "phone", - params: { - from_number: "+15551234567", - to_number: "+15557654321", - timeout: 30, - }, -}]]); -const action = await call.play([ - { type: "tts", text: "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }, -]); -await action.wait(); -await call.hangup(); - -await client.disconnect(); +try { + const call = await client.dial([[{ + type: "phone", + params: { + from_number: "+15551234567", + to_number: "+15557654321", + timeout: 30, + }, + }]]); + await call.play([ + { type: "tts", text: "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }, + ], { + onCompleted: async () => { + if (call.state !== "ended") await call.hangup(); + }, + }); + await call.waitForEnded(); +} finally { + await client.disconnect(); +} ``` @@ -284,7 +295,8 @@ for `ringing`, `answered`, and `ended`. Before running the request, replace `https://example.com/call-status` with a webhook endpoint you control that SignalWire can reach. Your endpoint receives HTTP POST requests as the call reaches the selected states. See the [webhooks guide][webhooks] for endpoint setup and local testing. -For the SDK snippets, reuse the `client` you created in the first example. +For the SDK snippets, keep the imports and `client` setup from your first REST example, and +replace its `dial` call with the code below. `status_events` accepts `created`, `ringing`, `answered`, and `ended`; if omitted, it defaults to `ended`. The `ended` payload includes an `end_reason` so you can tell how the call finished. @@ -304,7 +316,7 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ "version": "1.0.0", "sections": { "main": [ - { "play": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } + { "play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} } ] } }, @@ -323,7 +335,7 @@ call = client.calling.dial( "version": "1.0.0", "sections": { "main": [ - {"play": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} + {"play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}} ] }, }, @@ -341,7 +353,7 @@ const call = await client.calling.dial({ version: "1.0.0", sections: { main: [ - { play: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }, + { play: { url: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } }, ], }, }, @@ -385,8 +397,9 @@ sequenceDiagram The Relay SDK lets you follow the call in your code: `dial()` returns when the destination -answers, and `action.wait()` waits for the announcement to finish. Register a handler with -`call.on` to observe later state changes, including when the call ends. +answers, and a playback completion callback ends the call after the announcement. Register a +handler with `call.on` to observe later state changes, then wait for the call-ended event before +closing the connection. Replace your first Relay script with the version below. It registers a state handler immediately after `dial()` returns, before playback starts, and keeps the same announcement and connection setup. @@ -394,7 +407,7 @@ after `dial()` returns, before playback starts, and keeps the same announcement ```python -# Install: python -m pip install signalwire-sdk +# Install: python -m pip install signalwire-sdk==3.4.1 # Save as outbound_call.py and run: python outbound_call.py import asyncio from signalwire.relay import RelayClient @@ -423,19 +436,22 @@ async def main(): print(f"State: {event.call_state}, reason: {event.end_reason}") call.on("calling.call.state", handle_state) - action = await call.play([{ + async def hang_up_after_playback(_event): + if call.state != "ended": + await call.hangup() + + await call.play([{ "type": "tts", "params": {"text": "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}, - }]) - await action.wait() - await call.hangup() + }], on_completed=hang_up_after_playback) + await call.wait_for_ended() asyncio.run(main()) ``` ```typescript -// Install: npm install @signalwire/sdk +// Install: npm install @signalwire/sdk@2.0.5 // This sample also runs as JavaScript: save as outbound-call.mjs, // then run: node outbound-call.mjs import { RelayClient } from "@signalwire/sdk"; @@ -449,24 +465,29 @@ const client = new RelayClient({ await client.connect(); -const call = await client.dial([[{ - type: "phone", - params: { - from_number: "+15551234567", - to_number: "+15557654321", - timeout: 30, - }, -}]]); -call.on("calling.call.state", (event) => { - console.log(`State: ${event.callState}, reason: ${event.endReason}`); -}); -const action = await call.play([ - { type: "tts", text: "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }, -]); -await action.wait(); -await call.hangup(); - -await client.disconnect(); +try { + const call = await client.dial([[{ + type: "phone", + params: { + from_number: "+15551234567", + to_number: "+15557654321", + timeout: 30, + }, + }]]); + call.on("calling.call.state", (event) => { + console.log(`State: ${event.params.call_state}, reason: ${event.params.end_reason ?? ""}`); + }); + await call.play([ + { type: "tts", text: "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }, + ], { + onCompleted: async () => { + if (call.state !== "ended") await call.hangup(); + }, + }); + await call.waitForEnded(); +} finally { + await client.disconnect(); +} ``` @@ -480,12 +501,13 @@ and `ended`. For more event handlers, see the Relay uses one persistent WebSocket connection for commands and events. SignalWire acknowledges each command, then sends events as the call or action progresses. The SDK uses those events to -resolve the operations you await: +trigger callbacks and complete the waits in your script: | Operation in your code | Event that completes it | |---|---| | `dial()` | `calling.call.dial` reports `answered` | -| `action.wait()` for playback | `calling.call.play` reports `finished` | +| Playback completion callback | `calling.call.play` reports `finished` or `error` | +| `call.wait_for_ended()` / `call.waitForEnded()` | `calling.call.state` reports `ended` | This flow follows your first call: dial, play the announcement, hang up. @@ -528,28 +550,52 @@ conversation when the destination answers. Let someone place and speak on a call from your web app using the [Browser SDK][browser-sdk]. Before running the snippet: -- Install `@signalwire/js` in your web application. +- Install the Browser SDK and its observable dependency: `npm install @signalwire/js@4.0.0-rc.2 rxjs@7.8.2`. - Use your Project ID and API token on your backend to create a [Subscriber Access Token (SAT)][browser-auth] with permission to call phone numbers. -- Supply that subscriber token to the browser code as `SAT`. +- Replace `YOUR_SUBSCRIBER_ACCESS_TOKEN` below with the token issued by your backend. - Replace `+15557654321` with a phone number you can answer. -Use an HTTPS page with `` and -``. Run the dialing code from a user action, such as a -**Call** button. +Serve the page over HTTPS or use `localhost` for development. Add these elements before your +application script, and let the user select **Call** to start dialing: + +```html + + + +``` ```typescript import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; -// SAT is a Subscriber Access Token your backend issued for this user. -const client = new SignalWire(new StaticCredentialProvider({ token: SAT })); +const client = new SignalWire(new StaticCredentialProvider({ + token: "YOUR_SUBSCRIBER_ACCESS_TOKEN", +})); const remoteAudio = document.querySelector("#remote-audio")!; +const callButton = document.querySelector("#call")!; const hangupButton = document.querySelector("#hangup")!; -const call = await client.dial("+15557654321"); -call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); - -hangupButton.onclick = () => call.hangup(); +callButton.onclick = async () => { + callButton.disabled = true; + try { + const call = await client.dial("+15557654321", { audio: true, video: false }); + call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); + hangupButton.disabled = false; + hangupButton.onclick = () => { + void call.hangup().catch(console.error); + }; + call.status$.subscribe((status) => { + if (status === "disconnected" || status === "destroyed") { + remoteAudio.srcObject = null; + hangupButton.disabled = true; + callButton.disabled = false; + } + }); + } catch (error) { + callButton.disabled = false; + console.error(error); + } +}; ``` Allow microphone access when prompted, then answer the destination phone to speak with the @@ -570,7 +616,6 @@ the compliance section of [AI best practices][ai-best-practices] cover each obli technical guidance, not legal advice. -The examples below use Python `signalwire-sdk` 3.4.1 and TypeScript `@signalwire/sdk` 2.0.5. Replace the credentials and phone numbers as you did for your first call, then choose REST or WebSocket (Relay). From 313e6d3158cbad7f29baa57030a3fe75e741a048 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 9 Sep 2026 14:33:18 -0400 Subject: [PATCH 051/103] docs: remove Compatibility API mention from outbound calling guide --- fern/products/platform/pages/calling/voice/outbound-calling.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 6aed97fdf0..06f101b048 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -16,7 +16,6 @@ max-toc-depth: 3 [tcpa]: /docs/platform/compliance/tcpa [sdk-install]: /docs/server-sdks/guides/installation [browser-sdk]: /docs/browser-sdk/v4/guides/overview -[compatibility-api]: /docs/compatibility-api/rest [webhooks]: /docs/platform/webhooks Place an outbound phone call with SignalWire and choose what happens when the destination answers. @@ -33,7 +32,6 @@ an existing application, choose the approach that fits how you want to control t | Have your server start a call with instructions to run when it's answered | [REST Calling API](#place-a-call-from-a-server), using cURL or a Server SDK | | Send commands and receive call events over a persistent connection | [WebSocket (Relay)](#place-a-call-from-a-server), using a Server SDK | | Let someone place and speak on a call from your web app | [Browser SDK](#place-a-call-with-the-browser-sdk) | -| Add outbound calls to an existing Compatibility API application | [Compatibility API reference][compatibility-api] | ## Prepare for your first call From 13157027035a94f57036f5db7d31033ac5400ce4 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 9 Sep 2026 14:44:43 -0400 Subject: [PATCH 052/103] docs: surface Relay lifecycle diagram outside accordion --- .../pages/calling/voice/outbound-calling.mdx | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 06f101b048..092d5fc97c 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -495,19 +495,8 @@ and `ended`. For more event handlers, see the [Python events reference](/docs/server-sdks/reference/python/relay/events) or [TypeScript events reference](/docs/server-sdks/reference/typescript/relay/events). - - -Relay uses one persistent WebSocket connection for commands and events. SignalWire acknowledges -each command, then sends events as the call or action progresses. The SDK uses those events to -trigger callbacks and complete the waits in your script: - -| Operation in your code | Event that completes it | -|---|---| -| `dial()` | `calling.call.dial` reports `answered` | -| Playback completion callback | `calling.call.play` reports `finished` or `error` | -| `call.wait_for_ended()` / `call.waitForEnded()` | `calling.call.state` reports `ended` | - -This flow follows your first call: dial, play the announcement, hang up. +This flow shows the commands your code sends and the events SignalWire returns over the same +persistent connection: @@ -533,8 +522,6 @@ sequenceDiagram - - From b671592709d5bfc8fce67a4186011e328a776fbf Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 9 Sep 2026 14:47:57 -0400 Subject: [PATCH 053/103] docs: fold browser example into the place-the-call step --- .../pages/calling/voice/outbound-calling.mdx | 152 +++++++++--------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 092d5fc97c..545f0eef54 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -29,9 +29,9 @@ an existing application, choose the approach that fits how you want to control t | What you want to do | Where to start | |---|---| -| Have your server start a call with instructions to run when it's answered | [REST Calling API](#place-a-call-from-a-server), using cURL or a Server SDK | -| Send commands and receive call events over a persistent connection | [WebSocket (Relay)](#place-a-call-from-a-server), using a Server SDK | -| Let someone place and speak on a call from your web app | [Browser SDK](#place-a-call-with-the-browser-sdk) | +| Have your server start a call with instructions to run when it's answered | [REST Calling API](#place-the-call), using cURL or a Server SDK | +| Send commands and receive call events over a persistent connection | [WebSocket (Relay)](#place-the-call), using a Server SDK | +| Let someone place and speak on a call from your web app | [Browser SDK](#place-the-call), over the same WebSocket | ## Prepare for your first call @@ -57,7 +57,7 @@ internationally at all. Any other project can dial any number, but needs ## Make your first call -Call a phone you can answer and play a short announcement from your server. +Call a phone you can answer and play a short announcement. @@ -75,10 +75,12 @@ Replace these values in the code sample you choose: Use E.164 format for both numbers, including `+` and the country code. -### Place a call from a server +### Place the call -Select **REST** and run the **cURL — Calling API** request in your terminal. To use a Server SDK, -select a Python or TypeScript sample under **REST** or **WebSocket (Relay)**. +Every REST request comes from your server. WebSocket (Relay) works the same way from a server, +and also from a web page with the Browser SDK. + +For your first call, select **REST** and run the **cURL — Calling API** request in your terminal. @@ -184,7 +186,9 @@ This response excerpt shows the fields used in the walkthrough. -The SDK opens a WebSocket connection, dials the destination, and plays the announcement after +**From your server** + +A Server SDK opens a WebSocket connection, dials the destination, and plays the announcement after answer. A playback completion callback hangs up the call. The script keeps the connection open until the call ends, including when the destination hangs up during the announcement. @@ -268,6 +272,64 @@ try { +**From a web page** + +The [Browser SDK][browser-sdk] opens the same kind of connection from a browser, so a user can +place and speak on the call. It authenticates with a Subscriber Access Token instead of your API +token. Before running the snippet: + +- Install the Browser SDK and its observable dependency: `npm install @signalwire/js@4.0.0-rc.2 rxjs@7.8.2`. +- Use your Project ID and API token on your backend to create a + [Subscriber Access Token (SAT)][browser-auth] with permission to call phone numbers. +- Replace `YOUR_SUBSCRIBER_ACCESS_TOKEN` below with the token issued by your backend. +- Replace `+15557654321` with a phone number you can answer. + +Serve the page over HTTPS or use `localhost` for development. Add these elements before your +application script, and let the user select **Call** to start dialing: + +```html + + + +``` + +```typescript +import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; + +const client = new SignalWire(new StaticCredentialProvider({ + token: "YOUR_SUBSCRIBER_ACCESS_TOKEN", +})); +const remoteAudio = document.querySelector("#remote-audio")!; +const callButton = document.querySelector("#call")!; +const hangupButton = document.querySelector("#hangup")!; + +callButton.onclick = async () => { + callButton.disabled = true; + try { + const call = await client.dial("+15557654321", { audio: true, video: false }); + call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); + hangupButton.disabled = false; + hangupButton.onclick = () => { + void call.hangup().catch(console.error); + }; + call.status$.subscribe((status) => { + if (status === "disconnected" || status === "destroyed") { + remoteAudio.srcObject = null; + hangupButton.disabled = true; + callButton.disabled = false; + } + }); + } catch (error) { + callButton.disabled = false; + console.error(error); + } +}; +``` + +Allow microphone access when prompted, then answer the destination phone to speak with the +browser user. The [Browser SDK outbound calls guide](/docs/browser-sdk/v4/guides/outbound-calls) +includes a complete page with media handling and call controls. + @@ -525,73 +587,11 @@ sequenceDiagram -## Build on your first call - -Choose what to add next: let a user speak from your web app, or have an AI agent handle the -conversation when the destination answers. - -### Place a call with the Browser SDK - -Let someone place and speak on a call from your web app using the [Browser SDK][browser-sdk]. -Before running the snippet: - -- Install the Browser SDK and its observable dependency: `npm install @signalwire/js@4.0.0-rc.2 rxjs@7.8.2`. -- Use your Project ID and API token on your backend to create a - [Subscriber Access Token (SAT)][browser-auth] with permission to call phone numbers. -- Replace `YOUR_SUBSCRIBER_ACCESS_TOKEN` below with the token issued by your backend. -- Replace `+15557654321` with a phone number you can answer. - -Serve the page over HTTPS or use `localhost` for development. Add these elements before your -application script, and let the user select **Call** to start dialing: - -```html - - - -``` - -```typescript -import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; - -const client = new SignalWire(new StaticCredentialProvider({ - token: "YOUR_SUBSCRIBER_ACCESS_TOKEN", -})); -const remoteAudio = document.querySelector("#remote-audio")!; -const callButton = document.querySelector("#call")!; -const hangupButton = document.querySelector("#hangup")!; - -callButton.onclick = async () => { - callButton.disabled = true; - try { - const call = await client.dial("+15557654321", { audio: true, video: false }); - call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); - hangupButton.disabled = false; - hangupButton.onclick = () => { - void call.hangup().catch(console.error); - }; - call.status$.subscribe((status) => { - if (status === "disconnected" || status === "destroyed") { - remoteAudio.srcObject = null; - hangupButton.disabled = true; - callButton.disabled = false; - } - }); - } catch (error) { - callButton.disabled = false; - console.error(error); - } -}; -``` - -Allow microphone access when prompted, then answer the destination phone to speak with the -browser user. The [Browser SDK outbound calls guide](/docs/browser-sdk/v4/guides/outbound-calls) -includes a complete page with media handling and call controls. - -### Run an AI agent +## Run an AI agent -Replace the announcement in the [server example](#place-a-call-from-a-server) with an AI agent -that tells the rider when their taxi will arrive and answers follow-up questions. Keep the same -credentials, caller ID, and destination number. +Replace the announcement from [your first call](#place-the-call) with an AI agent that tells the +rider when their taxi will arrive and answers follow-up questions. Keep the same credentials, +caller ID, and destination number. An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). @@ -604,7 +604,7 @@ technical guidance, not legal advice. Replace the credentials and phone numbers as you did for your first call, then choose REST or WebSocket (Relay). -#### REST +### REST Send a `dial` request with an inline SWML `ai` instruction. SignalWire calls the destination and starts the agent when the call is answered. Run one of these complete examples: @@ -723,7 +723,7 @@ The response returns a call `id`; the conversation runs on SignalWire. To reuse document, pass its URL as `url` in place of `swml`, as described in the [Calling API reference](/docs/apis/rest/calls/call-commands). -#### WebSocket (Relay) +### WebSocket (Relay) Connect a Relay client, dial the destination, then start the agent with `call.ai()` on the answered call. Python uses `ai_params`; TypeScript uses `aiParams` for the same AI settings. From 2ca617c5fa06a8b764909dcfe18f6af1e626c732 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 09:46:53 -0400 Subject: [PATCH 054/103] docs: consolidate browser calling into code tabs --- .../pages/calling/voice/outbound-calling.mdx | 67 +++++++------------ 1 file changed, 25 insertions(+), 42 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 545f0eef54..276eb0aa00 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -5,7 +5,6 @@ description: Place your first outbound phone call, track its progress, and add A max-toc-depth: 3 --- -[browser-auth]: /docs/browser-sdk/v4/guides/authentication [caller-id]: /docs/platform/voice/how-to-set-caller-id-or-cnam [trial-mode]: /docs/platform/trial-mode [international]: /docs/platform/how-to-enable-international-services @@ -15,7 +14,6 @@ max-toc-depth: 3 [ai-best-practices]: /docs/platform/ai/best-practices [tcpa]: /docs/platform/compliance/tcpa [sdk-install]: /docs/server-sdks/guides/installation -[browser-sdk]: /docs/browser-sdk/v4/guides/overview [webhooks]: /docs/platform/webhooks Place an outbound phone call with SignalWire and choose what happens when the destination answers. @@ -31,7 +29,7 @@ an existing application, choose the approach that fits how you want to control t |---|---| | Have your server start a call with instructions to run when it's answered | [REST Calling API](#place-the-call), using cURL or a Server SDK | | Send commands and receive call events over a persistent connection | [WebSocket (Relay)](#place-the-call), using a Server SDK | -| Let someone place and speak on a call from your web app | [Browser SDK](#place-the-call), over the same WebSocket | +| Let someone place and speak on a call from your web app | [Browser SDK](#place-the-call) | ## Prepare for your first call @@ -77,8 +75,7 @@ Use E.164 format for both numbers, including `+` and the country code. ### Place the call -Every REST request comes from your server. WebSocket (Relay) works the same way from a server, -and also from a web page with the Browser SDK. +REST requests run on your server. For WebSocket calling, choose a Server SDK or the Browser SDK. For your first call, select **REST** and run the **cURL — Calling API** request in your terminal. @@ -186,11 +183,8 @@ This response excerpt shows the fields used in the walkthrough. -**From your server** - -A Server SDK opens a WebSocket connection, dials the destination, and plays the announcement after -answer. A playback completion callback hangs up the call. The script keeps the connection open -until the call ends, including when the destination hangs up during the announcement. +Use a Server SDK to play the announcement, or the Browser SDK to let a user speak on the call +from your web app. @@ -270,38 +264,22 @@ try { } ``` - - -**From a web page** - -The [Browser SDK][browser-sdk] opens the same kind of connection from a browser, so a user can -place and speak on the call. It authenticates with a Subscriber Access Token instead of your API -token. Before running the snippet: - -- Install the Browser SDK and its observable dependency: `npm install @signalwire/js@4.0.0-rc.2 rxjs@7.8.2`. -- Use your Project ID and API token on your backend to create a - [Subscriber Access Token (SAT)][browser-auth] with permission to call phone numbers. -- Replace `YOUR_SUBSCRIBER_ACCESS_TOKEN` below with the token issued by your backend. -- Replace `+15557654321` with a phone number you can answer. - -Serve the page over HTTPS or use `localhost` for development. Add these elements before your -application script, and let the user select **Call** to start dialing: - -```html - - - -``` - -```typescript + +```javascript +// Install: npm install @signalwire/js@4.0.0-rc.2 rxjs@7.8.2 +// Run on HTTPS or localhost with these elements in your page: +// +// +// +// Use a Subscriber Access Token issued by your backend. import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; const client = new SignalWire(new StaticCredentialProvider({ token: "YOUR_SUBSCRIBER_ACCESS_TOKEN", })); -const remoteAudio = document.querySelector("#remote-audio")!; -const callButton = document.querySelector("#call")!; -const hangupButton = document.querySelector("#hangup")!; +const remoteAudio = document.querySelector("#remote-audio"); +const callButton = document.querySelector("#call"); +const hangupButton = document.querySelector("#hangup"); callButton.onclick = async () => { callButton.disabled = true; @@ -325,18 +303,23 @@ callButton.onclick = async () => { } }; ``` + + -Allow microphone access when prompted, then answer the destination phone to speak with the -browser user. The [Browser SDK outbound calls guide](/docs/browser-sdk/v4/guides/outbound-calls) -includes a complete page with media handling and call controls. +For browser setup and a complete web page, see the +[Browser SDK outbound calls guide](/docs/browser-sdk/v4/guides/outbound-calls). +For server dialing options, see the +[Python dial reference](/docs/server-sdks/reference/python/relay/client/dial) or +[TypeScript dial reference](/docs/server-sdks/reference/typescript/relay/client/dial). ### Answer the call -Answer the destination phone. You should hear “Hi, this is Bayview Taxi” followed by the driver -arrival message. The call ends when the announcement finishes. +Answer the destination phone. With a server example, you should hear “Hi, this is Bayview Taxi” +followed by the driver arrival message, then the call ends. With the browser example, allow +microphone access to speak on the call and select **Hang up** when you finish. From 655e78475b4c8e3aeb188fefc3c5cd9adb81e27a Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 09:52:00 -0400 Subject: [PATCH 055/103] docs: remove outbound calling next steps section --- .../pages/calling/voice/outbound-calling.mdx | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 276eb0aa00..d3ec721dbd 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -816,20 +816,3 @@ you finish; the Relay script then exits. [`ai` method][swml-ai] reference covers every parameter, as does the Relay `ai` reference for [Python](/docs/server-sdks/reference/python/relay/call/ai), [TypeScript](/docs/server-sdks/reference/typescript/relay/call/ai), or your language's SDK. - -## Next steps - - - - Every `dial` field, plus the commands that control a call already in progress. - - - Connection management and event-driven call control from your backend. - - - A complete web page with media handling and call controls. - - - Build the agent that holds the conversation once someone answers. - - From e55e1da72a0f38575fcf28cada95688dce16563a Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 09:55:57 -0400 Subject: [PATCH 056/103] docs: use consistent headings for outbound calling surfaces --- .../pages/calling/voice/outbound-calling.mdx | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index d3ec721dbd..6f08a41688 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -77,10 +77,9 @@ Use E.164 format for both numbers, including `+` and the country code. REST requests run on your server. For WebSocket calling, choose a Server SDK or the Browser SDK. -For your first call, select **REST** and run the **cURL — Calling API** request in your terminal. +For your first call, run the **cURL — Calling API** request in the REST section below. - - +#### REST The request includes a SignalWire Markup Language (SWML) document in `swml`. SignalWire runs it when the destination answers, playing the announcement below. @@ -180,8 +179,7 @@ This response excerpt shows the fields used in the walkthrough. } ``` - - +#### WebSocket (Relay) Use a Server SDK to play the announcement, or the Browser SDK to let a user speak on the call from your web app. @@ -312,9 +310,6 @@ For server dialing options, see the [Python dial reference](/docs/server-sdks/reference/python/relay/client/dial) or [TypeScript dial reference](/docs/server-sdks/reference/typescript/relay/client/dial). - - - ### Answer the call Answer the destination phone. With a server example, you should hear “Hi, this is Bayview Taxi” @@ -326,10 +321,9 @@ microphone access to speak on the call and select **Hang up** when you finish. ## Track the call's progress Use callbacks with REST or event handlers with Relay to follow what happens after you dial. -Select the same approach you used for your first call. +Follow the section for the approach you used for your first call. - - +### REST Add `status_url` and `status_events` to your first request to receive call progress callbacks. Keep the inline `swml` announcement. The examples below place another call with notifications @@ -436,8 +430,7 @@ sequenceDiagram - - +### WebSocket (Relay) The Relay SDK lets you follow the call in your code: `dial()` returns when the destination answers, and a playback completion callback ends the call after the announcement. Register a @@ -567,9 +560,6 @@ sequenceDiagram - - - ## Run an AI agent Replace the announcement from [your first call](#place-the-call) with an AI agent that tells the From 01f7857eb8265dd1218eaeaa5f13b636c9be6acd Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 11:01:30 -0400 Subject: [PATCH 057/103] docs: group outbound AI agent under examples --- .../platform/pages/calling/voice/outbound-calling.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 6f08a41688..2009889526 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -560,7 +560,9 @@ sequenceDiagram -## Run an AI agent +## Examples + +### Run an AI agent Replace the announcement from [your first call](#place-the-call) with an AI agent that tells the rider when their taxi will arrive and answers follow-up questions. Keep the same credentials, @@ -577,7 +579,7 @@ technical guidance, not legal advice. Replace the credentials and phone numbers as you did for your first call, then choose REST or WebSocket (Relay). -### REST +#### REST Send a `dial` request with an inline SWML `ai` instruction. SignalWire calls the destination and starts the agent when the call is answered. Run one of these complete examples: @@ -696,7 +698,7 @@ The response returns a call `id`; the conversation runs on SignalWire. To reuse document, pass its URL as `url` in place of `swml`, as described in the [Calling API reference](/docs/apis/rest/calls/call-commands). -### WebSocket (Relay) +#### WebSocket (Relay) Connect a Relay client, dial the destination, then start the agent with `call.ai()` on the answered call. Python uses `ai_params`; TypeScript uses `aiParams` for the same AI settings. From 11d881f400ba25bc844d30a20fcf48c4106b4adf Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 12:01:00 -0400 Subject: [PATCH 058/103] docs: add voicemail, whisper, recording, and streaming examples --- .../pages/calling/voice/outbound-calling.mdx | 923 ++++++++++++++++++ 1 file changed, 923 insertions(+) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 2009889526..85879883f9 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -15,6 +15,13 @@ max-toc-depth: 3 [tcpa]: /docs/platform/compliance/tcpa [sdk-install]: /docs/server-sdks/guides/installation [webhooks]: /docs/platform/webhooks +[swml-detect-machine]: /docs/swml/reference/calling/detect-machine +[swml-connect]: /docs/swml/reference/calling/connect +[swml-record-call]: /docs/swml/reference/calling/record-call +[swml-stop-record-call]: /docs/swml/reference/calling/stop-record-call +[swml-stream]: /docs/swml/reference/calling/stream +[swml-stop-stream]: /docs/swml/reference/calling/stop-stream +[call-whisper]: /docs/swml/guides/call-whisper Place an outbound phone call with SignalWire and choose what happens when the destination answers. Start by calling your own phone and playing a short announcement, then track the call's progress, @@ -808,3 +815,919 @@ you finish; the Relay script then exits. [`ai` method][swml-ai] reference covers every parameter, as does the Relay `ai` reference for [Python](/docs/server-sdks/reference/python/relay/call/ai), [TypeScript](/docs/server-sdks/reference/typescript/relay/call/ai), or your language's SDK. + +### Leave a voicemail + +Not every call reaches a person. Answering machine detection tells you whether a human or a machine +picked up, so the rider hears the arrival time either way — spoken to them live, or left on their +voicemail after the greeting. + +A prerecorded voice is regulated much like an AI voice, so check consent and local calling hours +before you dial. The [TCPA guide][tcpa] covers the obligations. + +#### REST + +`detect_machine` runs before the rest of the document and saves its verdict in the `detect_result` +variable, which is `machine`, `human`, `fax`, `unknown`, or `error`. Setting `detect_message_end` to +`true` holds the document until the greeting ends, so your message records from the beginning +instead of over the greeting. The `switch` then picks what to play. + +The SDK snippets below keep the imports and `client` setup from your first REST example. + + + +```bash +curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ + -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "+15551234567", + "to": "+15557654321", + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { + "detect_machine": { + "detectors": "amd", + "detect_message_end": true, + "timeout": 30 + } + }, + { + "switch": { + "variable": "detect_result", + "case": { + "machine": [ + { "play": {"url": "say:This is Bayview Taxi. Your driver is on the way and will arrive in about five minutes. There is no need to call back."} } + ], + "human": [ + { "play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} } + ] + }, + "default": [ + { "play": {"url": "say:Hi, this is Bayview Taxi. Your driver will arrive in about five minutes."} } + ] + } + }, + { "hangup": {} } + ] + } + } + } + }' +``` + + +```python +voicemail = "say:This is Bayview Taxi. Your driver is on the way and will arrive in about five minutes. There is no need to call back." +live = "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." + +call = client.calling.dial( + from_="+15551234567", + to="+15557654321", + swml={ + "version": "1.0.0", + "sections": { + "main": [ + {"detect_machine": {"detectors": "amd", "detect_message_end": True, "timeout": 30}}, + { + "switch": { + "variable": "detect_result", + "case": { + "machine": [{"play": {"url": voicemail}}], + "human": [{"play": {"url": live}}], + }, + "default": [{"play": {"url": "say:Hi, this is Bayview Taxi. Your driver will arrive in about five minutes."}}], + } + }, + {"hangup": {}}, + ] + }, + }, +) +print(call["id"]) +``` + + +```typescript +const voicemail = "say:This is Bayview Taxi. Your driver is on the way and will arrive in about five minutes. There is no need to call back."; +const live = "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."; + +const call = await client.calling.dial({ + from: "+15551234567", + to: "+15557654321", + swml: { + version: "1.0.0", + sections: { + main: [ + { detect_machine: { detectors: "amd", detect_message_end: true, timeout: 30 } }, + { + switch: { + variable: "detect_result", + case: { + machine: [{ play: { url: voicemail } }], + human: [{ play: { url: live } }], + }, + default: [{ play: { url: "say:Hi, this is Bayview Taxi. Your driver will arrive in about five minutes." } }], + }, + }, + { hangup: {} }, + ], + }, + }, +}); +console.log(call.id); +``` + + + +Add a `status_url` to `detect_machine` to receive every detection event, including whether a beep +was heard. The [`detect_machine` reference][swml-detect-machine] lists the timing parameters that +control how patient the detector is. + +#### WebSocket (Relay) + +`call.detect()` starts the same detector on the answered call and reports its outcome in a +`calling.call.detect` event: `MACHINE`, `HUMAN`, or `UNKNOWN`. A machine sends a further `READY` +event once its greeting and beep have finished, which is the moment to start the message. Register +the handler before you start detection, so no event arrives before you're listening. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as outbound_call.py and run: python outbound_call.py +import asyncio +from signalwire.relay import RelayClient +from signalwire.relay.event import DetectEvent + +VOICEMAIL = "This is Bayview Taxi. Your driver is on the way and will arrive in about five minutes. There is no need to call back." +LIVE = "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." + +client = RelayClient( + project="YOUR_PROJECT_ID", + token="YOUR_API_TOKEN", + host="YOUR_SPACE.signalwire.com", + contexts=["default"], +) + +def outcome(event: DetectEvent) -> str: + return event.detect.get("params", {}).get("event", "") + +async def main(): + async with client: + call = await client.dial( + devices=[[{ + "type": "phone", + "params": { + "from_number": "+15551234567", + "to_number": "+15557654321", + "timeout": 30, + }, + }]], + ) + async def hang_up_after_playback(_event): + if call.state != "ended": + await call.hangup() + + announced = False + + async def announce(event: DetectEvent): + nonlocal announced + if announced or call.state == "ended": + return + result = outcome(event) + if result == "READY": + # The voicemail greeting and its beep have finished. + announced = True + await call.play( + [{"type": "tts", "params": {"text": VOICEMAIL}}], + on_completed=hang_up_after_playback, + ) + elif result in ("HUMAN", "UNKNOWN"): + announced = True + await call.play( + [{"type": "tts", "params": {"text": LIVE}}], + on_completed=hang_up_after_playback, + ) + elif result == "finished": + await call.hangup() + + call.on("calling.call.detect", announce) + await call.detect({"type": "machine", "params": {"detect_message_end": True}}, timeout=30) + await call.wait_for_ended() + +asyncio.run(main()) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// Save as outbound-call.mts and run: npx tsx outbound-call.mts +import { RelayClient, DetectEvent, RelayEvent } from "@signalwire/sdk"; + +const VOICEMAIL = "This is Bayview Taxi. Your driver is on the way and will arrive in about five minutes. There is no need to call back."; +const LIVE = "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."; + +const client = new RelayClient({ + project: "YOUR_PROJECT_ID", + token: "YOUR_API_TOKEN", + host: "YOUR_SPACE.signalwire.com", + contexts: ["default"], +}); + +function outcome(event: RelayEvent): string { + const detect = (event as DetectEvent).detect?.params as { event?: string } | undefined; + return detect?.event ?? ""; +} + +await client.connect(); + +try { + const call = await client.dial([[{ + type: "phone", + params: { + from_number: "+15551234567", + to_number: "+15557654321", + timeout: 30, + }, + }]]); + const hangUpAfterPlayback = async () => { + if (call.state !== "ended") await call.hangup(); + }; + let announced = false; + + call.on("calling.call.detect", async (event) => { + if (announced || call.state === "ended") return; + const result = outcome(event); + if (result === "READY") { + // The voicemail greeting and its beep have finished. + announced = true; + await call.play([{ type: "tts", text: VOICEMAIL }], { onCompleted: hangUpAfterPlayback }); + } else if (result === "HUMAN" || result === "UNKNOWN") { + announced = true; + await call.play([{ type: "tts", text: LIVE }], { onCompleted: hangUpAfterPlayback }); + } else if (result === "finished") { + await call.hangup(); + } + }); + + await call.detect({ type: "machine", params: { detect_message_end: true } }, { timeout: 30 }); + await call.waitForEnded(); +} finally { + await client.disconnect(); +} +``` + + + +Answer the destination phone to hear the live message, or let it ring through to voicemail to hear +the recorded one. + +### Whisper before connecting two people + +Tell the driver who they're about to speak to. A whisper plays to one leg of the call only, so the +other person hears nothing but ringback while it plays. + +This example dials two phones. Keep your first destination as the rider, and add a second number +you can answer as the driver. + +#### REST + +Call the rider, then `connect` to the driver. The `confirm` array runs on the driver's leg as soon +as they answer and finishes before the two legs are bridged, so only the driver hears the pickup +details. `confirm` also takes the URL of a SWML document, which the +[Call whisper guide][call-whisper] walks through. + +The SDK snippets below keep the imports and `client` setup from your first REST example. + + + +```bash +curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ + -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "+15551234567", + "to": "+15557654321", + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { "play": {"url": "say:Bayview Taxi here. Connecting you to your driver now."} }, + { + "connect": { + "from": "+15551234567", + "to": "+15559876543", + "confirm": [ + { "play": {"url": "say:Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now."} } + ], + "confirm_timeout": 20 + } + } + ] + } + } + } + }' +``` + + +```python +whisper = "say:Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now." + +call = client.calling.dial( + from_="+15551234567", + to="+15557654321", + swml={ + "version": "1.0.0", + "sections": { + "main": [ + {"play": {"url": "say:Bayview Taxi here. Connecting you to your driver now."}}, + { + "connect": { + "from": "+15551234567", + "to": "+15559876543", + "confirm": [{"play": {"url": whisper}}], + "confirm_timeout": 20, + } + }, + ] + }, + }, +) +print(call["id"]) +``` + + +```typescript +const whisper = "say:Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now."; + +const call = await client.calling.dial({ + from: "+15551234567", + to: "+15557654321", + swml: { + version: "1.0.0", + sections: { + main: [ + { play: { url: "say:Bayview Taxi here. Connecting you to your driver now." } }, + { + connect: { + from: "+15551234567", + to: "+15559876543", + confirm: [{ play: { url: whisper } }], + confirm_timeout: 20, + }, + }, + ], + }, + }, +}); +console.log(call.id); +``` + + + +`confirm_timeout` bounds how long the confirm script may run, and 20 seconds is ample for a +one-sentence whisper. The [`connect` reference][swml-connect] covers dialing several numbers in turn +or at once, and what to run when the connected call ends. + +#### WebSocket (Relay) + +Relay has no `confirm` parameter, so dial the person who gets the whisper first. These scripts call +the driver, play the pickup details to that leg, then bridge the rider with `connect()`. The rider's +phone rings only after the whisper finishes. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as outbound_call.py and run: python outbound_call.py +import asyncio +from signalwire.relay import RelayClient + +client = RelayClient( + project="YOUR_PROJECT_ID", + token="YOUR_API_TOKEN", + host="YOUR_SPACE.signalwire.com", + contexts=["default"], +) + +async def main(): + async with client: + # Call the driver first: only this leg hears the whisper. + call = await client.dial( + devices=[[{ + "type": "phone", + "params": { + "from_number": "+15551234567", + "to_number": "+15559876543", + "timeout": 30, + }, + }]], + ) + async def connect_the_rider(_event): + if call.state != "ended": + await call.connect([[{ + "type": "phone", + "params": { + "from_number": "+15551234567", + "to_number": "+15557654321", + "timeout": 30, + }, + }]]) + + await call.play( + [{"type": "tts", "params": {"text": "Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now."}}], + on_completed=connect_the_rider, + ) + await call.wait_for_ended() + +asyncio.run(main()) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as outbound-call.mjs, +// then run: node outbound-call.mjs +import { RelayClient } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "YOUR_PROJECT_ID", + token: "YOUR_API_TOKEN", + host: "YOUR_SPACE.signalwire.com", + contexts: ["default"], +}); + +await client.connect(); + +try { + // Call the driver first: only this leg hears the whisper. + const call = await client.dial([[{ + type: "phone", + params: { + from_number: "+15551234567", + to_number: "+15559876543", + timeout: 30, + }, + }]]); + await call.play( + [{ type: "tts", text: "Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now." }], + { + onCompleted: async () => { + if (call.state === "ended") return; + await call.connect([[{ + type: "phone", + params: { + from_number: "+15551234567", + to_number: "+15557654321", + timeout: 30, + }, + }]]); + }, + }, + ); + await call.waitForEnded(); +} finally { + await client.disconnect(); +} +``` + + + +Answer the driver's phone to hear the whisper, then answer the rider's phone: the two legs are +bridged once the whisper finishes. + +### Record the call + +Keep an audio record of what was said, whether that's an automated message or a live conversation +between the rider and a dispatcher. + + +Recording laws vary by country and by state, and some require the consent of every party on the +call. Confirm what applies to the numbers you dial, and announce the recording when consent is +required. This is technical guidance, not legal advice. + + +#### REST + +`record_call` starts a recording in the background and the document continues to the next +instruction. The recording ends with the call, or when you run +[`stop_record_call`][swml-stop-record-call]. SignalWire saves the recording's URL in the +`record_call_url` variable, and POSTs the finished recording's URL, duration, and size to +`status_url`. + +The SDK snippets below keep the imports and `client` setup from your first REST example. + + + +```bash +curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ + -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "+15551234567", + "to": "+15557654321", + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { + "record_call": { + "format": "mp3", + "direction": "both", + "stereo": true, + "beep": true, + "status_url": "https://example.com/recording-status" + } + }, + { "play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} } + ] + } + } + } + }' +``` + + +```python +call = client.calling.dial( + from_="+15551234567", + to="+15557654321", + swml={ + "version": "1.0.0", + "sections": { + "main": [ + { + "record_call": { + "format": "mp3", + "direction": "both", + "stereo": True, + "beep": True, + "status_url": "https://example.com/recording-status", + } + }, + {"play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}}, + ] + }, + }, +) +print(call["id"]) +``` + + +```typescript +const call = await client.calling.dial({ + from: "+15551234567", + to: "+15557654321", + swml: { + version: "1.0.0", + sections: { + main: [ + { + record_call: { + format: "mp3", + direction: "both", + stereo: true, + beep: true, + status_url: "https://example.com/recording-status", + }, + }, + { play: { url: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } }, + ], + }, + }, +}); +console.log(call.id); +``` + + + +`direction` chooses whose audio to keep: `speak` for what the destination says, `listen` for what +they hear, or `both`. With `stereo` set to `true`, each side lands on its own channel. The +[`record_call` reference][swml-record-call] lists the rest of the parameters and the status callback +payload. + +#### WebSocket (Relay) + +`call.record()` returns a handle you can pause, stop, and wait on. Here the recording ends with the +call and its `finished` event carries the URL; call `recording.stop()` to end it sooner. The zero +timeouts keep the recording running through pauses in the conversation. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as outbound_call.py and run: python outbound_call.py +import asyncio +from signalwire.relay import RelayClient + +client = RelayClient( + project="YOUR_PROJECT_ID", + token="YOUR_API_TOKEN", + host="YOUR_SPACE.signalwire.com", + contexts=["default"], +) + +async def main(): + async with client: + call = await client.dial( + devices=[[{ + "type": "phone", + "params": { + "from_number": "+15551234567", + "to_number": "+15557654321", + "timeout": 30, + }, + }]], + ) + recording = await call.record( + audio={ + "format": "mp3", + "direction": "both", + "stereo": True, + "beep": True, + "initial_timeout": 0, + "end_silence_timeout": 0, + }, + ) + async def hang_up_after_playback(_event): + if call.state != "ended": + await call.hangup() + + await call.play( + [{"type": "tts", "params": {"text": "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}}], + on_completed=hang_up_after_playback, + ) + await call.wait_for_ended() + finished = await recording.wait() + print(f"Recording: {finished.url}") + +asyncio.run(main()) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// Save as outbound-call.mts and run: npx tsx outbound-call.mts +import { RelayClient, RecordEvent } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "YOUR_PROJECT_ID", + token: "YOUR_API_TOKEN", + host: "YOUR_SPACE.signalwire.com", + contexts: ["default"], +}); + +await client.connect(); + +try { + const call = await client.dial([[{ + type: "phone", + params: { + from_number: "+15551234567", + to_number: "+15557654321", + timeout: 30, + }, + }]]); + const recording = await call.record({ + format: "mp3", + direction: "both", + stereo: true, + beep: true, + initial_timeout: 0, + end_silence_timeout: 0, + }); + await call.play( + [{ type: "tts", text: "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }], + { + onCompleted: async () => { + if (call.state !== "ended") await call.hangup(); + }, + }, + ); + await call.waitForEnded(); + const finished = await recording.wait(); + console.log(`Recording: ${(finished as RecordEvent).url}`); +} finally { + await client.disconnect(); +} +``` + + + +Answer the phone to hear the beep before the announcement. The Relay scripts print the recording's +URL when it finishes; with REST, that URL arrives at `status_url`. + +### Stream the call audio + +Send the call's audio to a WebSocket endpoint you run while the call is still in progress, for live +transcription, quality scoring, or any other processing that can't wait for a recording. + +#### REST + +`stream` opens the connection in the background and the document moves on to the next instruction. +`track` chooses which side of the audio to send: `inbound_track` for what the destination says, +`outbound_track` for what they hear, or `both_tracks` for both. Your endpoint has to accept a +secure WebSocket (`wss://`) connection. + +The SDK snippets below keep the imports and `client` setup from your first REST example. + + + +```bash +curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ + -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "command": "dial", + "params": { + "from": "+15551234567", + "to": "+15557654321", + "swml": { + "version": "1.0.0", + "sections": { + "main": [ + { + "stream": { + "url": "wss://example.com/audio-stream", + "track": "both_tracks", + "codec": "PCMU", + "status_url": "https://example.com/stream-status" + } + }, + { "play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} } + ] + } + } + } + }' +``` + + +```python +call = client.calling.dial( + from_="+15551234567", + to="+15557654321", + swml={ + "version": "1.0.0", + "sections": { + "main": [ + { + "stream": { + "url": "wss://example.com/audio-stream", + "track": "both_tracks", + "codec": "PCMU", + "status_url": "https://example.com/stream-status", + } + }, + {"play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}}, + ] + }, + }, +) +print(call["id"]) +``` + + +```typescript +const call = await client.calling.dial({ + from: "+15551234567", + to: "+15557654321", + swml: { + version: "1.0.0", + sections: { + main: [ + { + stream: { + url: "wss://example.com/audio-stream", + track: "both_tracks", + codec: "PCMU", + status_url: "https://example.com/stream-status", + }, + }, + { play: { url: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } }, + ], + }, + }, +}); +console.log(call.id); +``` + + + +The stream runs until the call ends or [`stop_stream`][swml-stop-stream] stops it. Give each stream +its own `control_id` to run more than one at a time, and use `authorization_bearer_token` to +authenticate the handshake with your endpoint. The [`stream` reference][swml-stream] documents the +status events your endpoint receives. + +#### WebSocket (Relay) + +`call.stream()` starts the same background stream over your existing Relay connection and returns a +handle. The stream ends with the call; call `stream.stop()` to end it sooner. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as outbound_call.py and run: python outbound_call.py +import asyncio +from signalwire.relay import RelayClient + +client = RelayClient( + project="YOUR_PROJECT_ID", + token="YOUR_API_TOKEN", + host="YOUR_SPACE.signalwire.com", + contexts=["default"], +) + +async def main(): + async with client: + call = await client.dial( + devices=[[{ + "type": "phone", + "params": { + "from_number": "+15551234567", + "to_number": "+15557654321", + "timeout": 30, + }, + }]], + ) + stream = await call.stream( + url="wss://example.com/audio-stream", + track="both_tracks", + codec="PCMU", + custom_parameters={"ride_id": "ride-4821"}, + ) + print(f"Streaming audio, control ID {stream.control_id}") + + async def hang_up_after_playback(_event): + if call.state != "ended": + await call.hangup() + + await call.play( + [{"type": "tts", "params": {"text": "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}}], + on_completed=hang_up_after_playback, + ) + await call.wait_for_ended() + +asyncio.run(main()) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as outbound-call.mjs, +// then run: node outbound-call.mjs +import { RelayClient } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "YOUR_PROJECT_ID", + token: "YOUR_API_TOKEN", + host: "YOUR_SPACE.signalwire.com", + contexts: ["default"], +}); + +await client.connect(); + +try { + const call = await client.dial([[{ + type: "phone", + params: { + from_number: "+15551234567", + to_number: "+15557654321", + timeout: 30, + }, + }]]); + const stream = await call.stream("wss://example.com/audio-stream", { + track: "both_tracks", + codec: "PCMU", + customParameters: { ride_id: "ride-4821" }, + }); + console.log(`Streaming audio, control ID ${stream.controlId}`); + await call.play( + [{ type: "tts", text: "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }], + { + onCompleted: async () => { + if (call.state !== "ended") await call.hangup(); + }, + }, + ); + await call.waitForEnded(); +} finally { + await client.disconnect(); +} +``` + + + +`custom_parameters` reaches your endpoint in the message that opens the stream, which is where to +put an identifier that ties the audio back to the ride. From a252ee7ada30fde633a070d2879d8cf13ebb1b1f Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 12:09:47 -0400 Subject: [PATCH 059/103] docs: add browser calling example --- .../pages/calling/voice/outbound-calling.mdx | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 85879883f9..b95015ca88 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -22,6 +22,8 @@ max-toc-depth: 3 [swml-stream]: /docs/swml/reference/calling/stream [swml-stop-stream]: /docs/swml/reference/calling/stop-stream [call-whisper]: /docs/swml/guides/call-whisper +[guest-token]: /docs/apis/rest/subscribers/tokens/create-subscriber-guest-token +[browser-auth]: /docs/browser-sdk/v4/guides/authentication Place an outbound phone call with SignalWire and choose what happens when the destination answers. Start by calling your own phone and playing a short announcement, then track the call's progress, @@ -1731,3 +1733,99 @@ try { `custom_parameters` reaches your endpoint in the message that opens the stream, which is where to put an identifier that ties the audio back to the ride. + +### Call from the browser + +Let a rider call Bayview Taxi from a web page. The Browser SDK places the call over WebRTC using a +Subscriber Access Token (SAT) that your backend creates, so no project credentials reach the page. + +A [guest token][guest-token] suits this page: it can dial only the destinations you allow, and +nothing else. Serve the page over HTTPS or from `localhost`, since browsers grant microphone access +only on a secure origin. The [Browser SDK authentication guide][browser-auth] covers the other token +types and how to refresh one before it expires. + + + +```html + + + + + Call Bayview Taxi + + +

Idle

+ + + + + + +``` +
+ +```javascript +// Install: npm install @signalwire/js@4.0.0-rc.2 rxjs@7.8.2 +// Save as call.js next to the page above. +// GET /api/guest-token is your own endpoint: it creates a guest SAT with your +// Project API token and returns it as {"token": "..."}. +import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; + +const statusLine = document.querySelector("#status"); +const remoteAudio = document.querySelector("#remote-audio"); +const callButton = document.querySelector("#call"); +const hangupButton = document.querySelector("#hangup"); + +let clientPromise; + +function getClient() { + clientPromise ??= (async () => { + const response = await fetch("/api/guest-token"); + const { token } = await response.json(); + return new SignalWire(new StaticCredentialProvider({ token })); + })(); + return clientPromise; +} + +function reset() { + remoteAudio.srcObject = null; + callButton.disabled = false; + hangupButton.disabled = true; +} + +callButton.onclick = async () => { + callButton.disabled = true; + statusLine.textContent = "Connecting"; + try { + const client = await getClient(); + const call = await client.dial("+15557654321", { audio: true, video: false }); + call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); + hangupButton.disabled = false; + hangupButton.onclick = () => { + void call.hangup().catch(console.error); + }; + call.status$.subscribe((status) => { + statusLine.textContent = status; + if (status === "disconnected" || status === "failed" || status === "destroyed") { + reset(); + } + }); + } catch (error) { + statusLine.textContent = "Call failed"; + console.error(error); + reset(); + } +}; +``` + +
+ +Select **Call dispatch** and allow microphone access. The status line follows the call through +`ringing`, `connecting`, and `connected`, then `disconnected` when either side hangs up. If the +browser blocks the microphone, the dial fails, the error reaches the console, and the page returns +to its idle state. + +For a mute control, subscribe to `call.self$` for the local participant, which carries +`toggleMute()` and an `audioMuted$` observable. The +[Browser SDK outbound calls guide](/docs/browser-sdk/v4/guides/outbound-calls) covers device +selection, inbound calls, and dialing resource addresses instead of phone numbers. From 623f6d2ef59d3c4c2c18da8ebf1205a1902029df Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 12:32:36 -0400 Subject: [PATCH 060/103] docs: harden outbound browser examples --- .../platform/pages/calling/voice/outbound-calling.mdx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index b95015ca88..3f3024f58c 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -298,7 +298,7 @@ callButton.onclick = async () => { void call.hangup().catch(console.error); }; call.status$.subscribe((status) => { - if (status === "disconnected" || status === "destroyed") { + if (status === "disconnected" || status === "failed" || status === "destroyed") { remoteAudio.srcObject = null; hangupButton.disabled = true; callButton.disabled = false; @@ -1781,9 +1781,14 @@ let clientPromise; function getClient() { clientPromise ??= (async () => { const response = await fetch("/api/guest-token"); + if (!response.ok) throw new Error(`Token request failed: ${response.status}`); const { token } = await response.json(); + if (!token) throw new Error("Token response did not include a token"); return new SignalWire(new StaticCredentialProvider({ token })); - })(); + })().catch((error) => { + clientPromise = undefined; + throw error; + }); return clientPromise; } From 0298bb4a4ac72fac9155cfa823a6da2359c015a0 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 13:28:06 -0400 Subject: [PATCH 061/103] docs: add outbound destination step --- .../platform/pages/calling/voice/outbound-calling.mdx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 3f3024f58c..9c62a33471 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -68,7 +68,7 @@ Call a phone you can answer and play a short announcement. -### Set your credentials and phone numbers +### Set your credentials and caller ID Replace these values in the code sample you choose: @@ -78,9 +78,13 @@ Replace these values in the code sample you choose: | `YOUR_PROJECT_ID` | Your Project ID | | `YOUR_API_TOKEN` | Your API token | | `+15551234567` | Your caller ID number | -| `+15557654321` | A phone number you can answer | -Use E.164 format for both numbers, including `+` and the country code. +Use E.164 format for the caller ID, including `+` and the country code. + +### Choose a destination + +For this walkthrough, choose a phone number you can answer and replace `+15557654321` with that +number in the code sample. Use E.164 format, including `+` and the country code. ### Place the call From bceecff10b13d74f83dea126c47714b760fc81fb Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 13:36:25 -0400 Subject: [PATCH 062/103] docs: standardize outbound call placeholders --- .../pages/calling/voice/outbound-calling.mdx | 355 +++++++++--------- 1 file changed, 178 insertions(+), 177 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 9c62a33471..f5f1589ea0 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -44,7 +44,7 @@ an existing application, choose the approach that fits how you want to control t For the server walkthrough, have these values ready: -- Your Space URL, such as `YOUR_SPACE.signalwire.com`. +- Your Space URL, such as `.signalwire.com`. - Your Project ID and API token from the Dashboard's [API credentials][api-credentials] page. Enable the token's **Voice** permission for the Calling API. - A caller ID: a voice-capable [number in your project][phone-numbers] or a @@ -74,17 +74,18 @@ Replace these values in the code sample you choose: | Value | Replace with | |---|---| -| `YOUR_SPACE` | Your Space's subdomain in `YOUR_SPACE.signalwire.com` | -| `YOUR_PROJECT_ID` | Your Project ID | -| `YOUR_API_TOKEN` | Your API token | -| `+15551234567` | Your caller ID number | +| `` | Your Space's subdomain in `.signalwire.com` | +| `` | Your Project ID | +| `` | Your API token | +| `` | Your caller ID number | Use E.164 format for the caller ID, including `+` and the country code. ### Choose a destination -For this walkthrough, choose a phone number you can answer and replace `+15557654321` with that -number in the code sample. Use E.164 format, including `+` and the country code. +For this walkthrough, choose a phone number you can answer and replace +`` with that number in the code sample. Use E.164 format, including `+` +and the country code. ### Place the call @@ -100,14 +101,14 @@ it when the destination answers, playing the announcement below. ```bash -curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ - -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ +curl -X POST "https://.signalwire.com/api/calling/calls" \ + -u ":" \ -H "Content-Type: application/json" \ -d '{ "command": "dial", "params": { - "from": "+15551234567", - "to": "+15557654321", + "from": "", + "to": "", "swml": { "version": "1.0.0", "sections": { @@ -127,14 +128,14 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ from signalwire.rest import RestClient client = RestClient( - project="YOUR_PROJECT_ID", - token="YOUR_API_TOKEN", - host="YOUR_SPACE.signalwire.com", + project="", + token="", + host=".signalwire.com", ) call = client.calling.dial( - from_="+15551234567", - to="+15557654321", + from_="", + to="", swml={ "version": "1.0.0", "sections": { @@ -155,14 +156,14 @@ print(call["id"]) import { RestClient } from "@signalwire/sdk"; const client = new RestClient({ - project: "YOUR_PROJECT_ID", - token: "YOUR_API_TOKEN", - host: "YOUR_SPACE.signalwire.com", + project: "", + token: "", + host: ".signalwire.com", }); const call = await client.calling.dial({ - from: "+15551234567", - to: "+15557654321", + from: "", + to: "", swml: { version: "1.0.0", sections: { @@ -184,8 +185,8 @@ This response excerpt shows the fields used in the walkthrough. ```json { "id": "0e9c80d7-a149-4917-892d-420043709f45", - "from": "+15551234567", - "to": "+15557654321", + "from": "", + "to": "", "direction": "outbound-api", "status": "queued", "created_at": "2026-09-04T15:20:00Z" @@ -206,9 +207,9 @@ import asyncio from signalwire.relay import RelayClient client = RelayClient( - project="YOUR_PROJECT_ID", - token="YOUR_API_TOKEN", - host="YOUR_SPACE.signalwire.com", + project="", + token="", + host=".signalwire.com", contexts=["default"], ) @@ -218,8 +219,8 @@ async def main(): devices=[[{ "type": "phone", "params": { - "from_number": "+15551234567", - "to_number": "+15557654321", + "from_number": "", + "to_number": "", "timeout": 30, }, }]], @@ -245,9 +246,9 @@ asyncio.run(main()) import { RelayClient } from "@signalwire/sdk"; const client = new RelayClient({ - project: "YOUR_PROJECT_ID", - token: "YOUR_API_TOKEN", - host: "YOUR_SPACE.signalwire.com", + project: "", + token: "", + host: ".signalwire.com", contexts: ["default"], }); @@ -257,8 +258,8 @@ try { const call = await client.dial([[{ type: "phone", params: { - from_number: "+15551234567", - to_number: "+15557654321", + from_number: "", + to_number: "", timeout: 30, }, }]]); @@ -286,7 +287,7 @@ try { import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; const client = new SignalWire(new StaticCredentialProvider({ - token: "YOUR_SUBSCRIBER_ACCESS_TOKEN", + token: "", })); const remoteAudio = document.querySelector("#remote-audio"); const callButton = document.querySelector("#call"); @@ -295,7 +296,7 @@ const hangupButton = document.querySelector("#hangup"); callButton.onclick = async () => { callButton.disabled = true; try { - const call = await client.dial("+15557654321", { audio: true, video: false }); + const call = await client.dial("", { audio: true, video: false }); call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); hangupButton.disabled = false; hangupButton.onclick = () => { @@ -342,7 +343,7 @@ Add `status_url` and `status_events` to your first request to receive call progr Keep the inline `swml` announcement. The examples below place another call with notifications for `ringing`, `answered`, and `ended`. -Before running the request, replace `https://example.com/call-status` with a webhook endpoint +Before running the request, replace `` with a webhook endpoint you control that SignalWire can reach. Your endpoint receives HTTP POST requests as the call reaches the selected states. See the [webhooks guide][webhooks] for endpoint setup and local testing. For the SDK snippets, keep the imports and `client` setup from your first REST example, and @@ -354,14 +355,14 @@ to `ended`. The `ended` payload includes an `end_reason` so you can tell how the ```bash -curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ - -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ +curl -X POST "https://.signalwire.com/api/calling/calls" \ + -u ":" \ -H "Content-Type: application/json" \ -d '{ "command": "dial", "params": { - "from": "+15551234567", - "to": "+15557654321", + "from": "", + "to": "", "swml": { "version": "1.0.0", "sections": { @@ -370,7 +371,7 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ ] } }, - "status_url": "https://example.com/call-status", + "status_url": "", "status_events": ["ringing", "answered", "ended"] } }' @@ -379,8 +380,8 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ ```python call = client.calling.dial( - from_="+15551234567", - to="+15557654321", + from_="", + to="", swml={ "version": "1.0.0", "sections": { @@ -389,7 +390,7 @@ call = client.calling.dial( ] }, }, - status_url="https://example.com/call-status", + status_url="", status_events=["ringing", "answered", "ended"], ) ``` @@ -397,8 +398,8 @@ call = client.calling.dial( ```typescript const call = await client.calling.dial({ - from: "+15551234567", - to: "+15557654321", + from: "", + to: "", swml: { version: "1.0.0", sections: { @@ -407,7 +408,7 @@ const call = await client.calling.dial({ ], }, }, - status_url: "https://example.com/call-status", + status_url: "", status_events: ["ringing", "answered", "ended"], }); ``` @@ -463,9 +464,9 @@ from signalwire.relay import RelayClient from signalwire.relay.event import CallStateEvent client = RelayClient( - project="YOUR_PROJECT_ID", - token="YOUR_API_TOKEN", - host="YOUR_SPACE.signalwire.com", + project="", + token="", + host=".signalwire.com", contexts=["default"], ) @@ -475,8 +476,8 @@ async def main(): devices=[[{ "type": "phone", "params": { - "from_number": "+15551234567", - "to_number": "+15557654321", + "from_number": "", + "to_number": "", "timeout": 30, }, }]], @@ -506,9 +507,9 @@ asyncio.run(main()) import { RelayClient } from "@signalwire/sdk"; const client = new RelayClient({ - project: "YOUR_PROJECT_ID", - token: "YOUR_API_TOKEN", - host: "YOUR_SPACE.signalwire.com", + project: "", + token: "", + host: ".signalwire.com", contexts: ["default"], }); @@ -518,8 +519,8 @@ try { const call = await client.dial([[{ type: "phone", params: { - from_number: "+15551234567", - to_number: "+15557654321", + from_number: "", + to_number: "", timeout: 30, }, }]]); @@ -600,14 +601,14 @@ and starts the agent when the call is answered. Run one of these complete exampl ```bash -curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ - -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ +curl -X POST "https://.signalwire.com/api/calling/calls" \ + -u ":" \ -H "Content-Type: application/json" \ -d '{ "command": "dial", "params": { - "from": "+15551234567", - "to": "+15557654321", + "from": "", + "to": "", "swml": { "version": "1.0.0", "sections": { @@ -637,14 +638,14 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ from signalwire.rest import RestClient client = RestClient( - project="YOUR_PROJECT_ID", - token="YOUR_API_TOKEN", - host="YOUR_SPACE.signalwire.com", + project="", + token="", + host=".signalwire.com", ) call = client.calling.dial( - from_="+15551234567", - to="+15557654321", + from_="", + to="", swml={ "version": "1.0.0", "sections": { @@ -675,14 +676,14 @@ print(call["id"]) import { RestClient } from "@signalwire/sdk"; const client = new RestClient({ - project: "YOUR_PROJECT_ID", - token: "YOUR_API_TOKEN", - host: "YOUR_SPACE.signalwire.com", + project: "", + token: "", + host: ".signalwire.com", }); const call = await client.calling.dial({ - from: "+15551234567", - to: "+15557654321", + from: "", + to: "", swml: { "version": "1.0.0", "sections": { @@ -726,9 +727,9 @@ import asyncio from signalwire.relay import RelayClient client = RelayClient( - project="YOUR_PROJECT_ID", - token="YOUR_API_TOKEN", - host="YOUR_SPACE.signalwire.com", + project="", + token="", + host=".signalwire.com", contexts=["default"], ) @@ -738,8 +739,8 @@ async def main(): devices=[[{ "type": "phone", "params": { - "from_number": "+15551234567", - "to_number": "+15557654321", + "from_number": "", + "to_number": "", "timeout": 30, }, }]], @@ -770,9 +771,9 @@ asyncio.run(main()) import { RelayClient } from "@signalwire/sdk"; const client = new RelayClient({ - project: "YOUR_PROJECT_ID", - token: "YOUR_API_TOKEN", - host: "YOUR_SPACE.signalwire.com", + project: "", + token: "", + host: ".signalwire.com", contexts: ["default"], }); @@ -782,8 +783,8 @@ try { const call = await client.dial([[{ type: "phone", params: { - from_number: "+15551234567", - to_number: "+15557654321", + from_number: "", + to_number: "", timeout: 30, }, }]]); @@ -843,14 +844,14 @@ The SDK snippets below keep the imports and `client` setup from your first REST ```bash -curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ - -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ +curl -X POST "https://.signalwire.com/api/calling/calls" \ + -u ":" \ -H "Content-Type: application/json" \ -d '{ "command": "dial", "params": { - "from": "+15551234567", - "to": "+15557654321", + "from": "", + "to": "", "swml": { "version": "1.0.0", "sections": { @@ -892,8 +893,8 @@ voicemail = "say:This is Bayview Taxi. Your driver is on the way and will arrive live = "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." call = client.calling.dial( - from_="+15551234567", - to="+15557654321", + from_="", + to="", swml={ "version": "1.0.0", "sections": { @@ -923,8 +924,8 @@ const voicemail = "say:This is Bayview Taxi. Your driver is on the way and will const live = "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."; const call = await client.calling.dial({ - from: "+15551234567", - to: "+15557654321", + from: "", + to: "", swml: { version: "1.0.0", sections: { @@ -974,9 +975,9 @@ VOICEMAIL = "This is Bayview Taxi. Your driver is on the way and will arrive in LIVE = "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." client = RelayClient( - project="YOUR_PROJECT_ID", - token="YOUR_API_TOKEN", - host="YOUR_SPACE.signalwire.com", + project="", + token="", + host=".signalwire.com", contexts=["default"], ) @@ -989,8 +990,8 @@ async def main(): devices=[[{ "type": "phone", "params": { - "from_number": "+15551234567", - "to_number": "+15557654321", + "from_number": "", + "to_number": "", "timeout": 30, }, }]], @@ -1039,9 +1040,9 @@ const VOICEMAIL = "This is Bayview Taxi. Your driver is on the way and will arri const LIVE = "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."; const client = new RelayClient({ - project: "YOUR_PROJECT_ID", - token: "YOUR_API_TOKEN", - host: "YOUR_SPACE.signalwire.com", + project: "", + token: "", + host: ".signalwire.com", contexts: ["default"], }); @@ -1056,8 +1057,8 @@ try { const call = await client.dial([[{ type: "phone", params: { - from_number: "+15551234567", - to_number: "+15557654321", + from_number: "", + to_number: "", timeout: 30, }, }]]); @@ -1098,8 +1099,8 @@ the recorded one. Tell the driver who they're about to speak to. A whisper plays to one leg of the call only, so the other person hears nothing but ringback while it plays. -This example dials two phones. Keep your first destination as the rider, and add a second number -you can answer as the driver. +This example dials two phones. Keep `` as the rider's number, and replace +`` with a second number you can answer as the driver. #### REST @@ -1113,14 +1114,14 @@ The SDK snippets below keep the imports and `client` setup from your first REST ```bash -curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ - -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ +curl -X POST "https://.signalwire.com/api/calling/calls" \ + -u ":" \ -H "Content-Type: application/json" \ -d '{ "command": "dial", "params": { - "from": "+15551234567", - "to": "+15557654321", + "from": "", + "to": "", "swml": { "version": "1.0.0", "sections": { @@ -1128,8 +1129,8 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ { "play": {"url": "say:Bayview Taxi here. Connecting you to your driver now."} }, { "connect": { - "from": "+15551234567", - "to": "+15559876543", + "from": "", + "to": "", "confirm": [ { "play": {"url": "say:Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now."} } ], @@ -1148,8 +1149,8 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ whisper = "say:Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now." call = client.calling.dial( - from_="+15551234567", - to="+15557654321", + from_="", + to="", swml={ "version": "1.0.0", "sections": { @@ -1157,8 +1158,8 @@ call = client.calling.dial( {"play": {"url": "say:Bayview Taxi here. Connecting you to your driver now."}}, { "connect": { - "from": "+15551234567", - "to": "+15559876543", + "from": "", + "to": "", "confirm": [{"play": {"url": whisper}}], "confirm_timeout": 20, } @@ -1175,8 +1176,8 @@ print(call["id"]) const whisper = "say:Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now."; const call = await client.calling.dial({ - from: "+15551234567", - to: "+15557654321", + from: "", + to: "", swml: { version: "1.0.0", sections: { @@ -1184,8 +1185,8 @@ const call = await client.calling.dial({ { play: { url: "say:Bayview Taxi here. Connecting you to your driver now." } }, { connect: { - from: "+15551234567", - to: "+15559876543", + from: "", + to: "", confirm: [{ play: { url: whisper } }], confirm_timeout: 20, }, @@ -1218,9 +1219,9 @@ import asyncio from signalwire.relay import RelayClient client = RelayClient( - project="YOUR_PROJECT_ID", - token="YOUR_API_TOKEN", - host="YOUR_SPACE.signalwire.com", + project="", + token="", + host=".signalwire.com", contexts=["default"], ) @@ -1231,8 +1232,8 @@ async def main(): devices=[[{ "type": "phone", "params": { - "from_number": "+15551234567", - "to_number": "+15559876543", + "from_number": "", + "to_number": "", "timeout": 30, }, }]], @@ -1242,8 +1243,8 @@ async def main(): await call.connect([[{ "type": "phone", "params": { - "from_number": "+15551234567", - "to_number": "+15557654321", + "from_number": "", + "to_number": "", "timeout": 30, }, }]]) @@ -1265,9 +1266,9 @@ asyncio.run(main()) import { RelayClient } from "@signalwire/sdk"; const client = new RelayClient({ - project: "YOUR_PROJECT_ID", - token: "YOUR_API_TOKEN", - host: "YOUR_SPACE.signalwire.com", + project: "", + token: "", + host: ".signalwire.com", contexts: ["default"], }); @@ -1278,8 +1279,8 @@ try { const call = await client.dial([[{ type: "phone", params: { - from_number: "+15551234567", - to_number: "+15559876543", + from_number: "", + to_number: "", timeout: 30, }, }]]); @@ -1291,8 +1292,8 @@ try { await call.connect([[{ type: "phone", params: { - from_number: "+15551234567", - to_number: "+15557654321", + from_number: "", + to_number: "", timeout: 30, }, }]]); @@ -1334,14 +1335,14 @@ The SDK snippets below keep the imports and `client` setup from your first REST ```bash -curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ - -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ +curl -X POST "https://.signalwire.com/api/calling/calls" \ + -u ":" \ -H "Content-Type: application/json" \ -d '{ "command": "dial", "params": { - "from": "+15551234567", - "to": "+15557654321", + "from": "", + "to": "", "swml": { "version": "1.0.0", "sections": { @@ -1352,7 +1353,7 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ "direction": "both", "stereo": true, "beep": true, - "status_url": "https://example.com/recording-status" + "status_url": "" } }, { "play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} } @@ -1366,8 +1367,8 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ ```python call = client.calling.dial( - from_="+15551234567", - to="+15557654321", + from_="", + to="", swml={ "version": "1.0.0", "sections": { @@ -1378,7 +1379,7 @@ call = client.calling.dial( "direction": "both", "stereo": True, "beep": True, - "status_url": "https://example.com/recording-status", + "status_url": "", } }, {"play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}}, @@ -1392,8 +1393,8 @@ print(call["id"]) ```typescript const call = await client.calling.dial({ - from: "+15551234567", - to: "+15557654321", + from: "", + to: "", swml: { version: "1.0.0", sections: { @@ -1404,7 +1405,7 @@ const call = await client.calling.dial({ direction: "both", stereo: true, beep: true, - status_url: "https://example.com/recording-status", + status_url: "", }, }, { play: { url: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } }, @@ -1437,9 +1438,9 @@ import asyncio from signalwire.relay import RelayClient client = RelayClient( - project="YOUR_PROJECT_ID", - token="YOUR_API_TOKEN", - host="YOUR_SPACE.signalwire.com", + project="", + token="", + host=".signalwire.com", contexts=["default"], ) @@ -1449,8 +1450,8 @@ async def main(): devices=[[{ "type": "phone", "params": { - "from_number": "+15551234567", - "to_number": "+15557654321", + "from_number": "", + "to_number": "", "timeout": 30, }, }]], @@ -1487,9 +1488,9 @@ asyncio.run(main()) import { RelayClient, RecordEvent } from "@signalwire/sdk"; const client = new RelayClient({ - project: "YOUR_PROJECT_ID", - token: "YOUR_API_TOKEN", - host: "YOUR_SPACE.signalwire.com", + project: "", + token: "", + host: ".signalwire.com", contexts: ["default"], }); @@ -1499,8 +1500,8 @@ try { const call = await client.dial([[{ type: "phone", params: { - from_number: "+15551234567", - to_number: "+15557654321", + from_number: "", + to_number: "", timeout: 30, }, }]]); @@ -1550,24 +1551,24 @@ The SDK snippets below keep the imports and `client` setup from your first REST ```bash -curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ - -u "YOUR_PROJECT_ID:YOUR_API_TOKEN" \ +curl -X POST "https://.signalwire.com/api/calling/calls" \ + -u ":" \ -H "Content-Type: application/json" \ -d '{ "command": "dial", "params": { - "from": "+15551234567", - "to": "+15557654321", + "from": "", + "to": "", "swml": { "version": "1.0.0", "sections": { "main": [ { "stream": { - "url": "wss://example.com/audio-stream", + "url": "", "track": "both_tracks", "codec": "PCMU", - "status_url": "https://example.com/stream-status" + "status_url": "" } }, { "play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} } @@ -1581,18 +1582,18 @@ curl -X POST "https://YOUR_SPACE.signalwire.com/api/calling/calls" \ ```python call = client.calling.dial( - from_="+15551234567", - to="+15557654321", + from_="", + to="", swml={ "version": "1.0.0", "sections": { "main": [ { "stream": { - "url": "wss://example.com/audio-stream", + "url": "", "track": "both_tracks", "codec": "PCMU", - "status_url": "https://example.com/stream-status", + "status_url": "", } }, {"play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}}, @@ -1606,18 +1607,18 @@ print(call["id"]) ```typescript const call = await client.calling.dial({ - from: "+15551234567", - to: "+15557654321", + from: "", + to: "", swml: { version: "1.0.0", sections: { main: [ { stream: { - url: "wss://example.com/audio-stream", + url: "", track: "both_tracks", codec: "PCMU", - status_url: "https://example.com/stream-status", + status_url: "", }, }, { play: { url: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } }, @@ -1649,9 +1650,9 @@ import asyncio from signalwire.relay import RelayClient client = RelayClient( - project="YOUR_PROJECT_ID", - token="YOUR_API_TOKEN", - host="YOUR_SPACE.signalwire.com", + project="", + token="", + host=".signalwire.com", contexts=["default"], ) @@ -1661,14 +1662,14 @@ async def main(): devices=[[{ "type": "phone", "params": { - "from_number": "+15551234567", - "to_number": "+15557654321", + "from_number": "", + "to_number": "", "timeout": 30, }, }]], ) stream = await call.stream( - url="wss://example.com/audio-stream", + url="", track="both_tracks", codec="PCMU", custom_parameters={"ride_id": "ride-4821"}, @@ -1696,9 +1697,9 @@ asyncio.run(main()) import { RelayClient } from "@signalwire/sdk"; const client = new RelayClient({ - project: "YOUR_PROJECT_ID", - token: "YOUR_API_TOKEN", - host: "YOUR_SPACE.signalwire.com", + project: "", + token: "", + host: ".signalwire.com", contexts: ["default"], }); @@ -1708,12 +1709,12 @@ try { const call = await client.dial([[{ type: "phone", params: { - from_number: "+15551234567", - to_number: "+15557654321", + from_number: "", + to_number: "", timeout: 30, }, }]]); - const stream = await call.stream("wss://example.com/audio-stream", { + const stream = await call.stream("", { track: "both_tracks", codec: "PCMU", customParameters: { ride_id: "ride-4821" }, @@ -1807,7 +1808,7 @@ callButton.onclick = async () => { statusLine.textContent = "Connecting"; try { const client = await getClient(); - const call = await client.dial("+15557654321", { audio: true, video: false }); + const call = await client.dial("", { audio: true, video: false }); call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); hangupButton.disabled = false; hangupButton.onclick = () => { From 525eef53079aef7319d1fe762ae878d74c506d42 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 14:11:28 -0400 Subject: [PATCH 063/103] docs: restore outbound destination table --- .../pages/calling/voice/outbound-calling.mdx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index f5f1589ea0..63e3aa55c5 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -24,6 +24,8 @@ max-toc-depth: 3 [call-whisper]: /docs/swml/guides/call-whisper [guest-token]: /docs/apis/rest/subscribers/tokens/create-subscriber-guest-token [browser-auth]: /docs/browser-sdk/v4/guides/authentication +[resource-addresses]: /docs/platform/addresses +[resources]: /docs/platform/resources Place an outbound phone call with SignalWire and choose what happens when the destination answers. Start by calling your own phone and playing a short announcement, then track the call's progress, @@ -83,9 +85,23 @@ Use E.164 format for the caller ID, including `+` and the country code. ### Choose a destination +A call can reach a phone, a SIP endpoint, or a Resource in your SignalWire Space. + +| Destination | Dial | Example | +|---|---|---| +| Phone | Its number in E.164 format | `` | +| SIP endpoint | Its SIP URI | `sip:support@example.com` | +| Subscriber | Its resource address | `/private/support-rep` | +| Application | Its resource address | `/public/support-agent` | +| Conference room | Its resource address | `/public/team-standup` | + +Subscribers, applications such as AI agents, SWML scripts, or Call Flows, and conference rooms are +[Resources][resources]. Each has a [resource address][resource-addresses] in the form +`/context/name`, where the context is `public` or `private`. The Relay Server SDK supports phone +numbers and SIP endpoints, but not resource addresses. + For this walkthrough, choose a phone number you can answer and replace -`` with that number in the code sample. Use E.164 format, including `+` -and the country code. +`` with it. Include `+` and the country code. ### Place the call From eaef4c057ba7139d2ded9960d3ebbebdfb7d8e98 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 14:30:59 -0400 Subject: [PATCH 064/103] docs: generalize outbound destination placeholder --- .../pages/calling/voice/outbound-calling.mdx | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 63e3aa55c5..af8780c007 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -89,7 +89,7 @@ A call can reach a phone, a SIP endpoint, or a Resource in your SignalWire Space | Destination | Dial | Example | |---|---|---| -| Phone | Its number in E.164 format | `` | +| Phone | Its number in E.164 format | `` | | SIP endpoint | Its SIP URI | `sip:support@example.com` | | Subscriber | Its resource address | `/private/support-rep` | | Application | Its resource address | `/public/support-agent` | @@ -101,7 +101,7 @@ Subscribers, applications such as AI agents, SWML scripts, or Call Flows, and co numbers and SIP endpoints, but not resource addresses. For this walkthrough, choose a phone number you can answer and replace -`` with it. Include `+` and the country code. +`` with it. Include `+` and the country code. ### Place the call @@ -124,7 +124,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ "command": "dial", "params": { "from": "", - "to": "", + "to": "", "swml": { "version": "1.0.0", "sections": { @@ -151,7 +151,7 @@ client = RestClient( call = client.calling.dial( from_="", - to="", + to="", swml={ "version": "1.0.0", "sections": { @@ -179,7 +179,7 @@ const client = new RestClient({ const call = await client.calling.dial({ from: "", - to: "", + to: "", swml: { version: "1.0.0", sections: { @@ -202,7 +202,7 @@ This response excerpt shows the fields used in the walkthrough. { "id": "0e9c80d7-a149-4917-892d-420043709f45", "from": "", - "to": "", + "to": "", "direction": "outbound-api", "status": "queued", "created_at": "2026-09-04T15:20:00Z" @@ -236,7 +236,7 @@ async def main(): "type": "phone", "params": { "from_number": "", - "to_number": "", + "to_number": "", "timeout": 30, }, }]], @@ -275,7 +275,7 @@ try { type: "phone", params: { from_number: "", - to_number: "", + to_number: "", timeout: 30, }, }]]); @@ -312,7 +312,7 @@ const hangupButton = document.querySelector("#hangup"); callButton.onclick = async () => { callButton.disabled = true; try { - const call = await client.dial("", { audio: true, video: false }); + const call = await client.dial("", { audio: true, video: false }); call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); hangupButton.disabled = false; hangupButton.onclick = () => { @@ -378,7 +378,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ "command": "dial", "params": { "from": "", - "to": "", + "to": "", "swml": { "version": "1.0.0", "sections": { @@ -397,7 +397,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ ```python call = client.calling.dial( from_="", - to="", + to="", swml={ "version": "1.0.0", "sections": { @@ -415,7 +415,7 @@ call = client.calling.dial( ```typescript const call = await client.calling.dial({ from: "", - to: "", + to: "", swml: { version: "1.0.0", sections: { @@ -493,7 +493,7 @@ async def main(): "type": "phone", "params": { "from_number": "", - "to_number": "", + "to_number": "", "timeout": 30, }, }]], @@ -536,7 +536,7 @@ try { type: "phone", params: { from_number: "", - to_number: "", + to_number: "", timeout: 30, }, }]]); @@ -624,7 +624,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ "command": "dial", "params": { "from": "", - "to": "", + "to": "", "swml": { "version": "1.0.0", "sections": { @@ -661,7 +661,7 @@ client = RestClient( call = client.calling.dial( from_="", - to="", + to="", swml={ "version": "1.0.0", "sections": { @@ -699,7 +699,7 @@ const client = new RestClient({ const call = await client.calling.dial({ from: "", - to: "", + to: "", swml: { "version": "1.0.0", "sections": { @@ -756,7 +756,7 @@ async def main(): "type": "phone", "params": { "from_number": "", - "to_number": "", + "to_number": "", "timeout": 30, }, }]], @@ -800,7 +800,7 @@ try { type: "phone", params: { from_number: "", - to_number: "", + to_number: "", timeout: 30, }, }]]); @@ -867,7 +867,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ "command": "dial", "params": { "from": "", - "to": "", + "to": "", "swml": { "version": "1.0.0", "sections": { @@ -910,7 +910,7 @@ live = "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive call = client.calling.dial( from_="", - to="", + to="", swml={ "version": "1.0.0", "sections": { @@ -941,7 +941,7 @@ const live = "say:Hi, this is Bayview Taxi. Your driver is on the way and will a const call = await client.calling.dial({ from: "", - to: "", + to: "", swml: { version: "1.0.0", sections: { @@ -1007,7 +1007,7 @@ async def main(): "type": "phone", "params": { "from_number": "", - "to_number": "", + "to_number": "", "timeout": 30, }, }]], @@ -1074,7 +1074,7 @@ try { type: "phone", params: { from_number: "", - to_number: "", + to_number: "", timeout: 30, }, }]]); @@ -1115,7 +1115,7 @@ the recorded one. Tell the driver who they're about to speak to. A whisper plays to one leg of the call only, so the other person hears nothing but ringback while it plays. -This example dials two phones. Keep `` as the rider's number, and replace +This example dials two phones. Keep `` as the rider's number, and replace `` with a second number you can answer as the driver. #### REST @@ -1137,7 +1137,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ "command": "dial", "params": { "from": "", - "to": "", + "to": "", "swml": { "version": "1.0.0", "sections": { @@ -1166,7 +1166,7 @@ whisper = "say:Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecti call = client.calling.dial( from_="", - to="", + to="", swml={ "version": "1.0.0", "sections": { @@ -1193,7 +1193,7 @@ const whisper = "say:Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Co const call = await client.calling.dial({ from: "", - to: "", + to: "", swml: { version: "1.0.0", sections: { @@ -1260,7 +1260,7 @@ async def main(): "type": "phone", "params": { "from_number": "", - "to_number": "", + "to_number": "", "timeout": 30, }, }]]) @@ -1309,7 +1309,7 @@ try { type: "phone", params: { from_number: "", - to_number: "", + to_number: "", timeout: 30, }, }]]); @@ -1358,7 +1358,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ "command": "dial", "params": { "from": "", - "to": "", + "to": "", "swml": { "version": "1.0.0", "sections": { @@ -1384,7 +1384,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ ```python call = client.calling.dial( from_="", - to="", + to="", swml={ "version": "1.0.0", "sections": { @@ -1410,7 +1410,7 @@ print(call["id"]) ```typescript const call = await client.calling.dial({ from: "", - to: "", + to: "", swml: { version: "1.0.0", sections: { @@ -1467,7 +1467,7 @@ async def main(): "type": "phone", "params": { "from_number": "", - "to_number": "", + "to_number": "", "timeout": 30, }, }]], @@ -1517,7 +1517,7 @@ try { type: "phone", params: { from_number: "", - to_number: "", + to_number: "", timeout: 30, }, }]]); @@ -1574,7 +1574,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ "command": "dial", "params": { "from": "", - "to": "", + "to": "", "swml": { "version": "1.0.0", "sections": { @@ -1599,7 +1599,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ ```python call = client.calling.dial( from_="", - to="", + to="", swml={ "version": "1.0.0", "sections": { @@ -1624,7 +1624,7 @@ print(call["id"]) ```typescript const call = await client.calling.dial({ from: "", - to: "", + to: "", swml: { version: "1.0.0", sections: { @@ -1679,7 +1679,7 @@ async def main(): "type": "phone", "params": { "from_number": "", - "to_number": "", + "to_number": "", "timeout": 30, }, }]], @@ -1726,7 +1726,7 @@ try { type: "phone", params: { from_number: "", - to_number: "", + to_number: "", timeout: 30, }, }]]); @@ -1824,7 +1824,7 @@ callButton.onclick = async () => { statusLine.textContent = "Connecting"; try { const client = await getClient(); - const call = await client.dial("", { audio: true, video: false }); + const call = await client.dial("", { audio: true, video: false }); call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); hangupButton.disabled = false; hangupButton.onclick = () => { From e87ea547ffe0c5cf8ad92f32bcd5c56456022dc7 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 14:49:08 -0400 Subject: [PATCH 065/103] docs: clarify outbound destination labels --- .../pages/calling/voice/outbound-calling.mdx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index af8780c007..8534f774a0 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -91,14 +91,15 @@ A call can reach a phone, a SIP endpoint, or a Resource in your SignalWire Space |---|---|---| | Phone | Its number in E.164 format | `` | | SIP endpoint | Its SIP URI | `sip:support@example.com` | -| Subscriber | Its resource address | `/private/support-rep` | +| Web client | Its subscriber resource address | `/private/support-rep` | | Application | Its resource address | `/public/support-agent` | -| Conference room | Its resource address | `/public/team-standup` | +| Conference | Its resource address | `/public/team-standup` | -Subscribers, applications such as AI agents, SWML scripts, or Call Flows, and conference rooms are -[Resources][resources]. Each has a [resource address][resource-addresses] in the form -`/context/name`, where the context is `public` or `private`. The Relay Server SDK supports phone -numbers and SIP endpoints, but not resource addresses. +Web clients connect through Subscriber resources. Subscribers, applications such as AI agents, +SWML scripts, or Call Flows, and conferences are [Resources][resources]. Each has a +[resource address][resource-addresses] in the form `/context/name`, where the context is `public` +or `private`. The Relay Server SDK supports phone numbers and SIP endpoints, but not resource +addresses. For this walkthrough, choose a phone number you can answer and replace `` with it. Include `+` and the country code. From f72b8934a49c0eba98d0acea005b4477ffbb6c17 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 14:53:45 -0400 Subject: [PATCH 066/103] docs: tab first outbound call methods --- .../platform/pages/calling/voice/outbound-calling.mdx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 8534f774a0..ab0c8fb68b 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -108,9 +108,10 @@ For this walkthrough, choose a phone number you can answer and replace REST requests run on your server. For WebSocket calling, choose a Server SDK or the Browser SDK. -For your first call, run the **cURL — Calling API** request in the REST section below. +For your first call, open the **REST** tab and run the **cURL — Calling API** request. -#### REST + + The request includes a SignalWire Markup Language (SWML) document in `swml`. SignalWire runs it when the destination answers, playing the announcement below. @@ -210,7 +211,8 @@ This response excerpt shows the fields used in the walkthrough. } ``` -#### WebSocket (Relay) + + Use a Server SDK to play the announcement, or the Browser SDK to let a user speak on the call from your web app. @@ -341,6 +343,9 @@ For server dialing options, see the [Python dial reference](/docs/server-sdks/reference/python/relay/client/dial) or [TypeScript dial reference](/docs/server-sdks/reference/typescript/relay/client/dial). + + + ### Answer the call Answer the destination phone. With a server example, you should hear “Hi, this is Bayview Taxi” From 447da51423e30cdd18c00a6ee34b87d358794b31 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 15:01:53 -0400 Subject: [PATCH 067/103] docs: trim outbound calling instructions --- .../pages/calling/voice/outbound-calling.mdx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index ab0c8fb68b..3c37c04029 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -33,8 +33,7 @@ run an AI agent, or let users call from your web app. ## Choose how to place your call -For your first call, start with the REST cURL example below. If you're adding outbound calling to -an existing application, choose the approach that fits how you want to control the call. +If you're adding outbound calling to an existing application, choose the approach that fits how you want to control the call. | What you want to do | Where to start | |---|---| @@ -81,8 +80,6 @@ Replace these values in the code sample you choose: | `` | Your API token | | `` | Your caller ID number | -Use E.164 format for the caller ID, including `+` and the country code. - ### Choose a destination A call can reach a phone, a SIP endpoint, or a Resource in your SignalWire Space. @@ -98,18 +95,15 @@ A call can reach a phone, a SIP endpoint, or a Resource in your SignalWire Space Web clients connect through Subscriber resources. Subscribers, applications such as AI agents, SWML scripts, or Call Flows, and conferences are [Resources][resources]. Each has a [resource address][resource-addresses] in the form `/context/name`, where the context is `public` -or `private`. The Relay Server SDK supports phone numbers and SIP endpoints, but not resource -addresses. +or `private`. For this walkthrough, choose a phone number you can answer and replace -`` with it. Include `+` and the country code. +`` with it. ### Place the call REST requests run on your server. For WebSocket calling, choose a Server SDK or the Browser SDK. -For your first call, open the **REST** tab and run the **cURL — Calling API** request. - From dbbef1ee4f82cf7bf5aff5e7f050500ae33a1951 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 15:06:28 -0400 Subject: [PATCH 068/103] docs: streamline outbound call examples --- .../pages/calling/voice/outbound-calling.mdx | 163 +++--------------- 1 file changed, 25 insertions(+), 138 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 3c37c04029..abbe8090e9 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -16,14 +16,10 @@ max-toc-depth: 3 [sdk-install]: /docs/server-sdks/guides/installation [webhooks]: /docs/platform/webhooks [swml-detect-machine]: /docs/swml/reference/calling/detect-machine -[swml-connect]: /docs/swml/reference/calling/connect [swml-record-call]: /docs/swml/reference/calling/record-call -[swml-stop-record-call]: /docs/swml/reference/calling/stop-record-call [swml-stream]: /docs/swml/reference/calling/stream -[swml-stop-stream]: /docs/swml/reference/calling/stop-stream [call-whisper]: /docs/swml/guides/call-whisper [guest-token]: /docs/apis/rest/subscribers/tokens/create-subscriber-guest-token -[browser-auth]: /docs/browser-sdk/v4/guides/authentication [resource-addresses]: /docs/platform/addresses [resources]: /docs/platform/resources @@ -594,25 +590,16 @@ sequenceDiagram ### Run an AI agent -Replace the announcement from [your first call](#place-the-call) with an AI agent that tells the -rider when their taxi will arrive and answers follow-up questions. Keep the same credentials, -caller ID, and destination number. +Call a rider with an AI agent that shares their arrival time and answers follow-up questions. -An AI voice counts as an artificial voice under the Telephone Consumer Protection Act (TCPA). -Check consent, your do-not-call list, and the local calling hours in your code before you send the -`dial` request. The [TCPA guide][tcpa] and -the compliance section of [AI best practices][ai-best-practices] cover each obligation. This is -technical guidance, not legal advice. +Before dialing, follow consent, do-not-call, and calling-hour requirements for artificial voices. +See the [TCPA guide][tcpa] and [AI best practices][ai-best-practices]. -Replace the credentials and phone numbers as you did for your first call, then choose REST or -WebSocket (Relay). - #### REST -Send a `dial` request with an inline SWML `ai` instruction. SignalWire calls the destination -and starts the agent when the call is answered. Run one of these complete examples: +Send a `dial` request with an inline SWML [`ai` instruction][swml-ai] that starts when the call is answered. @@ -724,15 +711,9 @@ console.log(call.id); -The response returns a call `id`; the conversation runs on SignalWire. To reuse a hosted SWML -document, pass its URL as `url` in place of `swml`, as described in the -[Calling API reference](/docs/apis/rest/calls/call-commands). - #### WebSocket (Relay) -Connect a Relay client, dial the destination, then start the agent with `call.ai()` on the -answered call. Python uses `ai_params`; TypeScript uses `aiParams` for the same AI settings. -These scripts keep the WebSocket open until the destination hangs up, then disconnect. +Dial with Relay, start the agent with `call.ai()`, and keep the connection open until the call ends. @@ -825,37 +806,16 @@ try { -Use [Python `call.wait_for_ended()`](/docs/server-sdks/reference/python/relay/call/wait-for-ended) -or [TypeScript `call.waitForEnded()`](/docs/server-sdks/reference/typescript/relay/call/wait-for-ended) -to wait for the call to end. The AI action's `wait()` method waits for an AI action completion -event, which is separate from the call-ended event. - -Answer the call to hear the automated greeting, then ask when your driver will arrive. -The agent should respond using the five-minute estimate. Hang up the destination phone when -you finish; the Relay script then exits. - -`static_greeting_no_barge` lets the greeting finish before the conversation begins. The SWML -[`ai` method][swml-ai] reference covers every parameter, as does the Relay `ai` reference for -[Python](/docs/server-sdks/reference/python/relay/call/ai), -[TypeScript](/docs/server-sdks/reference/typescript/relay/call/ai), or your language's SDK. - ### Leave a voicemail -Not every call reaches a person. Answering machine detection tells you whether a human or a machine -picked up, so the rider hears the arrival time either way — spoken to them live, or left on their -voicemail after the greeting. +Use answering machine detection (AMD) to speak to a person immediately or leave a message after a +voicemail greeting and beep. -A prerecorded voice is regulated much like an AI voice, so check consent and local calling hours -before you dial. The [TCPA guide][tcpa] covers the obligations. +Follow the consent and calling-hour requirements in the [TCPA guide][tcpa]. #### REST -`detect_machine` runs before the rest of the document and saves its verdict in the `detect_result` -variable, which is `machine`, `human`, `fax`, `unknown`, or `error`. Setting `detect_message_end` to -`true` holds the document until the greeting ends, so your message records from the beginning -instead of over the greeting. The `switch` then picks what to play. - -The SDK snippets below keep the imports and `client` setup from your first REST example. +Use [`detect_machine`][swml-detect-machine] and `switch` to choose the live or voicemail message. @@ -967,16 +927,10 @@ console.log(call.id); -Add a `status_url` to `detect_machine` to receive every detection event, including whether a beep -was heard. The [`detect_machine` reference][swml-detect-machine] lists the timing parameters that -control how patient the detector is. - #### WebSocket (Relay) -`call.detect()` starts the same detector on the answered call and reports its outcome in a -`calling.call.detect` event: `MACHINE`, `HUMAN`, or `UNKNOWN`. A machine sends a further `READY` -event once its greeting and beep have finished, which is the moment to start the message. Register -the handler before you start detection, so no event arrives before you're listening. +Use `call.detect()` to play the live message after `HUMAN` or `UNKNOWN`, or wait for `READY` after a +machine greeting. @@ -1107,25 +1061,14 @@ try { -Answer the destination phone to hear the live message, or let it ring through to voicemail to hear -the recorded one. - ### Whisper before connecting two people -Tell the driver who they're about to speak to. A whisper plays to one leg of the call only, so the -other person hears nothing but ringback while it plays. - -This example dials two phones. Keep `` as the rider's number, and replace -`` with a second number you can answer as the driver. +Play pickup details privately to ``, then connect that call to +``. #### REST -Call the rider, then `connect` to the driver. The `confirm` array runs on the driver's leg as soon -as they answer and finishes before the two legs are bridged, so only the driver hears the pickup -details. `confirm` also takes the URL of a SWML document, which the -[Call whisper guide][call-whisper] walks through. - -The SDK snippets below keep the imports and `client` setup from your first REST example. +Use `connect.confirm` to play the [whisper][call-whisper] to the driver before bridging the calls. @@ -1216,15 +1159,9 @@ console.log(call.id); -`confirm_timeout` bounds how long the confirm script may run, and 20 seconds is ample for a -one-sentence whisper. The [`connect` reference][swml-connect] covers dialing several numbers in turn -or at once, and what to run when the connected call ends. - #### WebSocket (Relay) -Relay has no `confirm` parameter, so dial the person who gets the whisper first. These scripts call -the driver, play the pickup details to that leg, then bridge the rider with `connect()`. The rider's -phone rings only after the whisper finishes. +Dial the driver first, play the whisper, then connect the rider after playback finishes. @@ -1324,29 +1261,17 @@ try { -Answer the driver's phone to hear the whisper, then answer the rider's phone: the two legs are -bridged once the whisper finishes. - ### Record the call -Keep an audio record of what was said, whether that's an automated message or a live conversation -between the rider and a dispatcher. +Record both sides of an outbound call and retrieve the finished recording URL. -Recording laws vary by country and by state, and some require the consent of every party on the -call. Confirm what applies to the numbers you dial, and announce the recording when consent is -required. This is technical guidance, not legal advice. +Confirm which parties must consent and announce the recording when required. #### REST -`record_call` starts a recording in the background and the document continues to the next -instruction. The recording ends with the call, or when you run -[`stop_record_call`][swml-stop-record-call]. SignalWire saves the recording's URL in the -`record_call_url` variable, and POSTs the finished recording's URL, duration, and size to -`status_url`. - -The SDK snippets below keep the imports and `client` setup from your first REST example. +Start [`record_call`][swml-record-call] in the background and receive the result at `status_url`. @@ -1434,16 +1359,9 @@ console.log(call.id); -`direction` chooses whose audio to keep: `speak` for what the destination says, `listen` for what -they hear, or `both`. With `stereo` set to `true`, each side lands on its own channel. The -[`record_call` reference][swml-record-call] lists the rest of the parameters and the status callback -payload. - #### WebSocket (Relay) -`call.record()` returns a handle you can pause, stop, and wait on. Here the recording ends with the -call and its `finished` event carries the URL; call `recording.stop()` to end it sooner. The zero -timeouts keep the recording running through pauses in the conversation. +Start `call.record()` and read the recording URL from its `finished` event. @@ -1547,22 +1465,13 @@ try { -Answer the phone to hear the beep before the announcement. The Relay scripts print the recording's -URL when it finishes; with REST, that URL arrives at `status_url`. - ### Stream the call audio -Send the call's audio to a WebSocket endpoint you run while the call is still in progress, for live -transcription, quality scoring, or any other processing that can't wait for a recording. +Stream both sides of a live call to your secure WebSocket endpoint for real-time processing. #### REST -`stream` opens the connection in the background and the document moves on to the next instruction. -`track` chooses which side of the audio to send: `inbound_track` for what the destination says, -`outbound_track` for what they hear, or `both_tracks` for both. Your endpoint has to accept a -secure WebSocket (`wss://`) connection. - -The SDK snippets below keep the imports and `client` setup from your first REST example. +Start [`stream`][swml-stream] in the background and send status events to your webhook. @@ -1647,15 +1556,9 @@ console.log(call.id); -The stream runs until the call ends or [`stop_stream`][swml-stop-stream] stops it. Give each stream -its own `control_id` to run more than one at a time, and use `authorization_bearer_token` to -authenticate the handshake with your endpoint. The [`stream` reference][swml-stream] documents the -status events your endpoint receives. - #### WebSocket (Relay) -`call.stream()` starts the same background stream over your existing Relay connection and returns a -handle. The stream ends with the call; call `stream.stop()` to end it sooner. +Start `call.stream()` over the Relay connection and keep it running until the call ends. @@ -1752,18 +1655,10 @@ try { -`custom_parameters` reaches your endpoint in the message that opens the stream, which is where to -put an identifier that ties the audio back to the ride. - ### Call from the browser -Let a rider call Bayview Taxi from a web page. The Browser SDK places the call over WebRTC using a -Subscriber Access Token (SAT) that your backend creates, so no project credentials reach the page. - -A [guest token][guest-token] suits this page: it can dial only the destinations you allow, and -nothing else. Serve the page over HTTPS or from `localhost`, since browsers grant microphone access -only on a secure origin. The [Browser SDK authentication guide][browser-auth] covers the other token -types and how to refresh one before it expires. +Place a WebRTC call from a web page using a restricted [guest token][guest-token] from your backend. +Serve the page over HTTPS or `localhost` so the browser can access the microphone. @@ -1846,12 +1741,4 @@ callButton.onclick = async () => { -Select **Call dispatch** and allow microphone access. The status line follows the call through -`ringing`, `connecting`, and `connected`, then `disconnected` when either side hangs up. If the -browser blocks the microphone, the dial fails, the error reaches the console, and the page returns -to its idle state. - -For a mute control, subscribe to `call.self$` for the local participant, which carries -`toggleMute()` and an `audioMuted$` observable. The -[Browser SDK outbound calls guide](/docs/browser-sdk/v4/guides/outbound-calls) covers device -selection, inbound calls, and dialing resource addresses instead of phone numbers. +See the [Browser SDK outbound calls guide](/docs/browser-sdk/v4/guides/outbound-calls) for more. From c533d0c57977b52dd7fae711ed4f0445165f6fa9 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 15:12:34 -0400 Subject: [PATCH 069/103] docs: add browser call lifecycle tracking --- .../pages/calling/voice/outbound-calling.mdx | 68 ++++++++++++++++--- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index abbe8090e9..29a520faf9 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -456,15 +456,13 @@ sequenceDiagram -### WebSocket (Relay) +### WebSocket -The Relay SDK lets you follow the call in your code: `dial()` returns when the destination -answers, and a playback completion callback ends the call after the announcement. Register a -handler with `call.on` to observe later state changes, then wait for the call-ended event before -closing the connection. +Relay exposes call state events through `call.on()`. Because `dial()` returns after the destination +answers, the handler observes later states such as `ending` and `ended`. -Replace your first Relay script with the version below. It registers a state handler immediately -after `dial()` returns, before playback starts, and keeps the same announcement and connection setup. +The Browser SDK returns a WebRTC `Call` while it is `ringing`. Subscribe to `call.status$` to follow +it through `connecting`, `connected`, and the final states. @@ -552,15 +550,63 @@ try { } ``` + +```javascript +// Install: npm install @signalwire/js@4.0.0-rc.2 rxjs@7.8.2 +// Run on HTTPS or localhost with these elements in your page: +//

Idle

+// +// +// +import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; + +const client = new SignalWire(new StaticCredentialProvider({ + token: "", +})); +const statusLine = document.querySelector("#status"); +const remoteAudio = document.querySelector("#remote-audio"); +const callButton = document.querySelector("#call"); +const hangupButton = document.querySelector("#hangup"); +const finalStatuses = new Set(["disconnected", "failed", "destroyed"]); + +callButton.onclick = async () => { + callButton.disabled = true; + try { + const call = await client.dial("", { audio: true, video: false }); + call.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); + call.status$.subscribe((status) => { + statusLine.textContent = `Call: ${status}`; + console.log(`Call: ${status}`); + + if (finalStatuses.has(status)) { + remoteAudio.srcObject = null; + hangupButton.disabled = true; + callButton.disabled = false; + } + }); + + hangupButton.disabled = false; + hangupButton.onclick = () => { + void call.hangup().catch(console.error); + }; + } catch (error) { + statusLine.textContent = "Call failed"; + callButton.disabled = false; + console.error(error); + } +}; +``` +
-Because you attach the handler after `dial()` returns, it observes later states such as `ending` -and `ended`. For more event handlers, see the +For more Relay event handlers, see the [Python events reference](/docs/server-sdks/reference/python/relay/events) or [TypeScript events reference](/docs/server-sdks/reference/typescript/relay/events). +For Browser SDK call states, see the +[outbound calls guide](/docs/browser-sdk/v4/guides/outbound-calls). -This flow shows the commands your code sends and the events SignalWire returns over the same -persistent connection: +For Relay, this flow shows the commands your code sends and the events SignalWire returns over the +same persistent connection: From 064acaeac52c90af7efefd23f73e4a141ce37b6b Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 15:19:15 -0400 Subject: [PATCH 070/103] docs: remove first request setup section --- .../platform/pages/calling/voice/outbound-calling.mdx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 29a520faf9..fa38894255 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -13,7 +13,6 @@ max-toc-depth: 3 [swml-ai]: /docs/swml/reference/calling/ai [ai-best-practices]: /docs/platform/ai/best-practices [tcpa]: /docs/platform/compliance/tcpa -[sdk-install]: /docs/server-sdks/guides/installation [webhooks]: /docs/platform/webhooks [swml-detect-machine]: /docs/swml/reference/calling/detect-machine [swml-record-call]: /docs/swml/reference/calling/record-call @@ -48,11 +47,6 @@ For the server walkthrough, have these values ready: [verified caller ID][caller-id]. - A destination phone you can answer. -Use cURL in your terminal to make the first REST request. If you choose Python or TypeScript, -follow the setup comments in that sample; the [Server SDK installation guide][sdk-install] -covers runtime requirements and environment setup. The server examples use Python -`signalwire-sdk` 3.4.1 and TypeScript `@signalwire/sdk` 2.0.5. - A [trial project][trial-mode] can dial only numbers it has purchased or verified, and cannot call internationally at all. Any other project can dial any number, but needs From 390182b44905556706abb341acb49fddefea9bad Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 15:52:56 -0400 Subject: [PATCH 071/103] docs: clarify caller ID prerequisite --- .../platform/pages/calling/voice/outbound-calling.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index fa38894255..061ade216a 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -43,8 +43,8 @@ For the server walkthrough, have these values ready: - Your Space URL, such as `.signalwire.com`. - Your Project ID and API token from the Dashboard's [API credentials][api-credentials] page. Enable the token's **Voice** permission for the Calling API. -- A caller ID: a voice-capable [number in your project][phone-numbers] or a - [verified caller ID][caller-id]. +- If calling a phone number, a voice-capable [phone number purchased in your Space][phone-numbers] + or a [verified caller ID][caller-id]. - A destination phone you can answer. From 3093d89e43857ab9b318eabe3a50414c0ef47fa9 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 15:56:26 -0400 Subject: [PATCH 072/103] docs: clarify destinations and response schema --- .../pages/calling/voice/outbound-calling.mdx | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 061ade216a..c66f9b01c8 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -45,7 +45,7 @@ For the server walkthrough, have these values ready: Enable the token's **Voice** permission for the Calling API. - If calling a phone number, a voice-capable [phone number purchased in your Space][phone-numbers] or a [verified caller ID][caller-id]. -- A destination phone you can answer. +- A destination device you can answer. A [trial project][trial-mode] can dial only numbers it has purchased or verified, and cannot call @@ -82,13 +82,13 @@ A call can reach a phone, a SIP endpoint, or a Resource in your SignalWire Space | Application | Its resource address | `/public/support-agent` | | Conference | Its resource address | `/public/team-standup` | -Web clients connect through Subscriber resources. Subscribers, applications such as AI agents, +Subscribers, applications such as AI agents, SWML scripts, or Call Flows, and conferences are [Resources][resources]. Each has a [resource address][resource-addresses] in the form `/context/name`, where the context is `public` or `private`. -For this walkthrough, choose a phone number you can answer and replace -`` with it. +For this walkthrough, choose a device you can answer and replace +`` with its address. ### Place the call @@ -182,18 +182,8 @@ console.log(call.id); The REST API returns a call `id` and status `queued`. This confirms that SignalWire accepted the request; the call hasn't necessarily rung or been answered yet. Save the `id` to identify this call. -This response excerpt shows the fields used in the walkthrough. - -```json -{ - "id": "0e9c80d7-a149-4917-892d-420043709f45", - "from": "", - "to": "", - "direction": "outbound-api", - "status": "queued", - "created_at": "2026-09-04T15:20:00Z" -} -``` + +
From 057b84460fc28eb25a20435902af5987eda70180 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 16:05:42 -0400 Subject: [PATCH 073/103] docs: render call leg response payload --- fern/products/platform/pages/calling/voice/outbound-calling.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index c66f9b01c8..e3e97be581 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -183,7 +183,7 @@ console.log(call.id); The REST API returns a call `id` and status `queued`. This confirms that SignalWire accepted the request; the call hasn't necessarily rung or been answered yet. Save the `id` to identify this call. - + From f44e4997fedc4fda613bd49b310337661c6b1afc Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 16:26:09 -0400 Subject: [PATCH 074/103] docs: build outbound SWML with SDK helpers --- .../pages/calling/voice/outbound-calling.mdx | 396 ++++++++---------- 1 file changed, 186 insertions(+), 210 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index e3e97be581..4e54c5d087 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -127,6 +127,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ ```python # Install: python -m pip install signalwire-sdk==3.4.1 # Save as outbound_call.py and run: python outbound_call.py +from signalwire import SWMLBuilder, SWMLService from signalwire.rest import RestClient client = RestClient( @@ -135,17 +136,16 @@ client = RestClient( host=".signalwire.com", ) +swml = ( + SWMLBuilder(SWMLService(name="outbound-call")) + .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .build() +) + call = client.calling.dial( from_="", to="", - swml={ - "version": "1.0.0", - "sections": { - "main": [ - {"play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}} - ] - }, - }, + swml=swml, ) print(call["id"]) ``` @@ -155,7 +155,7 @@ print(call["id"]) // Install: npm install @signalwire/sdk@2.0.5 // This sample also runs as JavaScript: save as outbound-call.mjs, // then run: node outbound-call.mjs -import { RestClient } from "@signalwire/sdk"; +import { RestClient, SwmlBuilder } from "@signalwire/sdk"; const client = new RestClient({ project: "", @@ -163,17 +163,14 @@ const client = new RestClient({ host: ".signalwire.com", }); +const swml = new SwmlBuilder() + .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .build(); + const call = await client.calling.dial({ from: "", to: "", - swml: { - version: "1.0.0", - sections: { - main: [ - { play: { url: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } }, - ], - }, - }, + swml, }); console.log(call.id); ``` @@ -375,17 +372,16 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \
```python +swml = ( + SWMLBuilder(SWMLService(name="outbound-call")) + .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .build() +) + call = client.calling.dial( from_="", to="", - swml={ - "version": "1.0.0", - "sections": { - "main": [ - {"play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}} - ] - }, - }, + swml=swml, status_url="", status_events=["ringing", "answered", "ended"], ) @@ -393,17 +389,14 @@ call = client.calling.dial( ```typescript +const swml = new SwmlBuilder() + .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .build(); + const call = await client.calling.dial({ from: "", to: "", - swml: { - version: "1.0.0", - sections: { - main: [ - { play: { url: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } }, - ], - }, - }, + swml, status_url: "", status_events: ["ringing", "answered", "ended"], }); @@ -668,6 +661,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ ```python # Install: python -m pip install signalwire-sdk==3.4.1 # Save as outbound_call.py and run: python outbound_call.py +from signalwire import SWMLBuilder, SWMLService from signalwire.rest import RestClient client = RestClient( @@ -676,27 +670,22 @@ client = RestClient( host=".signalwire.com", ) +swml = ( + SWMLBuilder(SWMLService(name="outbound-ai-call")) + .ai( + prompt_text="You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change.", + params={ + "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + "static_greeting_no_barge": True, + }, + ) + .build() +) + call = client.calling.dial( from_="", to="", - swml={ - "version": "1.0.0", - "sections": { - "main": [ - { - "ai": { - "params": { - "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", - "static_greeting_no_barge": True - }, - "prompt": { - "text": "You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change." - } - } - } - ] - } - }, + swml=swml, ) print(call["id"]) ``` @@ -706,7 +695,7 @@ print(call["id"]) // Install: npm install @signalwire/sdk@2.0.5 // This sample also runs as JavaScript: save as outbound-call.mjs, // then run: node outbound-call.mjs -import { RestClient } from "@signalwire/sdk"; +import { RestClient, SwmlBuilder } from "@signalwire/sdk"; const client = new RestClient({ project: "", @@ -714,27 +703,22 @@ const client = new RestClient({ host: ".signalwire.com", }); +const swmlBuilder = new SwmlBuilder(); +swmlBuilder.addVerb("ai", { + params: { + static_greeting: "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + static_greeting_no_barge: true, + }, + prompt: { + text: "You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change.", + }, +}); +const swml = swmlBuilder.build(); + const call = await client.calling.dial({ from: "", to: "", - swml: { - "version": "1.0.0", - "sections": { - "main": [ - { - "ai": { - "params": { - "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", - "static_greeting_no_barge": true - }, - "prompt": { - "text": "You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change." - } - } - } - ] - } - }, + swml, }); console.log(call.id); ``` @@ -898,28 +882,33 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ voicemail = "say:This is Bayview Taxi. Your driver is on the way and will arrive in about five minutes. There is no need to call back." live = "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." +swml = ( + SWMLBuilder(SWMLService(name="outbound-voicemail")) + .detect_machine( + detectors="amd", + detect_message_end=True, + timeout=30, + ) + .switch( + variable="detect_result", + case={ + "machine": [{"play": {"url": voicemail}}], + "human": [{"play": {"url": live}}], + }, + default=[{ + "play": { + "url": "say:Hi, this is Bayview Taxi. Your driver will arrive in about five minutes." + } + }], + ) + .hangup() + .build() +) + call = client.calling.dial( from_="", to="", - swml={ - "version": "1.0.0", - "sections": { - "main": [ - {"detect_machine": {"detectors": "amd", "detect_message_end": True, "timeout": 30}}, - { - "switch": { - "variable": "detect_result", - "case": { - "machine": [{"play": {"url": voicemail}}], - "human": [{"play": {"url": live}}], - }, - "default": [{"play": {"url": "say:Hi, this is Bayview Taxi. Your driver will arrive in about five minutes."}}], - } - }, - {"hangup": {}}, - ] - }, - }, + swml=swml, ) print(call["id"]) ``` @@ -929,28 +918,31 @@ print(call["id"]) const voicemail = "say:This is Bayview Taxi. Your driver is on the way and will arrive in about five minutes. There is no need to call back."; const live = "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."; +const swml = new SwmlBuilder() + .detect_machine({ + detectors: "amd", + detect_message_end: true, + timeout: 30, + }) + .switch({ + variable: "detect_result", + case: { + machine: [{ play: { url: voicemail } }], + human: [{ play: { url: live } }], + }, + default: [{ + play: { + url: "say:Hi, this is Bayview Taxi. Your driver will arrive in about five minutes.", + }, + }], + }) + .hangup() + .build(); + const call = await client.calling.dial({ from: "", to: "", - swml: { - version: "1.0.0", - sections: { - main: [ - { detect_machine: { detectors: "amd", detect_message_end: true, timeout: 30 } }, - { - switch: { - variable: "detect_result", - case: { - machine: [{ play: { url: voicemail } }], - human: [{ play: { url: live } }], - }, - default: [{ play: { url: "say:Hi, this is Bayview Taxi. Your driver will arrive in about five minutes." } }], - }, - }, - { hangup: {} }, - ], - }, - }, + swml, }); console.log(call.id); ``` @@ -1093,7 +1085,7 @@ try { ### Whisper before connecting two people -Play pickup details privately to ``, then connect that call to +Play pickup details privately to ``, then connect that call to ``. #### REST @@ -1119,7 +1111,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ { "connect": { "from": "", - "to": "", + "to": "", "confirm": [ { "play": {"url": "say:Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now."} } ], @@ -1137,25 +1129,22 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ ```python whisper = "say:Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now." +swml = ( + SWMLBuilder(SWMLService(name="outbound-whisper")) + .say("Bayview Taxi here. Connecting you to your driver now.") + .connect(**{ + "from": "", + "to": "", + "confirm": [{"play": {"url": whisper}}], + "confirm_timeout": 20, + }) + .build() +) + call = client.calling.dial( from_="", to="", - swml={ - "version": "1.0.0", - "sections": { - "main": [ - {"play": {"url": "say:Bayview Taxi here. Connecting you to your driver now."}}, - { - "connect": { - "from": "", - "to": "", - "confirm": [{"play": {"url": whisper}}], - "confirm_timeout": 20, - } - }, - ] - }, - }, + swml=swml, ) print(call["id"]) ``` @@ -1164,25 +1153,20 @@ print(call["id"]) ```typescript const whisper = "say:Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now."; +const swml = new SwmlBuilder() + .say("Bayview Taxi here. Connecting you to your driver now.") + .connect({ + from: "", + to: "", + confirm: [{ play: { url: whisper } }], + confirm_timeout: 20, + }) + .build(); + const call = await client.calling.dial({ from: "", to: "", - swml: { - version: "1.0.0", - sections: { - main: [ - { play: { url: "say:Bayview Taxi here. Connecting you to your driver now." } }, - { - connect: { - from: "", - to: "", - confirm: [{ play: { url: whisper } }], - confirm_timeout: 20, - }, - }, - ], - }, - }, + swml, }); console.log(call.id); ``` @@ -1216,7 +1200,7 @@ async def main(): "type": "phone", "params": { "from_number": "", - "to_number": "", + "to_number": "", "timeout": 30, }, }]], @@ -1263,7 +1247,7 @@ try { type: "phone", params: { from_number: "", - to_number: "", + to_number: "", timeout: 30, }, }]]); @@ -1337,52 +1321,44 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ ```python +swml = ( + SWMLBuilder(SWMLService(name="outbound-recording")) + .record_call( + format="mp3", + direction="both", + stereo=True, + beep=True, + status_url="", + ) + .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .build() +) + call = client.calling.dial( from_="", to="", - swml={ - "version": "1.0.0", - "sections": { - "main": [ - { - "record_call": { - "format": "mp3", - "direction": "both", - "stereo": True, - "beep": True, - "status_url": "", - } - }, - {"play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}}, - ] - }, - }, + swml=swml, ) print(call["id"]) ``` ```typescript +const swml = new SwmlBuilder() + .record_call({ + format: "mp3", + direction: "both", + stereo: true, + beep: true, + status_url: "", + }) + .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .build(); + const call = await client.calling.dial({ from: "", to: "", - swml: { - version: "1.0.0", - sections: { - main: [ - { - record_call: { - format: "mp3", - direction: "both", - stereo: true, - beep: true, - status_url: "", - }, - }, - { play: { url: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } }, - ], - }, - }, + swml, }); console.log(call.id); ``` @@ -1536,50 +1512,50 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ ```python +# `stream` is not yet in the builder's bundled schema, so add it as raw SWML. +swml_builder = SWMLBuilder( + SWMLService(name="outbound-stream", schema_validation=False) +) +swml_builder.service.add_verb("stream", { + "url": "", + "track": "both_tracks", + "codec": "PCMU", + "status_url": "", +}) +swml = ( + swml_builder + .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .build() +) + call = client.calling.dial( from_="", to="", - swml={ - "version": "1.0.0", - "sections": { - "main": [ - { - "stream": { - "url": "", - "track": "both_tracks", - "codec": "PCMU", - "status_url": "", - } - }, - {"play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}}, - ] - }, - }, + swml=swml, ) print(call["id"]) ``` ```typescript +// `stream` is not yet in the builder's bundled schema, so add it as raw SWML. +const swmlBuilder = new SwmlBuilder(); +swmlBuilder.setValidation(false); +swmlBuilder.addVerb("stream", { + url: "", + track: "both_tracks", + codec: "PCMU", + status_url: "", +}); +swmlBuilder.setValidation(true); +const swml = swmlBuilder + .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .build(); + const call = await client.calling.dial({ from: "", to: "", - swml: { - version: "1.0.0", - sections: { - main: [ - { - stream: { - url: "", - track: "both_tracks", - codec: "PCMU", - status_url: "", - }, - }, - { play: { url: "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." } }, - ], - }, - }, + swml, }); console.log(call.id); ``` From 0b7779533de0d7514b4397d93e008ae6e0934906 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 16:32:13 -0400 Subject: [PATCH 075/103] docs: use TypeScript SWML AI helper --- .../pages/calling/voice/outbound-calling.mdx | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 4e54c5d087..c806934592 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -703,17 +703,15 @@ const client = new RestClient({ host: ".signalwire.com", }); -const swmlBuilder = new SwmlBuilder(); -swmlBuilder.addVerb("ai", { - params: { - static_greeting: "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", - static_greeting_no_barge: true, - }, - prompt: { - text: "You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change.", - }, -}); -const swml = swmlBuilder.build(); +const swml = new SwmlBuilder() + .ai({ + prompt: "You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change.", + params: { + static_greeting: "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + static_greeting_no_barge: true, + }, + }) + .build(); const call = await client.calling.dial({ from: "", From 5ed31fcf79da05af4f6277b0b1e559bf9b13b5c5 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 17:13:42 -0400 Subject: [PATCH 076/103] docs: preview union call response schema --- fern/products/platform/pages/calling/voice/outbound-calling.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index c806934592..f2ddea58cc 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -180,7 +180,7 @@ console.log(call.id); The REST API returns a call `id` and status `queued`. This confirms that SignalWire accepted the request; the call hasn't necessarily rung or been answered yet. Save the `id` to identify this call. - + From 56a2da2c826f0ac37b051a83443b32d68b37a82a Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 17:27:10 -0400 Subject: [PATCH 077/103] docs: restore call leg response schema --- fern/products/platform/pages/calling/voice/outbound-calling.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index f2ddea58cc..c806934592 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -180,7 +180,7 @@ console.log(call.id); The REST API returns a call `id` and status `queued`. This confirms that SignalWire accepted the request; the call hasn't necessarily rung or been answered yet. Save the `id` to identify this call. - + From e79fe939fabf295f17272693b499e98f27dfa792 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 17:28:49 -0400 Subject: [PATCH 078/103] docs: compare call response snippet rendering --- fern/products/platform/pages/calling/voice/outbound-calling.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index c806934592..b237a941fb 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -182,6 +182,8 @@ request; the call hasn't necessarily rung or been answered yet. Save the `id` to + + From e9133a28754ce2a0240d6e3e5b72ba4d237ae187 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 17:32:56 -0400 Subject: [PATCH 079/103] docs: use endpoint call response snippet --- fern/products/platform/pages/calling/voice/outbound-calling.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index b237a941fb..d74d92751e 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -180,8 +180,6 @@ console.log(call.id); The REST API returns a call `id` and status `queued`. This confirms that SignalWire accepted the request; the call hasn't necessarily rung or been answered yet. Save the `id` to identify this call. - - From c198c745f4d6726ada403e256643304965acd4a8 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 17:38:35 -0400 Subject: [PATCH 080/103] docs: simplify outbound example messages --- .../pages/calling/voice/outbound-calling.mdx | 148 +++++++++--------- 1 file changed, 73 insertions(+), 75 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index d74d92751e..9a8c6d95d9 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -115,7 +115,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ "version": "1.0.0", "sections": { "main": [ - { "play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} } + { "play": {"url": "say:Hello, welcome to SignalWire!"} } ] } } @@ -138,7 +138,7 @@ client = RestClient( swml = ( SWMLBuilder(SWMLService(name="outbound-call")) - .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .say("Hello, welcome to SignalWire!") .build() ) @@ -164,7 +164,7 @@ const client = new RestClient({ }); const swml = new SwmlBuilder() - .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .say("Hello, welcome to SignalWire!") .build(); const call = await client.calling.dial({ @@ -221,7 +221,7 @@ async def main(): await call.play([{ "type": "tts", - "params": {"text": "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}, + "params": {"text": "Hello, welcome to SignalWire!"}, }], on_completed=hang_up_after_playback) await call.wait_for_ended() @@ -254,7 +254,7 @@ try { }, }]]); await call.play([ - { type: "tts", text: "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }, + { type: "tts", text: "Hello, welcome to SignalWire!" }, ], { onCompleted: async () => { if (call.state !== "ended") await call.hangup(); @@ -319,8 +319,8 @@ For server dialing options, see the ### Answer the call -Answer the destination phone. With a server example, you should hear “Hi, this is Bayview Taxi” -followed by the driver arrival message, then the call ends. With the browser example, allow +Answer the destination phone. With a server example, you should hear “Hello, welcome to +SignalWire!”, then the call ends. With the browser example, allow microphone access to speak on the call and select **Hang up** when you finish.
@@ -360,7 +360,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ "version": "1.0.0", "sections": { "main": [ - { "play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} } + { "play": {"url": "say:Hello, welcome to SignalWire!"} } ] } }, @@ -374,7 +374,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ ```python swml = ( SWMLBuilder(SWMLService(name="outbound-call")) - .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .say("Hello, welcome to SignalWire!") .build() ) @@ -390,7 +390,7 @@ call = client.calling.dial( ```typescript const swml = new SwmlBuilder() - .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .say("Hello, welcome to SignalWire!") .build(); const call = await client.calling.dial({ @@ -479,7 +479,7 @@ async def main(): await call.play([{ "type": "tts", - "params": {"text": "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}, + "params": {"text": "Hello, welcome to SignalWire!"}, }], on_completed=hang_up_after_playback) await call.wait_for_ended() @@ -515,7 +515,7 @@ try { console.log(`State: ${event.params.call_state}, reason: ${event.params.end_reason ?? ""}`); }); await call.play([ - { type: "tts", text: "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }, + { type: "tts", text: "Hello, welcome to SignalWire!" }, ], { onCompleted: async () => { if (call.state !== "ended") await call.hangup(); @@ -613,7 +613,7 @@ sequenceDiagram ### Run an AI agent -Call a rider with an AI agent that shares their arrival time and answers follow-up questions. +Start an AI agent that welcomes the person and answers basic questions about SignalWire. Before dialing, follow consent, do-not-call, and calling-hour requirements for artificial voices. @@ -642,11 +642,11 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ { "ai": { "params": { - "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + "static_greeting": "Hello, welcome to SignalWire! This call uses an artificial voice.", "static_greeting_no_barge": true }, "prompt": { - "text": "You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change." + "text": "Welcome the caller to SignalWire. Briefly explain that SignalWire provides APIs and SDKs for voice, messaging, video, and AI. Answer basic follow-up questions. If you are unsure, direct the caller to signalwire.com." } } } @@ -673,9 +673,9 @@ client = RestClient( swml = ( SWMLBuilder(SWMLService(name="outbound-ai-call")) .ai( - prompt_text="You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change.", + prompt_text="Welcome the caller to SignalWire. Briefly explain that SignalWire provides APIs and SDKs for voice, messaging, video, and AI. Answer basic follow-up questions. If you are unsure, direct the caller to signalwire.com.", params={ - "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + "static_greeting": "Hello, welcome to SignalWire! This call uses an artificial voice.", "static_greeting_no_barge": True, }, ) @@ -705,9 +705,9 @@ const client = new RestClient({ const swml = new SwmlBuilder() .ai({ - prompt: "You are calling to tell the rider their driver is about five minutes away. Answer questions using that estimate. You cannot change bookings or contact preferences. If asked, explain that the rider needs to contact Bayview Taxi to make those changes. Do not claim to have made a change.", + prompt: "Welcome the caller to SignalWire. Briefly explain that SignalWire provides APIs and SDKs for voice, messaging, video, and AI. Answer basic follow-up questions. If you are unsure, direct the caller to signalwire.com.", params: { - static_greeting: "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + static_greeting: "Hello, welcome to SignalWire! This call uses an artificial voice.", static_greeting_no_barge: true, }, }) @@ -756,14 +756,13 @@ async def main(): ) await call.ai( ai_params={ - "static_greeting": "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + "static_greeting": "Hello, welcome to SignalWire! This call uses an artificial voice.", "static_greeting_no_barge": True, }, prompt={ - "text": """You are calling to tell the rider their driver is about five minutes away. -Answer questions using that estimate. You cannot change bookings or -contact preferences. If asked, explain that the rider needs to contact -Bayview Taxi to make those changes. Do not claim to have made a change.""" + "text": """Welcome the caller to SignalWire. Briefly explain that SignalWire +provides APIs and SDKs for voice, messaging, video, and AI. Answer basic +follow-up questions. If you are unsure, direct the caller to signalwire.com.""" }, ) # Keep the connection open until the destination hangs up. @@ -799,14 +798,13 @@ try { }]]); await call.ai({ aiParams: { - static_greeting: "Hello, this is an automated assistant calling from Bayview Taxi about your pickup. This call uses an artificial voice.", + static_greeting: "Hello, welcome to SignalWire! This call uses an artificial voice.", static_greeting_no_barge: true, }, prompt: { - text: `You are calling to tell the rider their driver is about five minutes away. - Answer questions using that estimate. You cannot change bookings or - contact preferences. If asked, explain that the rider needs to contact - Bayview Taxi to make those changes. Do not claim to have made a change.`, + text: `Welcome the caller to SignalWire. Briefly explain that SignalWire + provides APIs and SDKs for voice, messaging, video, and AI. Answer basic + follow-up questions. If you are unsure, direct the caller to signalwire.com.`, }, }); // Keep the connection open until the destination hangs up. @@ -856,14 +854,14 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ "variable": "detect_result", "case": { "machine": [ - { "play": {"url": "say:This is Bayview Taxi. Your driver is on the way and will arrive in about five minutes. There is no need to call back."} } + { "play": {"url": "say:Hello, welcome to SignalWire! Visit signalwire.com to learn more."} } ], "human": [ - { "play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} } + { "play": {"url": "say:Hello, welcome to SignalWire!"} } ] }, "default": [ - { "play": {"url": "say:Hi, this is Bayview Taxi. Your driver will arrive in about five minutes."} } + { "play": {"url": "say:Hello, welcome to SignalWire!"} } ] } }, @@ -877,8 +875,8 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ ```python -voicemail = "say:This is Bayview Taxi. Your driver is on the way and will arrive in about five minutes. There is no need to call back." -live = "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." +voicemail = "say:Hello, welcome to SignalWire! Visit signalwire.com to learn more." +live = "say:Hello, welcome to SignalWire!" swml = ( SWMLBuilder(SWMLService(name="outbound-voicemail")) @@ -895,7 +893,7 @@ swml = ( }, default=[{ "play": { - "url": "say:Hi, this is Bayview Taxi. Your driver will arrive in about five minutes." + "url": "say:Hello, welcome to SignalWire!" } }], ) @@ -913,8 +911,8 @@ print(call["id"]) ```typescript -const voicemail = "say:This is Bayview Taxi. Your driver is on the way and will arrive in about five minutes. There is no need to call back."; -const live = "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."; +const voicemail = "say:Hello, welcome to SignalWire! Visit signalwire.com to learn more."; +const live = "say:Hello, welcome to SignalWire!"; const swml = new SwmlBuilder() .detect_machine({ @@ -930,7 +928,7 @@ const swml = new SwmlBuilder() }, default: [{ play: { - url: "say:Hi, this is Bayview Taxi. Your driver will arrive in about five minutes.", + url: "say:Hello, welcome to SignalWire!", }, }], }) @@ -961,8 +959,8 @@ import asyncio from signalwire.relay import RelayClient from signalwire.relay.event import DetectEvent -VOICEMAIL = "This is Bayview Taxi. Your driver is on the way and will arrive in about five minutes. There is no need to call back." -LIVE = "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." +VOICEMAIL = "Hello, welcome to SignalWire! Visit signalwire.com to learn more." +LIVE = "Hello, welcome to SignalWire!" client = RelayClient( project="", @@ -1026,8 +1024,8 @@ asyncio.run(main()) // Save as outbound-call.mts and run: npx tsx outbound-call.mts import { RelayClient, DetectEvent, RelayEvent } from "@signalwire/sdk"; -const VOICEMAIL = "This is Bayview Taxi. Your driver is on the way and will arrive in about five minutes. There is no need to call back."; -const LIVE = "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."; +const VOICEMAIL = "Hello, welcome to SignalWire! Visit signalwire.com to learn more."; +const LIVE = "Hello, welcome to SignalWire!"; const client = new RelayClient({ project: "", @@ -1083,12 +1081,12 @@ try { ### Whisper before connecting two people -Play pickup details privately to ``, then connect that call to +Play a private message to ``, then connect that call to ``. #### REST -Use `connect.confirm` to play the [whisper][call-whisper] to the driver before bridging the calls. +Use `connect.confirm` to play the [whisper][call-whisper] to the agent before bridging the calls. @@ -1105,13 +1103,13 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ "version": "1.0.0", "sections": { "main": [ - { "play": {"url": "say:Bayview Taxi here. Connecting you to your driver now."} }, + { "play": {"url": "say:Hello, welcome to SignalWire!"} }, { "connect": { "from": "", - "to": "", + "to": "", "confirm": [ - { "play": {"url": "say:Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now."} } + { "play": {"url": "say:You are about to be connected to the caller."} } ], "confirm_timeout": 20 } @@ -1125,14 +1123,14 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ ```python -whisper = "say:Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now." +whisper = "say:You are about to be connected to the caller." swml = ( SWMLBuilder(SWMLService(name="outbound-whisper")) - .say("Bayview Taxi here. Connecting you to your driver now.") + .say("Hello, welcome to SignalWire!") .connect(**{ "from": "", - "to": "", + "to": "", "confirm": [{"play": {"url": whisper}}], "confirm_timeout": 20, }) @@ -1149,13 +1147,13 @@ print(call["id"]) ```typescript -const whisper = "say:Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now."; +const whisper = "say:You are about to be connected to the caller."; const swml = new SwmlBuilder() - .say("Bayview Taxi here. Connecting you to your driver now.") + .say("Hello, welcome to SignalWire!") .connect({ from: "", - to: "", + to: "", confirm: [{ play: { url: whisper } }], confirm_timeout: 20, }) @@ -1173,7 +1171,7 @@ console.log(call.id); #### WebSocket (Relay) -Dial the driver first, play the whisper, then connect the rider after playback finishes. +Dial the agent first, play the whisper, then connect the caller after playback finishes. @@ -1192,18 +1190,18 @@ client = RelayClient( async def main(): async with client: - # Call the driver first: only this leg hears the whisper. + # Call the agent first: only this leg hears the whisper. call = await client.dial( devices=[[{ "type": "phone", "params": { "from_number": "", - "to_number": "", + "to_number": "", "timeout": 30, }, }]], ) - async def connect_the_rider(_event): + async def connect_the_caller(_event): if call.state != "ended": await call.connect([[{ "type": "phone", @@ -1215,8 +1213,8 @@ async def main(): }]]) await call.play( - [{"type": "tts", "params": {"text": "Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now."}}], - on_completed=connect_the_rider, + [{"type": "tts", "params": {"text": "You are about to be connected to the caller."}}], + on_completed=connect_the_caller, ) await call.wait_for_ended() @@ -1240,17 +1238,17 @@ const client = new RelayClient({ await client.connect(); try { - // Call the driver first: only this leg hears the whisper. + // Call the agent first: only this leg hears the whisper. const call = await client.dial([[{ type: "phone", params: { from_number: "", - to_number: "", + to_number: "", timeout: 30, }, }]]); await call.play( - [{ type: "tts", text: "Bayview Taxi dispatch. Pickup for Alex at 5 Main Street. Connecting the rider now." }], + [{ type: "tts", text: "You are about to be connected to the caller." }], { onCompleted: async () => { if (call.state === "ended") return; @@ -1309,7 +1307,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ "status_url": "" } }, - { "play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} } + { "play": {"url": "say:Hello, welcome to SignalWire!"} } ] } } @@ -1328,7 +1326,7 @@ swml = ( beep=True, status_url="", ) - .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .say("Hello, welcome to SignalWire!") .build() ) @@ -1350,7 +1348,7 @@ const swml = new SwmlBuilder() beep: true, status_url: "", }) - .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .say("Hello, welcome to SignalWire!") .build(); const call = await client.calling.dial({ @@ -1409,7 +1407,7 @@ async def main(): await call.hangup() await call.play( - [{"type": "tts", "params": {"text": "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}}], + [{"type": "tts", "params": {"text": "Hello, welcome to SignalWire!"}}], on_completed=hang_up_after_playback, ) await call.wait_for_ended() @@ -1452,7 +1450,7 @@ try { end_silence_timeout: 0, }); await call.play( - [{ type: "tts", text: "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }], + [{ type: "tts", text: "Hello, welcome to SignalWire!" }], { onCompleted: async () => { if (call.state !== "ended") await call.hangup(); @@ -1500,7 +1498,7 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ "status_url": "" } }, - { "play": {"url": "say:Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."} } + { "play": {"url": "say:Hello, welcome to SignalWire!"} } ] } } @@ -1522,7 +1520,7 @@ swml_builder.service.add_verb("stream", { }) swml = ( swml_builder - .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .say("Hello, welcome to SignalWire!") .build() ) @@ -1547,7 +1545,7 @@ swmlBuilder.addVerb("stream", { }); swmlBuilder.setValidation(true); const swml = swmlBuilder - .say("Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes.") + .say("Hello, welcome to SignalWire!") .build(); const call = await client.calling.dial({ @@ -1595,7 +1593,7 @@ async def main(): url="", track="both_tracks", codec="PCMU", - custom_parameters={"ride_id": "ride-4821"}, + custom_parameters={"session_id": ""}, ) print(f"Streaming audio, control ID {stream.control_id}") @@ -1604,7 +1602,7 @@ async def main(): await call.hangup() await call.play( - [{"type": "tts", "params": {"text": "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes."}}], + [{"type": "tts", "params": {"text": "Hello, welcome to SignalWire!"}}], on_completed=hang_up_after_playback, ) await call.wait_for_ended() @@ -1640,11 +1638,11 @@ try { const stream = await call.stream("", { track: "both_tracks", codec: "PCMU", - customParameters: { ride_id: "ride-4821" }, + customParameters: { session_id: "" }, }); console.log(`Streaming audio, control ID ${stream.controlId}`); await call.play( - [{ type: "tts", text: "Hi, this is Bayview Taxi. Your driver is on the way and will arrive in about five minutes." }], + [{ type: "tts", text: "Hello, welcome to SignalWire!" }], { onCompleted: async () => { if (call.state !== "ended") await call.hangup(); @@ -1671,7 +1669,7 @@ Serve the page over HTTPS or `localhost` so the browser can access the microphon - Call Bayview Taxi + Call with SignalWire

Idle

From f3cd7a4efd2fcbd350d6fb0be37690bc9351e24b Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 17:44:48 -0400 Subject: [PATCH 081/103] docs: link Relay event listener guide --- .../platform/pages/calling/voice/outbound-calling.mdx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 9a8c6d95d9..ecf03783a7 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -576,9 +576,8 @@ callButton.onclick = async () => {
-For more Relay event handlers, see the -[Python events reference](/docs/server-sdks/reference/python/relay/events) or -[TypeScript events reference](/docs/server-sdks/reference/typescript/relay/events). +For more Relay event handlers, see +[Event listeners in the Relay client guide](https://signalwire.com/docs/server-sdks/guides/relay-client#event-listeners). For Browser SDK call states, see the [outbound calls guide](/docs/browser-sdk/v4/guides/outbound-calls). From a61c5ceeb84dc5fed4ed8729369077a839069844 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 17:47:27 -0400 Subject: [PATCH 082/103] docs: trim REST snippet instructions --- .../products/platform/pages/calling/voice/outbound-calling.mdx | 3 --- 1 file changed, 3 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index ecf03783a7..126589666d 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -339,9 +339,6 @@ for `ringing`, `answered`, and `ended`. Before running the request, replace `` with a webhook endpoint you control that SignalWire can reach. Your endpoint receives HTTP POST requests as the call reaches the selected states. See the [webhooks guide][webhooks] for endpoint setup and local testing. -For the SDK snippets, keep the imports and `client` setup from your first REST example, and -replace its `dial` call with the code below. - `status_events` accepts `created`, `ringing`, `answered`, and `ended`; if omitted, it defaults to `ended`. The `ended` payload includes an `end_reason` so you can tell how the call finished. From 5de0ae9c95c0d6f37863577f90f6c16e08b7e053 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 17:53:16 -0400 Subject: [PATCH 083/103] docs: clarify server and browser calling --- .../platform/pages/calling/voice/outbound-calling.mdx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 126589666d..0c6a2c723a 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -185,8 +185,8 @@ request; the call hasn't necessarily rung or been answered yet. Save the `id` to -Use a Server SDK to play the announcement, or the Browser SDK to let a user speak on the call -from your web app. +Use a Server SDK when controlling a call flow from the server, or the Browser SDK when placing a +call from a web app. @@ -308,11 +308,8 @@ callButton.onclick = async () => { -For browser setup and a complete web page, see the +For setup and a complete web page, see the [Browser SDK outbound calls guide](/docs/browser-sdk/v4/guides/outbound-calls). -For server dialing options, see the -[Python dial reference](/docs/server-sdks/reference/python/relay/client/dial) or -[TypeScript dial reference](/docs/server-sdks/reference/typescript/relay/client/dial). From bcc496e543b8571977e897ee8a2baaf684ea07d8 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 17:57:57 -0400 Subject: [PATCH 084/103] docs: simplify outbound destinations --- .../pages/calling/voice/outbound-calling.mdx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 0c6a2c723a..a3c0bc8abe 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -19,8 +19,9 @@ max-toc-depth: 3 [swml-stream]: /docs/swml/reference/calling/stream [call-whisper]: /docs/swml/guides/call-whisper [guest-token]: /docs/apis/rest/subscribers/tokens/create-subscriber-guest-token -[resource-addresses]: /docs/platform/addresses [resources]: /docs/platform/resources +[sip-credentials]: /docs/platform/voice/sip/sip-credentials +[web-clients]: /docs/platform/subscribers Place an outbound phone call with SignalWire and choose what happens when the destination answers. Start by calling your own phone and playing a short announcement, then track the call's progress, @@ -72,21 +73,17 @@ Replace these values in the code sample you choose: ### Choose a destination -A call can reach a phone, a SIP endpoint, or a Resource in your SignalWire Space. +A call can reach a phone, a [SIP destination][sip-credentials], a [Web Client][web-clients], or +another [Resource][resources] in your SignalWire Space. | Destination | Dial | Example | |---|---|---| | Phone | Its number in E.164 format | `` | -| SIP endpoint | Its SIP URI | `sip:support@example.com` | -| Web client | Its subscriber resource address | `/private/support-rep` | +| SIP destination | Its SIP URI | `sip:support@example.com` | +| Web Client | Its resource address | `/private/support-rep` | | Application | Its resource address | `/public/support-agent` | | Conference | Its resource address | `/public/team-standup` | -Subscribers, applications such as AI agents, -SWML scripts, or Call Flows, and conferences are [Resources][resources]. Each has a -[resource address][resource-addresses] in the form `/context/name`, where the context is `public` -or `private`. - For this walkthrough, choose a device you can answer and replace `` with its address. From 00de3b644447e2222f47ae4fe6a8d702a90cdf49 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 18:02:34 -0400 Subject: [PATCH 085/103] docs: label browser example files --- .../platform/pages/calling/voice/outbound-calling.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index a3c0bc8abe..acdbef72dc 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -1653,7 +1653,7 @@ Place a WebRTC call from a web page using a restricted [guest token][guest-token Serve the page over HTTPS or `localhost` so the browser can access the microphone. - + ```html @@ -1671,7 +1671,7 @@ Serve the page over HTTPS or `localhost` so the browser can access the microphon ``` - + ```javascript // Install: npm install @signalwire/js@4.0.0-rc.2 rxjs@7.8.2 // Save as call.js next to the page above. From 62b51c49a444886658bbac15b26f1b97337f70e2 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 18:05:05 -0400 Subject: [PATCH 086/103] docs: deduplicate browser guide links --- .../platform/pages/calling/voice/outbound-calling.mdx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index acdbef72dc..9e03ef2a22 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -305,9 +305,6 @@ callButton.onclick = async () => { -For setup and a complete web page, see the -[Browser SDK outbound calls guide](/docs/browser-sdk/v4/guides/outbound-calls). - @@ -569,8 +566,6 @@ callButton.onclick = async () => { For more Relay event handlers, see [Event listeners in the Relay client guide](https://signalwire.com/docs/server-sdks/guides/relay-client#event-listeners). -For Browser SDK call states, see the -[outbound calls guide](/docs/browser-sdk/v4/guides/outbound-calls). For Relay, this flow shows the commands your code sends and the events SignalWire returns over the same persistent connection: From dc292a2bb2f13f72f4cfe04727f39dce4c1c6c03 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 18:06:13 -0400 Subject: [PATCH 087/103] docs: clarify REST and WebSocket SDK options --- .../products/platform/pages/calling/voice/outbound-calling.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 9e03ef2a22..044422de75 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -89,7 +89,8 @@ For this walkthrough, choose a device you can answer and replace ### Place the call -REST requests run on your server. For WebSocket calling, choose a Server SDK or the Browser SDK. +Use the REST Calling API with any server-side HTTP client or a SignalWire Server SDK. For +WebSocket calling, use a Server SDK or the Browser SDK. From 6581613098670dc0019e8fd2bb553ea161bb0e78 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 18:08:25 -0400 Subject: [PATCH 088/103] docs: add browser token prerequisite --- .../platform/pages/calling/voice/outbound-calling.mdx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 044422de75..815a8d5022 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -18,6 +18,7 @@ max-toc-depth: 3 [swml-record-call]: /docs/swml/reference/calling/record-call [swml-stream]: /docs/swml/reference/calling/stream [call-whisper]: /docs/swml/guides/call-whisper +[subscriber-token]: /docs/apis/rest/subscribers/tokens/create-subscriber-token [guest-token]: /docs/apis/rest/subscribers/tokens/create-subscriber-guest-token [resources]: /docs/platform/resources [sip-credentials]: /docs/platform/voice/sip/sip-credentials @@ -39,13 +40,16 @@ If you're adding outbound calling to an existing application, choose the approac ## Prepare for your first call -For the server walkthrough, have these values ready: +Have these values ready: - Your Space URL, such as `.signalwire.com`. - Your Project ID and API token from the Dashboard's [API credentials][api-credentials] page. Enable the token's **Voice** permission for the Calling API. - If calling a phone number, a voice-capable [phone number purchased in your Space][phone-numbers] or a [verified caller ID][caller-id]. +- If calling from a Web Client, a [Subscriber Access Token][subscriber-token] for a known subscriber + or a restricted [guest token][guest-token], minted by your server using your project API credentials + and then supplied to the Browser SDK client. - A destination device you can answer. From ddbcaf09fe04a4997cc44ac62f10e18a843047be Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 18:10:08 -0400 Subject: [PATCH 089/103] docs: simplify browser token prerequisite --- .../platform/pages/calling/voice/outbound-calling.mdx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 815a8d5022..87a9242c26 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -47,9 +47,8 @@ Have these values ready: Enable the token's **Voice** permission for the Calling API. - If calling a phone number, a voice-capable [phone number purchased in your Space][phone-numbers] or a [verified caller ID][caller-id]. -- If calling from a Web Client, a [Subscriber Access Token][subscriber-token] for a known subscriber - or a restricted [guest token][guest-token], minted by your server using your project API credentials - and then supplied to the Browser SDK client. +- If calling from a Web Client, you need a [Subscriber token][subscriber-token] or + [guest token][guest-token]. - A destination device you can answer. From 8cd431b2f7d95968a5071608371b0aaa3db30cd0 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 10 Sep 2026 18:11:07 -0400 Subject: [PATCH 090/103] docs: clarify browser token handoff --- fern/products/platform/pages/calling/voice/outbound-calling.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 87a9242c26..e0c009581c 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -48,7 +48,7 @@ Have these values ready: - If calling a phone number, a voice-capable [phone number purchased in your Space][phone-numbers] or a [verified caller ID][caller-id]. - If calling from a Web Client, you need a [Subscriber token][subscriber-token] or - [guest token][guest-token]. + [guest token][guest-token] to provide to the Browser SDK client. - A destination device you can answer. From dffe72b9d252d4af08608eb04186ae33e1fbd7d8 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Fri, 11 Sep 2026 09:06:57 -0400 Subject: [PATCH 091/103] docs: move calling approach into walkthrough --- .../pages/calling/voice/outbound-calling.mdx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index e0c009581c..86a04cef09 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -28,16 +28,6 @@ Place an outbound phone call with SignalWire and choose what happens when the de Start by calling your own phone and playing a short announcement, then track the call's progress, run an AI agent, or let users call from your web app. -## Choose how to place your call - -If you're adding outbound calling to an existing application, choose the approach that fits how you want to control the call. - -| What you want to do | Where to start | -|---|---| -| Have your server start a call with instructions to run when it's answered | [REST Calling API](#place-the-call), using cURL or a Server SDK | -| Send commands and receive call events over a persistent connection | [WebSocket (Relay)](#place-the-call), using a Server SDK | -| Let someone place and speak on a call from your web app | [Browser SDK](#place-the-call) | - ## Prepare for your first call Have these values ready: @@ -63,6 +53,16 @@ Call a phone you can answer and play a short announcement. +### Choose how to place your call + +Choose the approach that fits how you want to control the call. + +| What you want to do | Where to start | +|---|---| +| Have your server start a call with instructions to run when it's answered | [REST Calling API](#place-the-call), using cURL or a Server SDK | +| Send commands and receive call events over a persistent connection | [WebSocket (Relay)](#place-the-call), using a Server SDK | +| Let someone place and speak on a call from your web app | [Browser SDK](#place-the-call) | + ### Set your credentials and caller ID Replace these values in the code sample you choose: From d1492731cc3ac7d2f63f034b7860f57b52a1a007 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Fri, 11 Sep 2026 09:12:00 -0400 Subject: [PATCH 092/103] docs: give outbound approach headings task context --- .../pages/calling/voice/outbound-calling.mdx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 86a04cef09..617db48efd 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -325,7 +325,7 @@ microphone access to speak on the call and select **Hang up** when you finish. Use callbacks with REST or event handlers with Relay to follow what happens after you dial. Follow the section for the approach you used for your first call. -### REST +### Track call progress via REST Add `status_url` and `status_events` to your first request to receive call progress callbacks. Keep the inline `swml` announcement. The examples below place another call with notifications @@ -425,7 +425,7 @@ sequenceDiagram -### WebSocket +### Track call progress via WebSocket Relay exposes call state events through `call.on()`. Because `dial()` returns after the destination answers, the handler observes later states such as `ending` and `ended`. @@ -609,7 +609,7 @@ Before dialing, follow consent, do-not-call, and calling-hour requirements for a See the [TCPA guide][tcpa] and [AI best practices][ai-best-practices]. -#### REST +#### Run an AI agent via REST Send a `dial` request with an inline SWML [`ai` instruction][swml-ai] that starts when the call is answered. @@ -712,7 +712,7 @@ console.log(call.id);
-#### WebSocket (Relay) +#### Run an AI agent via WebSocket (Relay) Dial with Relay, start the agent with `call.ai()`, and keep the connection open until the call ends. @@ -812,7 +812,7 @@ voicemail greeting and beep. Follow the consent and calling-hour requirements in the [TCPA guide][tcpa]. -#### REST +#### Leave a voicemail via REST Use [`detect_machine`][swml-detect-machine] and `switch` to choose the live or voicemail message. @@ -934,7 +934,7 @@ console.log(call.id);
-#### WebSocket (Relay) +#### Leave a voicemail via WebSocket (Relay) Use `call.detect()` to play the live message after `HUMAN` or `UNKNOWN`, or wait for `READY` after a machine greeting. @@ -1073,7 +1073,7 @@ try { Play a private message to ``, then connect that call to ``. -#### REST +#### Play a whisper via REST Use `connect.confirm` to play the [whisper][call-whisper] to the agent before bridging the calls. @@ -1158,7 +1158,7 @@ console.log(call.id); -#### WebSocket (Relay) +#### Play a whisper via WebSocket (Relay) Dial the agent first, play the whisper, then connect the caller after playback finishes. @@ -1268,7 +1268,7 @@ Record both sides of an outbound call and retrieve the finished recording URL. Confirm which parties must consent and announce the recording when required.
-#### REST +#### Record the call via REST Start [`record_call`][swml-record-call] in the background and receive the result at `status_url`. @@ -1350,7 +1350,7 @@ console.log(call.id); -#### WebSocket (Relay) +#### Record the call via WebSocket (Relay) Start `call.record()` and read the recording URL from its `finished` event. @@ -1460,7 +1460,7 @@ try { Stream both sides of a live call to your secure WebSocket endpoint for real-time processing. -#### REST +#### Stream call audio via REST Start [`stream`][swml-stream] in the background and send status events to your webhook. @@ -1547,7 +1547,7 @@ console.log(call.id); -#### WebSocket (Relay) +#### Stream call audio via WebSocket (Relay) Start `call.stream()` over the Relay connection and keep it running until the call ends. From ec9429a6073317ac421c6991ff3e7d85f76984d7 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Fri, 11 Sep 2026 09:20:39 -0400 Subject: [PATCH 093/103] docs: show example phone destination in E.164 format --- fern/products/platform/pages/calling/voice/outbound-calling.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 617db48efd..c402fa5447 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -81,7 +81,7 @@ another [Resource][resources] in your SignalWire Space. | Destination | Dial | Example | |---|---|---| -| Phone | Its number in E.164 format | `` | +| Phone | Its number in E.164 format | `+12025550123` | | SIP destination | Its SIP URI | `sip:support@example.com` | | Web Client | Its resource address | `/private/support-rep` | | Application | Its resource address | `/public/support-agent` | From 56ffacc15a1d98a4c777d305d4a5eff93d2b3f33 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Fri, 11 Sep 2026 09:22:29 -0400 Subject: [PATCH 094/103] docs: link phone destination format to E.164 guide --- fern/products/platform/pages/calling/voice/outbound-calling.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index c402fa5447..7a21feee93 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -81,7 +81,7 @@ another [Resource][resources] in your SignalWire Space. | Destination | Dial | Example | |---|---|---| -| Phone | Its number in E.164 format | `+12025550123` | +| Phone | Its number in [E.164 format](/docs/platform/what-is-e164) | `+12025550123` | | SIP destination | Its SIP URI | `sip:support@example.com` | | Web Client | Its resource address | `/private/support-rep` | | Application | Its resource address | `/public/support-agent` | From 3ac522736f6e7f316a692464ce931965b6ed32ea Mon Sep 17 00:00:00 2001 From: Devon-White Date: Fri, 11 Sep 2026 09:56:24 -0400 Subject: [PATCH 095/103] docs: clarify when to use REST and WebSocket calling --- .../platform/pages/calling/voice/outbound-calling.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 7a21feee93..013f270391 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -59,8 +59,8 @@ Choose the approach that fits how you want to control the call. | What you want to do | Where to start | |---|---| -| Have your server start a call with instructions to run when it's answered | [REST Calling API](#place-the-call), using cURL or a Server SDK | -| Send commands and receive call events over a persistent connection | [WebSocket (Relay)](#place-the-call), using a Server SDK | +| Give SignalWire call instructions over HTTP, supplied inline or returned by your webhook | [REST Calling API](#place-the-call), using cURL or a Server SDK | +| Control the call in real time, asynchronously receiving events and sending commands over a persistent WebSocket connection | [WebSocket (Relay)](#place-the-call), using a Server SDK | | Let someone place and speak on a call from your web app | [Browser SDK](#place-the-call) | ### Set your credentials and caller ID From b7e30fda07951f9531b48c6d1a501d8917a2b5dd Mon Sep 17 00:00:00 2001 From: Devon-White Date: Fri, 11 Sep 2026 11:01:09 -0400 Subject: [PATCH 096/103] docs: compare generated outbound request snippet in a tab --- fern/apis/signalwire-rest/openapi.yaml | 14 +++++++++++ .../pages/calling/voice/outbound-calling.mdx | 23 ++++++++++++------- .../calling-api/calls/main.tsp | 7 ++++++ .../calling-api/calls/models/examples.tsp | 16 +++++++++++++ 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/fern/apis/signalwire-rest/openapi.yaml b/fern/apis/signalwire-rest/openapi.yaml index 4270718728..9670610cd7 100644 --- a/fern/apis/signalwire-rest/openapi.yaml +++ b/fern/apis/signalwire-rest/openapi.yaml @@ -619,6 +619,20 @@ paths: from: '+15551234567' to: '+15557654321' url: https://example.com/swml + Outbound announcement: + summary: Outbound announcement + description: Dial a destination and play a greeting using inline SWML + value: + command: dial + params: + from: + to: + swml: + version: 1.0.0 + sections: + main: + - play: + url: say:Hello, welcome to SignalWire! update: summary: update description: Modify an existing call's parameters in real-time diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 013f270391..4f8af929aa 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -101,8 +101,8 @@ WebSocket calling, use a Server SDK or the Browser SDK. The request includes a SignalWire Markup Language (SWML) document in `swml`. SignalWire runs it when the destination answers, playing the announcement below. - - + + ```bash curl -X POST "https://.signalwire.com/api/calling/calls" \ -u ":" \ @@ -123,8 +123,8 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ } }' ``` - - + + ```python # Install: python -m pip install signalwire-sdk==3.4.1 # Save as outbound_call.py and run: python outbound_call.py @@ -150,8 +150,8 @@ call = client.calling.dial( ) print(call["id"]) ``` - - + + ```typescript // Install: npm install @signalwire/sdk@2.0.5 // This sample also runs as JavaScript: save as outbound-call.mjs, @@ -175,8 +175,15 @@ const call = await client.calling.dial({ }); console.log(call.id); ``` - - + + + + + The REST API returns a call `id` and status `queued`. This confirms that SignalWire accepted the request; the call hasn't necessarily rung or been answered yet. Save the `id` to identify this call. diff --git a/specs/signalwire-rest/calling-api/calls/main.tsp b/specs/signalwire-rest/calling-api/calls/main.tsp index 9213c65103..58d0b9b559 100644 --- a/specs/signalwire-rest/calling-api/calls/main.tsp +++ b/specs/signalwire-rest/calling-api/calls/main.tsp @@ -354,6 +354,13 @@ namespace SignalWireAPI.Calling.Calls { description: "Modify an existing call's parameters in real-time", } ) + @opExample( + #{ parameters: outboundAnnouncementExample }, + #{ + title: "Outbound announcement", + description: "Dial a destination and play a greeting using inline SWML", + } + ) @opExample( #{ parameters: dialCallExample }, #{ diff --git a/specs/signalwire-rest/calling-api/calls/models/examples.tsp b/specs/signalwire-rest/calling-api/calls/models/examples.tsp index 31898c25ae..9ceabc0626 100644 --- a/specs/signalwire-rest/calling-api/calls/models/examples.tsp +++ b/specs/signalwire-rest/calling-api/calls/models/examples.tsp @@ -21,6 +21,22 @@ const dialCallExample = #{ }, }; +const outboundAnnouncementExample = #{ + request: #{ + command: "dial", + params: #{ + from: "", + to: "", + swml: #{ + version: "1.0.0", + sections: #{ + main: #[#{ play: #{ url: "say:Hello, welcome to SignalWire!" } }], + }, + }, + }, + }, +}; + const updateCallExample = #{ request: #{ command: "update", From a076f8786fe50ae7c2545e826097a10b3710c30d Mon Sep 17 00:00:00 2001 From: Devon-White Date: Fri, 11 Sep 2026 11:05:10 -0400 Subject: [PATCH 097/103] docs: flag generated snippet comparison limitations --- .../platform/pages/calling/voice/outbound-calling.mdx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 4f8af929aa..481b251e98 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -176,11 +176,14 @@ const call = await client.calling.dial({ console.log(call.id); ``` - + + +This generated snippet is included for comparison. It currently adds an example `url` alongside +the inline `swml`; use the manual samples for this walkthrough. + From c273abfffcb7a8792b2649a9d9ae63cfc8d5ef67 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Fri, 11 Sep 2026 11:25:56 -0400 Subject: [PATCH 098/103] Revert "docs: compare generated outbound request snippet in a tab" Restores the manual REST examples to tabbed CodeBlocks and removes the named API example added only for that comparison. --- fern/apis/signalwire-rest/openapi.yaml | 14 ---------- .../pages/calling/voice/outbound-calling.mdx | 26 ++++++------------- .../calling-api/calls/main.tsp | 7 ----- .../calling-api/calls/models/examples.tsp | 16 ------------ 4 files changed, 8 insertions(+), 55 deletions(-) diff --git a/fern/apis/signalwire-rest/openapi.yaml b/fern/apis/signalwire-rest/openapi.yaml index 9670610cd7..4270718728 100644 --- a/fern/apis/signalwire-rest/openapi.yaml +++ b/fern/apis/signalwire-rest/openapi.yaml @@ -619,20 +619,6 @@ paths: from: '+15551234567' to: '+15557654321' url: https://example.com/swml - Outbound announcement: - summary: Outbound announcement - description: Dial a destination and play a greeting using inline SWML - value: - command: dial - params: - from: - to: - swml: - version: 1.0.0 - sections: - main: - - play: - url: say:Hello, welcome to SignalWire! update: summary: update description: Modify an existing call's parameters in real-time diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 481b251e98..013f270391 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -101,8 +101,8 @@ WebSocket calling, use a Server SDK or the Browser SDK. The request includes a SignalWire Markup Language (SWML) document in `swml`. SignalWire runs it when the destination answers, playing the announcement below. - - + + ```bash curl -X POST "https://.signalwire.com/api/calling/calls" \ -u ":" \ @@ -123,8 +123,8 @@ curl -X POST "https://.signalwire.com/api/calling/calls" \ } }' ``` - - + + ```python # Install: python -m pip install signalwire-sdk==3.4.1 # Save as outbound_call.py and run: python outbound_call.py @@ -150,8 +150,8 @@ call = client.calling.dial( ) print(call["id"]) ``` - - + + ```typescript // Install: npm install @signalwire/sdk@2.0.5 // This sample also runs as JavaScript: save as outbound-call.mjs, @@ -175,18 +175,8 @@ const call = await client.calling.dial({ }); console.log(call.id); ``` - - - -This generated snippet is included for comparison. It currently adds an example `url` alongside -the inline `swml`; use the manual samples for this walkthrough. - - - - + + The REST API returns a call `id` and status `queued`. This confirms that SignalWire accepted the request; the call hasn't necessarily rung or been answered yet. Save the `id` to identify this call. diff --git a/specs/signalwire-rest/calling-api/calls/main.tsp b/specs/signalwire-rest/calling-api/calls/main.tsp index 58d0b9b559..9213c65103 100644 --- a/specs/signalwire-rest/calling-api/calls/main.tsp +++ b/specs/signalwire-rest/calling-api/calls/main.tsp @@ -354,13 +354,6 @@ namespace SignalWireAPI.Calling.Calls { description: "Modify an existing call's parameters in real-time", } ) - @opExample( - #{ parameters: outboundAnnouncementExample }, - #{ - title: "Outbound announcement", - description: "Dial a destination and play a greeting using inline SWML", - } - ) @opExample( #{ parameters: dialCallExample }, #{ diff --git a/specs/signalwire-rest/calling-api/calls/models/examples.tsp b/specs/signalwire-rest/calling-api/calls/models/examples.tsp index 9ceabc0626..31898c25ae 100644 --- a/specs/signalwire-rest/calling-api/calls/models/examples.tsp +++ b/specs/signalwire-rest/calling-api/calls/models/examples.tsp @@ -21,22 +21,6 @@ const dialCallExample = #{ }, }; -const outboundAnnouncementExample = #{ - request: #{ - command: "dial", - params: #{ - from: "", - to: "", - swml: #{ - version: "1.0.0", - sections: #{ - main: #[#{ play: #{ url: "say:Hello, welcome to SignalWire!" } }], - }, - }, - }, - }, -}; - const updateCallExample = #{ request: #{ command: "update", From c39a27e65e0d9f22b4ac7ad374c58666ef3f88e1 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Mon, 14 Sep 2026 12:01:40 -0400 Subject: [PATCH 099/103] docs: add comparison table for call placement methods using REST, WebSocket, and Browser SDK --- .../platform/pages/calling/voice/outbound-calling.mdx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 013f270391..2d6d9d8f03 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -63,6 +63,16 @@ Choose the approach that fits how you want to control the call. | Control the call in real time, asynchronously receiving events and sending commands over a persistent WebSocket connection | [WebSocket (Relay)](#place-the-call), using a Server SDK | | Let someone place and speak on a call from your web app | [Browser SDK](#place-the-call) | +Each approach places the same call, but they differ in how you follow and control it afterward. + +| Function | REST | WebSocket (Relay) | Browser SDK | +|---|---|---|---| +| Place the call without holding a connection open | | | | +| Place the call from a web page, with the user speaking on it | | | | +| Command a call already in progress from any process, by its call ID | | | | +| Follow the call's events in your own code, with no public webhook URL | | | | +| Receive call progress as HTTP callbacks to a URL you host | | | | + ### Set your credentials and caller ID Replace these values in the code sample you choose: From 4cf0d22a2889eb4503a525ff7e4ea6f109db46dd Mon Sep 17 00:00:00 2001 From: Devon-White Date: Mon, 14 Sep 2026 13:07:02 -0400 Subject: [PATCH 100/103] docs: consolidate Browser SDK outbound guide into platform outbound calling guide and update links --- fern/docs.yml | 4 + fern/llms.txt | 4 +- .../build-voice-video/call-controls.mdx | 2 +- .../build-voice-video/device-management.mdx | 2 +- .../build-voice-video/inbound-calls.mdx | 2 +- .../build-voice-video/outbound-calls.mdx | 262 ------------------ .../v4/guides/build-voice-video/overview.mdx | 2 +- .../build-voice-video/screen-sharing.mdx | 2 +- .../guides/getting-started/authentication.mdx | 2 +- fern/products/browser-sdk/versions/v4.yml | 2 - .../pages/calling/voice/outbound-calling.mdx | 73 ++++- 11 files changed, 84 insertions(+), 273 deletions(-) delete mode 100644 fern/products/browser-sdk/pages/v4/guides/build-voice-video/outbound-calls.mdx diff --git a/fern/docs.yml b/fern/docs.yml index 99b59296ec..009aefb240 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -195,6 +195,10 @@ css: - components/voice-widget/styles.css redirects: + # Browser SDK outbound guide consolidated into the platform outbound calling + # guide, which now carries the browser walkthrough and media options. + - source: /docs/browser-sdk/v4/guides/outbound-calls + destination: /docs/platform/voice/outbound-calling # Legacy Agents SDK pages (#526). Use known paths and equivalent existing # content only. Exact entries prevent unknown or removed pages (examples, # best-practices, patterns, migration, changelog) from redirecting to an diff --git a/fern/llms.txt b/fern/llms.txt index d05ee2bcc8..05fe9d20e5 100644 --- a/fern/llms.txt +++ b/fern/llms.txt @@ -76,14 +76,14 @@ Send text and media messages from your application, and choose how to process in Give users an identity, authorize their access, and connect them to other users or shared resources. Your backend manages subscribers and credentials; the client provides the calling and chat interface. -- Browser SDK: For web clients, [place audio or video calls](/docs/browser-sdk/v4/guides/outbound-calls) and [send and display chat messages](/docs/browser-sdk/v4/guides/messaging-chat). +- Browser SDK: For web clients, [place audio or video calls](/docs/platform/voice/outbound-calling) and [send and display chat messages](/docs/browser-sdk/v4/guides/messaging-chat). - REST APIs: [Create subscribers](/docs/apis/rest/subscribers/create-subscriber) and [issue subscriber access tokens](/docs/apis/rest/subscribers/tokens/create-subscriber-token) from your backend. Follow the [Browser SDK authentication guide](/docs/browser-sdk/v4/guides/authentication) to connect a web client. ### Video conferences Build a browser conference interface or use a conference with a prebuilt interface. The [platform video overview](/docs/platform/video) explains rooms, participant permissions, and recordings. -- Browser SDK: [Join a room with audio and video](/docs/browser-sdk/v4/guides/outbound-calls), then add [call controls](/docs/browser-sdk/v4/guides/call-controls), [screen sharing](/docs/browser-sdk/v4/guides/screen-sharing), and [participant layouts](/docs/browser-sdk/v4/guides/layouts). +- Browser SDK: [Join a room with audio and video](/docs/browser-sdk/v4/guides/build-voice-video), then add [call controls](/docs/browser-sdk/v4/guides/call-controls), [screen sharing](/docs/browser-sdk/v4/guides/screen-sharing), and [participant layouts](/docs/browser-sdk/v4/guides/layouts). - REST APIs: [Create a video room](/docs/apis/rest/video/rooms/create-room) and [issue room access tokens](/docs/apis/rest/video/room-tokens/create-room-token), or [create a video conference with a prebuilt interface](/docs/apis/rest/video/video-conferences/create-video-conference). ### Migrate a Twilio application diff --git a/fern/products/browser-sdk/pages/v4/guides/build-voice-video/call-controls.mdx b/fern/products/browser-sdk/pages/v4/guides/build-voice-video/call-controls.mdx index 1523b83990..85c3f017eb 100644 --- a/fern/products/browser-sdk/pages/v4/guides/build-voice-video/call-controls.mdx +++ b/fern/products/browser-sdk/pages/v4/guides/build-voice-video/call-controls.mdx @@ -24,7 +24,7 @@ instance — get one of these going first. Install the SDK and create a `SignalWire` client with a credential provider. - + Dial a destination with `client.dial()` to get a `Call` instance. diff --git a/fern/products/browser-sdk/pages/v4/guides/build-voice-video/device-management.mdx b/fern/products/browser-sdk/pages/v4/guides/build-voice-video/device-management.mdx index 92f1d2af38..e0e44a331e 100644 --- a/fern/products/browser-sdk/pages/v4/guides/build-voice-video/device-management.mdx +++ b/fern/products/browser-sdk/pages/v4/guides/build-voice-video/device-management.mdx @@ -448,7 +448,7 @@ Pick a different mic, camera, or speaker before dialing. The log records each pi ### Switch a device mid-call -Enter a destination in the **Destination** field (a `/public/`, `/private/`, PSTN number, or SIP URI the token can reach — see [Outbound Calls](/docs/browser-sdk/v4/guides/outbound-calls#pick-a-destination) for the destination shapes) and click **Dial**. The local and remote tiles populate once the call connects. +Enter a destination in the **Destination** field (a `/public/`, `/private/`, PSTN number, or SIP URI the token can reach — see [Outbound calling](/docs/platform/voice/outbound-calling#choose-a-destination) for the destination shapes) and click **Dial**. The local and remote tiles populate once the call connects. With the call connected, change the **Microphone** or **Camera** dropdown again. The log now records the change as `live: …` — the picker is routing through `activeCall.self.selectAudioInputDevice()` instead of the client-level preference, and the remote side hears or sees the new device immediately without renegotiation. Picking a new **Speaker** triggers `applySelectedAudioOutputDevice()` on the remote `