From 0723ce591e8a8c67fbd85823e67ea2dd18cc0171 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 16 Sep 2026 08:23:56 -0400 Subject: [PATCH 01/14] docs(voice): add inbound calling guide Companion to the outbound calling guide. Covers how a call reaches a Resource through its addresses (phone number, SIP address, alias), then answers a first call with SDK-served SWML, Relay, or the Browser SDK, assigns the number in the Dashboard or with REST, follows the call, and adds AI agent, phone menu, whisper, voicemail, recording, streaming, and browser-answering examples. Adds two themed lifecycle diagrams, a card on the Voice overview, and a root llms.txt link. --- .../img/inbound-call-lifecycle-themed.svg | 85 + .../inbound-call-relay-lifecycle-themed.svg | 86 + fern/llms.txt | 2 +- .../pages/calling/voice/inbound-calling.mdx | 1807 +++++++++++++++++ .../platform/pages/calling/voice/overview.mdx | 3 + 5 files changed, 1982 insertions(+), 1 deletion(-) create mode 100644 fern/assets/images/img/inbound-call-lifecycle-themed.svg create mode 100644 fern/assets/images/img/inbound-call-relay-lifecycle-themed.svg create mode 100644 fern/products/platform/pages/calling/voice/inbound-calling.mdx diff --git a/fern/assets/images/img/inbound-call-lifecycle-themed.svg b/fern/assets/images/img/inbound-call-lifecycle-themed.svg new file mode 100644 index 0000000000..e0404cdb89 --- /dev/null +++ b/fern/assets/images/img/inbound-call-lifecycle-themed.svg @@ -0,0 +1,85 @@ + + Inbound call handled by a SWML Script + A caller dials your SignalWire number. SignalWire looks up the Resource assigned to the number, then sends your server the call details: from, to, direction, and call id. Your server returns a SWML document. SignalWire answers the caller and runs your SWML. When the call finishes, the SWML session ends. + + + + + + + + + + + + + Caller + + + + + SignalWire + + + + + Your server + + + dials + your number + + + + Looks up the number's Resource + + + call details + from, to, direction, call id + + SWML document + + + + answers + + + + Your SWML runs + Call finishes + + ended + SWML session ends + + diff --git a/fern/assets/images/img/inbound-call-relay-lifecycle-themed.svg b/fern/assets/images/img/inbound-call-relay-lifecycle-themed.svg new file mode 100644 index 0000000000..d87f443c5a --- /dev/null +++ b/fern/assets/images/img/inbound-call-relay-lifecycle-themed.svg @@ -0,0 +1,86 @@ + + Relay: an inbound call as commands and events over one WebSocket + Your code and SignalWire share one persistent WebSocket. SignalWire reports the new call through calling.call.receive. Your code sends calling.answer; SignalWire reports 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 + call received + calling.call.receive + + answer + calling.answer + + 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/llms.txt b/fern/llms.txt index 663001674e..a5e235efba 100644 --- a/fern/llms.txt +++ b/fern/llms.txt @@ -97,7 +97,7 @@ 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/platform/voice/outbound-calling) 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), [answer calls to a phone number](/docs/platform/voice/inbound-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 diff --git a/fern/products/platform/pages/calling/voice/inbound-calling.mdx b/fern/products/platform/pages/calling/voice/inbound-calling.mdx new file mode 100644 index 0000000000..9f40b506b4 --- /dev/null +++ b/fern/products/platform/pages/calling/voice/inbound-calling.mdx @@ -0,0 +1,1807 @@ +--- +title: Inbound calling +slug: /voice/inbound-calling +description: Answer your first inbound phone call, find out who is calling, and add an AI agent, a phone menu, or browser answering to your application. +max-toc-depth: 3 +--- + +[trial-mode]: /docs/platform/trial-mode +[api-credentials]: /docs/platform/your-signalwire-api-space +[phone-numbers]: /docs/platform/phone-numbers +[resources]: /docs/platform/resources +[addresses]: /docs/platform/addresses +[subscribers]: /docs/platform/subscribers +[webhooks]: /docs/platform/webhooks +[ai-best-practices]: /docs/platform/ai/best-practices +[sip-credentials]: /docs/platform/voice/sip/sip-credentials +[swml-quickstart]: /docs/swml/guides +[swml-webhook-security]: /docs/swml/guides/webhook-security +[swml-ivr]: /docs/swml/guides/ivr +[swml-call-whisper]: /docs/swml/guides/call-whisper +[swml-webhook-payload]: /docs/swml/reference/calling#webhook-payload +[swml-variables]: /docs/swml/reference/variables +[swml-ai]: /docs/swml/reference/calling/ai +[swml-prompt]: /docs/swml/reference/calling/prompt +[swml-switch]: /docs/swml/reference/calling/switch +[swml-connect]: /docs/swml/reference/calling/connect +[swml-record]: /docs/swml/reference/calling/record +[swml-record-call]: /docs/swml/reference/calling/record-call +[swml-stream]: /docs/swml/reference/calling/stream +[py-relay-call]: /docs/server-sdks/reference/python/relay/call +[py-relay-events]: /docs/server-sdks/reference/python/relay/events +[py-swml-service]: /docs/server-sdks/reference/python/agents/swml-service +[ts-swml-builder]: /docs/server-sdks/reference/typescript/agents/swml-builder +[py-relay-client]: /docs/server-sdks/reference/python/relay/client +[ts-relay-client]: /docs/server-sdks/reference/typescript/relay/client +[py-set-swml-webhook]: /docs/server-sdks/reference/python/rest/phone-numbers/set-swml-webhook +[ts-set-swml-webhook]: /docs/server-sdks/reference/typescript/rest/phone-numbers/set-swml-webhook +[py-set-relay-topic]: /docs/server-sdks/reference/python/rest/phone-numbers/set-relay-topic +[ts-set-relay-topic]: /docs/server-sdks/reference/typescript/rest/phone-numbers/set-relay-topic +[rest-update-number]: /docs/apis/rest/phone-numbers/update-phone-number +[rest-list-numbers]: /docs/apis/rest/phone-numbers/list-phone-numbers +[rest-create-script]: /docs/apis/rest/swml-scripts/create-swml-script +[rest-link-number]: /docs/apis/rest/phone-number-addresses/create-phone-number-address +[subscriber-token]: /docs/apis/rest/subscribers/tokens/create-subscriber-token +[browser-auth]: /docs/browser-sdk/v4/guides/authentication +[browser-inbound]: /docs/browser-sdk/v4/guides/inbound-calls +[browser-call-controls]: /docs/browser-sdk/v4/guides/call-controls +[browser-device-management]: /docs/browser-sdk/v4/guides/device-management +[browser-register]: /docs/browser-sdk/v4/reference/signalwire/register +[browser-session-state]: /docs/browser-sdk/v4/reference/interfaces/session-state +[browser-call]: /docs/browser-sdk/v4/reference/interfaces/call +[browser-transfer]: /docs/browser-sdk/v4/reference/webrtc-call/transfer + +Answer a call to your SignalWire phone number and choose what happens when it rings. Start by +playing a short announcement to yourself, then find out who is calling, run an AI agent, build a +phone menu, or let a signed-in user answer from your web app. + +## Prepare for your first call + +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, and its **Numbers** permission if you assign the + handler with the REST API rather than the Dashboard. +- A voice-capable [phone number purchased in your Space][phone-numbers]. +- If answering in the browser, a [Subscriber token][subscriber-token] for the [Subscriber][subscribers] + the number rings. Guest and embed tokens can't receive calls. +- A phone you can call from. + + +A [trial project][trial-mode] receives calls only from phone numbers it has verified. Verify the +phone you'll call from, or upgrade the project, before you dial. + + +## How a call reaches your code + +Everything that can handle a call in your Space is a [Resource][resources]: a SWML Script, a +Relay Application, a Subscriber, an AI Agent. Callers never dial the Resource itself. They dial +one of its [addresses][addresses], and SignalWire hands the call to the Resource behind it. A +Resource can have several addresses, and you can move an address to a different Resource later +without changing any code. + +| Address type | Looks like | Who can reach it | +|---|---|---| +| Phone number | `+12025550123` | Anyone on the phone network. Each number has one call handler and one message handler, assigned separately. | +| SIP address | `sip:@-.dapp.signalwire.com` | SIP callers outside SignalWire. You choose the user and context parts, and can require a password or an IP allowlist. | +| Alias | `/public/support` or `/private/john-doe` | Your own SWML, Browser SDK clients, and REST dials. A `public` alias is reachable by anyone, a `private` one only by authenticated users. | + +SignalWire creates one alias from the Resource's name when you create it. Add more to expose the +Resource under other names, or to limit an alias to the audio, video, or messaging channels. + +In the Dashboard, a Resource's **Addresses & Phone Numbers** tab lists its addresses and adds new +ones, and the Space-wide **Addresses** page lists every address with its context, type, call +handler, and message handler. An unassigned phone number appears there with no call handler, +and nothing happens on an inbound call until you assign one. + +The Resource type decides how you control the call: + +| Resource | What SignalWire does with the call | +|---|---| +| SWML Script | Runs a SignalWire Markup Language (SWML) document that it fetches from your server on every call, or that you host in your Space | +| Relay Application | Delivers the call over a persistent WebSocket to your Server SDK client subscribed to the application's topic | +| Subscriber | Rings that user's registered devices, including a Browser SDK client and any SIP endpoints | +| AI Agent, Call Flow | Runs the agent or flow you built in the Dashboard | + +The rest of this page covers the first three. Because the Resource is what handles the call, the +same handler runs whether the caller dialed your phone number, its [SIP address][sip-credentials], +or one of its aliases. + +## Answer your first call + +Play a short announcement to anyone who dials your number. + + + +### Choose how to handle the call + +Choose the approach that fits how you want to control the call. + +| What you want to do | Where to start | +|---|---| +| Return call instructions from your server as a document, built with a Server SDK on every call | [SWML](#create-your-call-handler), served by a Server SDK | +| Control the call in real time, receiving events and sending commands over a persistent WebSocket connection | [WebSocket (Relay)](#create-your-call-handler), using a Server SDK | +| Let someone answer and speak on the call from your web app | [Browser SDK](#create-your-call-handler) | + +Each approach answers the same call, but they differ in how you follow and control it afterward. + +| Function | SWML | WebSocket (Relay) | Browser SDK | +|---|---|---|---| +| Return call instructions from your Server SDK app without holding a connection open | | | | +| Command a call already in progress from any process, by its call ID | | | | +| React to events in your own code while the call is live, with no public URL | | | | +| Answer from a web page, with the user speaking on the call | | | | +| Start an AI agent, record, stream, or bridge the call from code | | | | + +The REST Calling API places calls; it doesn't answer them. To act on an inbound call from REST +later, use the call ID that SWML or Relay gives you. + +### Set your credentials + +Replace these values in the code sample you choose: + +| Value | Replace with | +|---|---| +| `` | Your Space's subdomain in `.signalwire.com` | +| `` | Your Project ID | +| `` | Your API token | +| `` | The ID shown on the phone number's page in the Dashboard, or returned by [List phone numbers][rest-list-numbers] | +| `` | The public URL where your server serves the SWML document, including the basic-auth credentials for the Python example | +| `` | A [Subscriber token][subscriber-token] created by your backend, for the Browser SDK examples | + +### Create your call handler + +Write the code SignalWire runs, or connects to, when the number rings. + + + + +A SWML document tells SignalWire what to do with the call. When the number rings, SignalWire +requests the document from your server and runs it. Build and serve it with a Server SDK, +[`SWMLService`][py-swml-service] in Python or [`SwmlBuilder`][ts-swml-builder] behind an HTTP +server in TypeScript, then expose the port on a public HTTPS URL. A tunnel such as [ngrok](https://ngrok.com/) works for +development. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as inbound_call.py and run: python inbound_call.py +from signalwire import SWMLService + +service = SWMLService( + name="inbound-call", + route="/swml", + port=3000, + basic_auth=("signalwire", ""), +) +service.add_verb("play", {"url": "say:Hello, welcome to SignalWire!"}) + +# Serves the document at /swml. Put the credentials in the URL you give +# SignalWire: https://signalwire:@/swml +service.serve() +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as inbound-call.mjs, +// then run: node inbound-call.mjs +import { createServer } from "node:http"; +import { SwmlBuilder } from "@signalwire/sdk"; + +const swml = new SwmlBuilder() + .say("Hello, welcome to SignalWire!") + .build(); + +// Serves the document at every path on port 3000. +createServer((request, response) => { + response.setHeader("Content-Type", "application/json"); + response.end(JSON.stringify(swml)); +}).listen(3000); +``` + + + +Anyone who learns the URL can fetch your document, so [verify the request signature][swml-webhook-security] +before you serve anything sensitive. + +Both servers return this document. It plays the announcement, then hangs up when the document +ends. If you'd rather not run a server yet, you can paste it into a hosted SWML Script in the +next step instead, and the [SWML quickstart][swml-quickstart] covers that path. + + + +```yaml +version: 1.0.0 +sections: + main: + - play: + url: 'say:Hello, welcome to SignalWire!' +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "play": { "url": "say:Hello, welcome to SignalWire!" } } + ] + } +} +``` + + + + + + +A [`RelayClient`][py-relay-client] subscribes to a topic and receives every call routed to it. +The handler answers, plays the announcement, and hangs up. Keep the process running; it holds the connection open. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as inbound_call.py and run: python inbound_call.py +from signalwire.relay import RelayClient + +client = RelayClient( + project="", + token="", + contexts=["inbound-calling"], +) + +@client.on_call +async def handle_call(call): + await call.answer() + playback = await call.play([{ + "type": "tts", + "params": {"text": "Hello, welcome to SignalWire!"}, + }]) + await playback.wait() + await call.hangup() + +client.run() +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as inbound-call.mjs, +// then run: node inbound-call.mjs +import { RelayClient } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "", + token: "", + contexts: ["inbound-calling"], +}); + +client.onCall(async (call) => { + await call.answer(); + const playback = await call.play([ + { type: "tts", text: "Hello, welcome to SignalWire!" }, + ]); + await playback.wait(); + await call.hangup(); +}); + +await client.run(); +``` + + + +The `contexts` value is the topic. You'll give the same name to the Relay Application in the +next step, and only a client subscribed to it receives the call. + + + + +A Browser SDK client registers as a Subscriber and receives the call in the page. Show the +caller, then let the user answer or decline. + +```javascript +// Install: npm install @signalwire/js@latest rxjs +// Run on HTTPS or localhost with these elements in your page: +//

Offline

+//

+// +// +// +// +// Use a Subscriber Access Token issued by your backend for the Subscriber +// that the phone number rings. +import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; + +const client = new SignalWire(new StaticCredentialProvider({ + token: "", +})); +const statusLine = document.querySelector("#status"); +const callerLine = document.querySelector("#caller"); +const remoteAudio = document.querySelector("#remote-audio"); +const answerButton = document.querySelector("#answer"); +const declineButton = document.querySelector("#decline"); +const hangupButton = document.querySelector("#hangup"); +const finalStatuses = new Set(["disconnected", "failed", "destroyed"]); + +let currentCall = null; + +await client.register(); +statusLine.textContent = "Online"; + +client.session.incomingCalls$.subscribe((calls) => { + const ringing = calls.find((call) => call.status === "ringing"); + if (!ringing || ringing === currentCall) return; + currentCall = ringing; + callerLine.textContent = `Incoming call from ${ringing.from}`; + answerButton.disabled = false; + declineButton.disabled = false; + + ringing.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); + ringing.status$.subscribe((status) => { + statusLine.textContent = status; + if (status !== "ringing") { + answerButton.disabled = true; + declineButton.disabled = true; + } + if (status === "connected") hangupButton.disabled = false; + if (finalStatuses.has(status)) { + remoteAudio.srcObject = null; + hangupButton.disabled = true; + callerLine.textContent = ""; + if (currentCall === ringing) currentCall = null; + } + }); +}); + +answerButton.onclick = () => { + void currentCall?.answer({ audio: true, video: false }); +}; +declineButton.onclick = () => { + void currentCall?.reject(); +}; +hangupButton.onclick = () => { + void currentCall?.hangup().catch(console.error); +}; +``` + +Constructing the client authenticates the user, and [`register()`][browser-register] brings them +online so calls can reach them. Leave the page open while you test. + +
+
+ +### Point your number at the handler + +Assign a Resource as the number's call handler. Create the Resource first if your handler needs +one. You can assign from either side: from the number's **Edit Settings** page as shown below, +or from the Resource by opening its **Addresses & Phone Numbers** tab, selecting **+ Add**, then +**Phone Number**, and choosing a number you own. The same menu adds a **SIP Address** or an +**Alias** to the Resource. + + + + +In the Dashboard, open **My Resources**, select **+ Add**, then **Script**, then **SWML Script**. +Give it a name and leave **Used For** set to **Calling**. Under **Handle Calls Using**, choose +**External URL** and enter your server's URL in **Primary Script URL**. To use the hosted +document instead, choose **Hosted Script** and paste it into **Primary Script**. Select +**Create**. + + + + + +![The New SWML Script form in the Dashboard with Handle Calls Using set to External URL](/assets/images/dashboard/resources/external-swml-script.webp) + + + + + +Then open **Phone Numbers**, select your number, and select **Edit Settings**. Under **Inbound +Call Settings**, select **Assign Resource**, choose the SWML Script you created, and save. + + + + + +![A phone number's settings page showing the Assign Resource option under Inbound Call Settings](/assets/images/dashboard/phone-numbers/assign-resource-voice.webp) + + + + + +You can do the same with the REST API. [Update the phone number][rest-update-number] with the +`relay_script` handler. This creates the SWML Script Resource for the URL and assigns it in one +call; the Server SDKs wrap it as [`set_swml_webhook`][py-set-swml-webhook] and +[`setSwmlWebhook`][ts-set-swml-webhook]. Find the number's ID on its Dashboard page or with +[List phone numbers][rest-list-numbers]. + + + +```bash +curl -X PUT "https://.signalwire.com/api/relay/rest/phone_numbers/" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "call_handler": "relay_script", + "call_relay_script_url": "" + }' +``` + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +from signalwire.rest import RestClient + +client = RestClient( + project="", + token="", + host=".signalwire.com", +) + +client.phone_numbers.set_swml_webhook("", "") +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +import { RestClient } from "@signalwire/sdk"; + +const client = new RestClient({ + project: "", + token: "", + host: ".signalwire.com", +}); + +await client.phoneNumbers.setSwmlWebhook("", ""); +``` + + + + + +[Create the SWML Script][rest-create-script] with the document as `contents`, then +[link it to the number][rest-link-number]. The link endpoint is in beta and requires the +number's calling channel to have no Resource assigned yet. + +```bash +curl -X POST "https://.signalwire.com/api/fabric/resources/swml_scripts" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Inbound welcome", + "contents": { + "version": "1.0.0", + "sections": { + "main": [ + { "play": { "url": "say:Hello, welcome to SignalWire!" } } + ] + } + } + }' + +# Use the "id" from the response as the resource_id. +curl -X POST "https://.signalwire.com/api/fabric/phone_number_addresses" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "number": "", + "resource_id": "", + "handler_type": "calling" + }' +``` + + + + + + +In the Dashboard, open **My Resources**, select **+ Add**, then **Relay Application**. Give it +a name, enter `inbound-calling` as the **Topic** to match the `contexts` value in your code, and +select **Create**. + +Then open **Phone Numbers**, select your number, and select **Edit Settings**. Under **Inbound +Call Settings**, select **Assign Resource**, choose the Relay Application, and save. + + + + + +![A phone number's settings page showing the Assign Resource option under Inbound Call Settings](/assets/images/dashboard/phone-numbers/assign-resource-voice.webp) + + + + + +To assign with the REST API instead, [update the phone number][rest-update-number] with the +`relay_topic` handler and the topic name, or use the [`set_relay_topic`][py-set-relay-topic] and +[`setRelayTopic`][ts-set-relay-topic] wrappers. + + + +```bash +curl -X PUT "https://.signalwire.com/api/relay/rest/phone_numbers/" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "call_handler": "relay_topic", + "call_relay_topic": "inbound-calling" + }' +``` + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +from signalwire.rest import RestClient + +client = RestClient( + project="", + token="", + host=".signalwire.com", +) + +client.phone_numbers.set_relay_topic("", "inbound-calling") +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +import { RestClient } from "@signalwire/sdk"; + +const client = new RestClient({ + project: "", + token: "", + host: ".signalwire.com", +}); + +await client.phoneNumbers.setRelayTopic("", { topic: "inbound-calling" }); +``` + + + + + + +Assign the number to the Subscriber your token was issued for. In the Dashboard, open +**Phone Numbers**, select your number, and select **Edit Settings**. Under **Inbound Call +Settings**, select **Assign Resource**, choose the Subscriber, and save. + + + + + +![A phone number's settings page showing the Assign Resource option under Inbound Call Settings](/assets/images/dashboard/phone-numbers/assign-resource-voice.webp) + + + + + +To assign with the REST API instead, [link the Subscriber to the number][rest-link-number]. This +endpoint is in beta and requires the number's calling channel to have no Resource assigned yet. + +```bash +curl -X POST "https://.signalwire.com/api/fabric/phone_number_addresses" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "number": "", + "resource_id": "", + "handler_type": "calling" + }' +``` + +The Subscriber also keeps its `/private/` address, so another app or Resource in your +Space can call the same browser client without a phone number. + + + + +### Call your number + +Dial your SignalWire number from your phone. With SWML or Relay, you hear "Hello, welcome to +SignalWire!" and the call ends. With the browser example, the page shows the incoming call. +Select **Answer**, allow microphone access, and select **Hang up** when you finish. + +If the call rings without an answer or fails, check these first: + +- The number's **Inbound Call Settings** show your Resource. An unassigned number takes no action. +- You're calling from a verified number if the project is in trial mode. +- For Relay, the Relay Application's **Topic** matches your client's `contexts` value and the client is running. +- For SWML served from your server, SignalWire could reach the URL and got a valid document. Check the + call in the Dashboard's **Logs**. + +
+ +## Know who's calling and follow the call + +Each approach gives you the caller's details and the call's progress in its own way. Follow the +section for the approach you used for your first call. + +### Read the call details via SWML + +When SignalWire fetches your document, it sends your server a POST request whose `call` object +carries the `from` and `to` addresses, the `direction`, and the `call_id`. See the +[webhook payload reference][swml-webhook-payload] for every field. + +The same fields are available inside the document as [variables][swml-variables], so the +document itself can use them, whether your server built it or you hosted it. This document reads +the caller's number back to them: + + + +```yaml +version: 1.0.0 +sections: + main: + - play: + url: 'say:Thanks for calling from ${call.from}.' +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "play": { "url": "say:Thanks for calling from ${call.from}." } } + ] + } +} +``` + + + +Individual methods report their own progress. Set `status_url` on [`record`][swml-record], +[`connect`][swml-connect], or [`stream`][swml-stream] to receive HTTP callbacks as that step +runs. The [webhooks guide][webhooks] covers endpoint setup and callback reliability. + +This flow shows an inbound call handled by a SWML Script that fetches the document from your +server: + + + +A caller dials your number. SignalWire looks up the number's Resource and sends your server the call details: from, to, direction, and call id. Your server returns a SWML document. SignalWire answers the caller and runs your SWML. When the call finishes, the SWML session ends. + + + + + +```mermaid +sequenceDiagram + participant Caller + participant SW as SignalWire + participant Server as Your server + + Caller->>SW: dials your number + Note over SW: Looks up the number's Resource + SW->>Server: POST call details: from, to, direction, call id + Server-->>SW: SWML document + SW->>Caller: answers + Note over Caller,SW: Your SWML runs + Note over Caller,SW: Call finishes +``` + + + +### Follow the call via WebSocket (Relay) + +The [`Call`][py-relay-call] object your handler receives carries the caller in `device`, +together with `direction`, `context`, and `call_id`. Register a `calling.call.state` listener +to see `answered`, `ending`, and `ended` as they happen, and wait for the call to end before your +handler returns. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as inbound_call.py and run: python inbound_call.py +from signalwire.relay import RelayClient +from signalwire.relay.event import CallStateEvent + +client = RelayClient( + project="", + token="", + contexts=["inbound-calling"], +) + +@client.on_call +async def handle_call(call): + caller = call.device.get("params", {}).get("from_number", "unknown") + print(f"Call {call.call_id} from {caller} on topic {call.context}") + + def handle_state(event: CallStateEvent): + print(f"State: {event.call_state}, reason: {event.end_reason}") + + call.on("calling.call.state", handle_state) + + await call.answer() + playback = await call.play([{ + "type": "tts", + "params": {"text": "Hello, welcome to SignalWire!"}, + }]) + await playback.wait() + await call.hangup() + await call.wait_for_ended() + +client.run() +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as inbound-call.mjs, +// then run: node inbound-call.mjs +import { RelayClient } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "", + token: "", + contexts: ["inbound-calling"], +}); + +client.onCall(async (call) => { + const caller = call.device?.params?.from_number ?? "unknown"; + console.log(`Call ${call.callId} from ${caller} on topic ${call.context}`); + + call.on("calling.call.state", (event) => { + console.log(`State: ${event.params.call_state}, reason: ${event.params.end_reason ?? ""}`); + }); + + await call.answer(); + const playback = await call.play([ + { type: "tts", text: "Hello, welcome to SignalWire!" }, + ]); + await playback.wait(); + await call.hangup(); + await call.waitForEnded(); +}); + +await client.run(); +``` + + + +For more event handlers, see +[Event listeners in the Relay client guide](/docs/server-sdks/guides/relay-client#event-listeners) +and the [events reference][py-relay-events]. + +This flow shows the events SignalWire sends and the commands your code returns over the same +persistent connection: + + + +Your code and SignalWire share one persistent WebSocket. SignalWire reports the new call through calling.call.receive. Your code sends calling.answer; SignalWire reports 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. + + + + + +```mermaid +sequenceDiagram + participant App as Your code + participant SW as SignalWire + + Note over App,SW: One persistent WebSocket, both directions + SW-->>App: calling.call.receive + App->>SW: calling.answer + SW-->>App: calling.call.state: answered + App->>SW: calling.play + SW-->>App: calling.call.play: playing, then finished + App->>SW: calling.end + SW-->>App: calling.call.state: ending, then ended +``` + + + +### Follow the call in the browser + +Each entry in [`incomingCalls$`][browser-session-state] is a [`Call`][browser-call] with +`direction` set to `inbound`. Read `from` for the caller's address, `fromName` for a display +name when the caller supplied one, and `to` for the address that was dialed. SignalWire sends +`_undef_` as the display name when the calling leg didn't supply one, so fall back to `from`. + +After the user answers, `status$` moves through `connecting` and `connected`, then +`disconnecting`, `disconnected`, and `destroyed` once the call ends. A call that leaves +`ringing` without reaching `connected` was declined or abandoned by the caller, so one +`status$` subscription can dismiss the ringing UI for every outcome. + +## Examples + +### Run an AI agent + +Start an AI agent that welcomes the caller and answers basic questions about SignalWire. + + +Open with a greeting that says the call uses an artificial voice, and follow the +[AI best practices][ai-best-practices]. + + +#### Run an AI agent via SWML + +Put an [`ai`][swml-ai] method in the document. SignalWire answers the call and hands it to the +agent, which runs until the caller hangs up. + + + +```yaml +version: 1.0.0 +sections: + main: + - ai: + params: + static_greeting: Hello, welcome to SignalWire! This call uses an artificial voice. + static_greeting_no_barge: true + 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. +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { + "ai": { + "params": { + "static_greeting": "Hello, welcome to SignalWire! This call uses an artificial voice.", + "static_greeting_no_barge": true + }, + "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." + } + } + } + ] + } +} +``` + + + +#### Run an AI agent via WebSocket (Relay) + +Answer the call, start the agent with `call.ai()`, and keep the handler alive until the call ends. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as inbound_call.py and run: python inbound_call.py +from signalwire.relay import RelayClient + +client = RelayClient( + project="", + token="", + contexts=["inbound-calling"], +) + +@client.on_call +async def handle_call(call): + await call.answer() + await call.ai( + ai_params={ + "static_greeting": "Hello, welcome to SignalWire! This call uses an artificial voice.", + "static_greeting_no_barge": True, + }, + 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.""" + }, + ) + # Keep the handler alive until the caller hangs up. + await call.wait_for_ended() + +client.run() +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as inbound-call.mjs, +// then run: node inbound-call.mjs +import { RelayClient } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "", + token: "", + contexts: ["inbound-calling"], +}); + +client.onCall(async (call) => { + await call.answer(); + await call.ai({ + aiParams: { + static_greeting: "Hello, welcome to SignalWire! This call uses an artificial voice.", + static_greeting_no_barge: true, + }, + 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.`, + }, + }); + // Keep the handler alive until the caller hangs up. + await call.waitForEnded(); +}); + +await client.run(); +``` + + + +### Build a phone menu + +Offer the caller a choice, then connect them to the matching destination. Replace +`` and `` with phone numbers, SIP URIs, or +Resource addresses. + +#### Build a phone menu via SWML + +Use [`prompt`][swml-prompt] to collect a digit and [`switch`][swml-switch] on `prompt_value` to +pick the destination. See the [IVR guide][swml-ivr] for a fuller menu with speech input. + + + +```yaml +version: 1.0.0 +sections: + main: + - prompt: + play: 'say:Thanks for calling SignalWire. Press 1 for sales or 2 for support.' + max_digits: 1 + - switch: + variable: prompt_value + case: + '1': + - connect: + to: '' + '2': + - connect: + to: '' + default: + - play: + url: 'say:Sorry, that is not a valid choice. Goodbye.' + - hangup: {} +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { + "prompt": { + "play": "say:Thanks for calling SignalWire. Press 1 for sales or 2 for support.", + "max_digits": 1 + } + }, + { + "switch": { + "variable": "prompt_value", + "case": { + "1": [{ "connect": { "to": "" } }], + "2": [{ "connect": { "to": "" } }] + }, + "default": [ + { "play": { "url": "say:Sorry, that is not a valid choice. Goodbye." } }, + { "hangup": {} } + ] + } + } + ] + } +} +``` + + + +#### Build a phone menu via WebSocket (Relay) + +Use `play_and_collect()` to play the prompt and wait for a digit, then bridge the call with +`connect()`. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as inbound_call.py and run: python inbound_call.py +from signalwire.relay import RelayClient + +DESTINATIONS = { + "1": "", + "2": "", +} + +client = RelayClient( + project="", + token="", + contexts=["inbound-calling"], +) + +@client.on_call +async def handle_call(call): + await call.answer() + menu = await call.play_and_collect( + media=[{ + "type": "tts", + "params": {"text": "Thanks for calling SignalWire. Press 1 for sales or 2 for support."}, + }], + collect={"digits": {"max": 1, "digit_timeout": 5}}, + ) + result = await menu.wait() + digit = result.params.get("result", {}).get("digits", "") + + destination = DESTINATIONS.get(digit) + if destination: + await call.connect([[{"type": "phone", "params": {"to_number": destination}}]]) + else: + goodbye = await call.play([{ + "type": "tts", + "params": {"text": "Sorry, that is not a valid choice. Goodbye."}, + }]) + await goodbye.wait() + await call.hangup() + +client.run() +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as inbound-call.mjs, +// then run: node inbound-call.mjs +import { RelayClient } from "@signalwire/sdk"; + +const DESTINATIONS = { + "1": "", + "2": "", +}; + +const client = new RelayClient({ + project: "", + token: "", + contexts: ["inbound-calling"], +}); + +client.onCall(async (call) => { + await call.answer(); + const menu = await call.playAndCollect( + [{ type: "tts", text: "Thanks for calling SignalWire. Press 1 for sales or 2 for support." }], + { digits: { max: 1, digit_timeout: 5 } }, + ); + const result = await menu.wait(); + const digit = result.params?.result?.digits ?? ""; + + const destination = DESTINATIONS[digit]; + if (destination) { + await call.connect([[{ type: "phone", params: { to_number: destination } }]]); + } else { + const goodbye = await call.play([ + { type: "tts", text: "Sorry, that is not a valid choice. Goodbye." }, + ]); + await goodbye.wait(); + await call.hangup(); + } +}); + +await client.run(); +``` + + + +### Forward the call with a whisper + +Ring ``, play a private message to whoever answers, then connect them +to the caller. + +#### Forward the call via SWML + +Use `connect.confirm` to play the [whisper][swml-call-whisper] to the agent before bridging the +two legs. The caller keeps hearing ringing until the agent is connected. + + + +```yaml +version: 1.0.0 +sections: + main: + - play: + url: 'say:Thanks for calling SignalWire. Connecting you now.' + - connect: + to: '' + confirm: + - play: + url: 'say:You are about to be connected to a caller from ${call.from}.' + confirm_timeout: 20 +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "play": { "url": "say:Thanks for calling SignalWire. Connecting you now." } }, + { + "connect": { + "to": "", + "confirm": [ + { "play": { "url": "say:You are about to be connected to a caller from ${call.from}." } } + ], + "confirm_timeout": 20 + } + } + ] + } +} +``` + + + +#### Forward the call via WebSocket (Relay) + +Answer, tell the caller what's happening, then bridge with `connect()`. Pass `ringback` so the +caller hears ringing while the agent's phone rings. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as inbound_call.py and run: python inbound_call.py +from signalwire.relay import RelayClient + +client = RelayClient( + project="", + token="", + contexts=["inbound-calling"], +) + +@client.on_call +async def handle_call(call): + await call.answer() + intro = await call.play([{ + "type": "tts", + "params": {"text": "Thanks for calling SignalWire. Connecting you now."}, + }]) + await intro.wait() + await call.connect( + devices=[[{ + "type": "phone", + "params": {"to_number": "", "timeout": 30}, + }]], + ringback=[{"type": "ringtone", "params": {"name": "us"}}], + ) + await call.wait_for_ended() + +client.run() +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as inbound-call.mjs, +// then run: node inbound-call.mjs +import { RelayClient } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "", + token: "", + contexts: ["inbound-calling"], +}); + +client.onCall(async (call) => { + await call.answer(); + const intro = await call.play([ + { type: "tts", text: "Thanks for calling SignalWire. Connecting you now." }, + ]); + await intro.wait(); + await call.connect( + [[{ type: "phone", params: { to_number: "", timeout: 30 } }]], + { ringback: [{ type: "ringtone", name: "us" }] }, + ); + await call.waitForEnded(); +}); + +await client.run(); +``` + + + +### Take a voicemail + +Play a greeting, record the caller after a beep, and stop when they go quiet or press the pound +key. + + +Confirm which parties must consent and announce the recording when required. + + +#### Take a voicemail via SWML + +[`record`][swml-record] runs in the foreground, so the document waits until the recording ends. +SignalWire posts the result to `status_url`, and `record_url` holds the recording's URL for +the rest of the document. + + + +```yaml +version: 1.0.0 +sections: + main: + - play: + url: 'say:Thanks for calling SignalWire. Leave a message after the beep, then press pound.' + - record: + format: mp3 + beep: true + end_silence_timeout: 3 + terminators: '#' + status_url: '' + - play: + url: 'say:Thanks, goodbye.' + - hangup: {} +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "play": { "url": "say:Thanks for calling SignalWire. Leave a message after the beep, then press pound." } }, + { + "record": { + "format": "mp3", + "beep": true, + "end_silence_timeout": 3, + "terminators": "#", + "status_url": "" + } + }, + { "play": { "url": "say:Thanks, goodbye." } }, + { "hangup": {} } + ] + } +} +``` + + + +#### Take a voicemail via WebSocket (Relay) + +Start `call.record()` after the greeting and read the recording URL from its `finished` event. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as inbound_call.py and run: python inbound_call.py +from signalwire.relay import RelayClient + +client = RelayClient( + project="", + token="", + contexts=["inbound-calling"], +) + +@client.on_call +async def handle_call(call): + await call.answer() + greeting = await call.play([{ + "type": "tts", + "params": {"text": "Thanks for calling SignalWire. Leave a message after the beep, then press pound."}, + }]) + await greeting.wait() + + recording = await call.record( + audio={ + "format": "mp3", + "beep": True, + "end_silence_timeout": 3, + "terminators": "#", + }, + ) + finished = await recording.wait() + print(f"Recording: {finished.url}") + + goodbye = await call.play([{"type": "tts", "params": {"text": "Thanks, goodbye."}}]) + await goodbye.wait() + await call.hangup() + +client.run() +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// Save as inbound-call.mts and run: npx tsx inbound-call.mts +import { RelayClient, RecordEvent } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "", + token: "", + contexts: ["inbound-calling"], +}); + +client.onCall(async (call) => { + await call.answer(); + const greeting = await call.play([ + { type: "tts", text: "Thanks for calling SignalWire. Leave a message after the beep, then press pound." }, + ]); + await greeting.wait(); + + const recording = await call.record({ + format: "mp3", + beep: true, + end_silence_timeout: 3, + terminators: "#", + }); + const finished = await recording.wait(); + console.log(`Recording: ${(finished as RecordEvent).url}`); + + const goodbye = await call.play([{ type: "tts", text: "Thanks, goodbye." }]); + await goodbye.wait(); + await call.hangup(); +}); + +await client.run(); +``` + + + +### Record the call + +Record both sides of an inbound call in the background while it's forwarded to +``, and retrieve the finished recording URL. + +Announce the recording where consent rules require it, as in the voicemail example above. + +#### Record the call via SWML + +Start [`record_call`][swml-record-call] before [`connect`][swml-connect]. Recording continues +while the two legs talk, and SignalWire posts the result to `status_url` when the call ends. + + + +```yaml +version: 1.0.0 +sections: + main: + - play: + url: 'say:This call may be recorded. Connecting you now.' + - record_call: + format: mp3 + direction: both + stereo: true + status_url: '' + - connect: + to: '' +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "play": { "url": "say:This call may be recorded. Connecting you now." } }, + { + "record_call": { + "format": "mp3", + "direction": "both", + "stereo": true, + "status_url": "" + } + }, + { "connect": { "to": "" } } + ] + } +} +``` + + + +#### Record the call via WebSocket (Relay) + +Start `call.record()` with both directions, bridge the call, and read the URL after the call +ends. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as inbound_call.py and run: python inbound_call.py +from signalwire.relay import RelayClient + +client = RelayClient( + project="", + token="", + contexts=["inbound-calling"], +) + +@client.on_call +async def handle_call(call): + await call.answer() + notice = await call.play([{ + "type": "tts", + "params": {"text": "This call may be recorded. Connecting you now."}, + }]) + await notice.wait() + + recording = await call.record( + audio={ + "format": "mp3", + "direction": "both", + "stereo": True, + "initial_timeout": 0, + "end_silence_timeout": 0, + }, + ) + await call.connect([[{ + "type": "phone", + "params": {"to_number": "", "timeout": 30}, + }]]) + await call.wait_for_ended() + finished = await recording.wait() + print(f"Recording: {finished.url}") + +client.run() +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// Save as inbound-call.mts and run: npx tsx inbound-call.mts +import { RelayClient, RecordEvent } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "", + token: "", + contexts: ["inbound-calling"], +}); + +client.onCall(async (call) => { + await call.answer(); + const notice = await call.play([ + { type: "tts", text: "This call may be recorded. Connecting you now." }, + ]); + await notice.wait(); + + const recording = await call.record({ + format: "mp3", + direction: "both", + stereo: true, + initial_timeout: 0, + end_silence_timeout: 0, + }); + await call.connect([[ + { type: "phone", params: { to_number: "", timeout: 30 } }, + ]]); + await call.waitForEnded(); + const finished = await recording.wait(); + console.log(`Recording: ${(finished as RecordEvent).url}`); +}); + +await client.run(); +``` + + + +### Stream the call audio + +Stream both sides of a live inbound call to your secure WebSocket endpoint for real-time +processing. + +#### Stream call audio via SWML + +Start [`stream`][swml-stream] in the background and send its status events to your webhook. +The stream runs for as long as the rest of the document does, so follow it with the methods +that hold the call, such as `ai` or `connect`. + + + +```yaml +version: 1.0.0 +sections: + main: + - stream: + url: '' + track: both_tracks + codec: PCMU + status_url: '' + - play: + url: 'say:Thanks for calling SignalWire. Connecting you now.' + - connect: + to: '' +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { + "stream": { + "url": "", + "track": "both_tracks", + "codec": "PCMU", + "status_url": "" + } + }, + { "play": { "url": "say:Thanks for calling SignalWire. Connecting you now." } }, + { "connect": { "to": "" } } + ] + } +} +``` + + + +#### Stream call audio via WebSocket (Relay) + +Start `call.stream()` right after answering and keep it running until the call ends. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as inbound_call.py and run: python inbound_call.py +from signalwire.relay import RelayClient + +client = RelayClient( + project="", + token="", + contexts=["inbound-calling"], +) + +@client.on_call +async def handle_call(call): + await call.answer() + stream = await call.stream( + url="", + track="both_tracks", + codec="PCMU", + custom_parameters={"session_id": ""}, + ) + print(f"Streaming audio, control ID {stream.control_id}") + + intro = await call.play([{ + "type": "tts", + "params": {"text": "Thanks for calling SignalWire. Connecting you now."}, + }]) + await intro.wait() + await call.connect([[{ + "type": "phone", + "params": {"to_number": "", "timeout": 30}, + }]]) + await call.wait_for_ended() + +client.run() +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as inbound-call.mjs, +// then run: node inbound-call.mjs +import { RelayClient } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "", + token: "", + contexts: ["inbound-calling"], +}); + +client.onCall(async (call) => { + await call.answer(); + const stream = await call.stream("", { + track: "both_tracks", + codec: "PCMU", + customParameters: { session_id: "" }, + }); + console.log(`Streaming audio, control ID ${stream.controlId}`); + + const intro = await call.play([ + { type: "tts", text: "Thanks for calling SignalWire. Connecting you now." }, + ]); + await intro.wait(); + await call.connect([[ + { type: "phone", params: { to_number: "", timeout: 30 } }, + ]]); + await call.waitForEnded(); +}); + +await client.run(); +``` + + + +### Answer in the browser + +Receive the call on a web page as the Subscriber the number is assigned to. The page fetches a +[Subscriber token][subscriber-token] from your backend, comes online, and shows a ringing state +until the user answers or declines. Serve the page over HTTPS or `localhost` so the browser can +access the microphone. + + + +```html + + + + + Answer with SignalWire + + +

Offline

+

+ + + + + + + +``` +
+ +```javascript +// Install: npm install @signalwire/js@latest rxjs +// Save as answer.js next to the page above. +// GET /api/subscriber-token is your own endpoint: it creates a Subscriber +// token for the signed-in user with your Project API token and returns it +// as {"token": "..."}. +import { SignalWire, StaticCredentialProvider } from "@signalwire/js"; + +const statusLine = document.querySelector("#status"); +const callerLine = document.querySelector("#caller"); +const remoteAudio = document.querySelector("#remote-audio"); +const answerButton = document.querySelector("#answer"); +const declineButton = document.querySelector("#decline"); +const hangupButton = document.querySelector("#hangup"); +const finalStatuses = new Set(["disconnected", "failed", "destroyed"]); + +let currentCall = null; + +function reset() { + remoteAudio.srcObject = null; + callerLine.textContent = ""; + answerButton.disabled = true; + declineButton.disabled = true; + hangupButton.disabled = true; +} + +async function comeOnline() { + const response = await fetch("/api/subscriber-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"); + + const client = new SignalWire(new StaticCredentialProvider({ token })); + await client.register(); + statusLine.textContent = "Online"; + + client.session.incomingCalls$.subscribe((calls) => { + const ringing = calls.find((call) => call.status === "ringing"); + if (!ringing || ringing === currentCall) return; + currentCall = ringing; + + const callerName = + ringing.fromName && ringing.fromName !== "_undef_" ? ringing.fromName : ringing.from; + callerLine.textContent = `Incoming call from ${callerName}`; + answerButton.disabled = false; + declineButton.disabled = false; + + ringing.remoteStream$.subscribe((stream) => (remoteAudio.srcObject = stream)); + ringing.status$.subscribe((status) => { + statusLine.textContent = status; + if (status !== "ringing") { + answerButton.disabled = true; + declineButton.disabled = true; + } + if (status === "connected") hangupButton.disabled = false; + if (finalStatuses.has(status)) { + if (currentCall === ringing) currentCall = null; + reset(); + statusLine.textContent = "Online"; + } + }); + }); +} + +answerButton.onclick = () => { + void currentCall?.answer({ audio: true, video: false }); +}; +declineButton.onclick = () => { + void currentCall?.reject(); +}; +hangupButton.onclick = () => { + void currentCall?.hangup().catch(console.error); +}; + +comeOnline().catch((error) => { + statusLine.textContent = "Failed to come online"; + console.error(error); +}); +``` + +
+ +#### Choose what the user sends back + +`answer()` takes the same `audio` and `video` options as an outbound `dial()`. Audio defaults +to on and video to off, so `answer()` with no options is a phone-style call. + + + + +```javascript +currentCall.answer({ audio: true, video: false }); +``` + +A phone-style call, with no camera permission prompt. Bind `remoteStream$` to an ` + + +```javascript +currentCall.answer({ audio: true, video: true }); +``` + +A standard video call. Bind `localStream$` to a muted ` + + +```javascript +currentCall.answer({ audio: false, video: true }); +``` + +Joins on camera with the microphone muted, for a kiosk or a viewer who watches without speaking. + + + + +To pin the microphone, camera, or speaker across every call, use the +[device management APIs][browser-device-management]. + +Watch the browser console as the call arrives: the call is `ringing` until the user acts, then +moves through `connecting` to `connected`, and through `disconnecting`, `disconnected`, and +`destroyed` once it ends. If the page comes online but never rings, the number is assigned to a +different Subscriber than the one the token was issued for. If `register()` rejects, the token +is a guest or embed token, which can't receive calls. See the [authentication guide][browser-auth] +for the token lifecycle. + +`hangup()` ends the call for everyone. To leave the page but keep the call alive on the platform, +use [`transfer()`][browser-transfer] instead. For the full receiver walkthrough, two callers +ringing at once, and a test dial from the REST API, see the [inbound calls guide][browser-inbound]; +for mute, hold, and other in-call controls, see [call controls][browser-call-controls]. + +## Next steps + + + + Place calls from your backend or the browser, and choose what runs when someone answers. + + + Serve a different SWML document for every call and read the caller's details on your server. + + + Contexts, actions, events, and every call-control method in the Server SDKs. + + + Configure webhooks for phone numbers and understand callback reliability. + + diff --git a/fern/products/platform/pages/calling/voice/overview.mdx b/fern/products/platform/pages/calling/voice/overview.mdx index 10c58c1a15..137908d92c 100644 --- a/fern/products/platform/pages/calling/voice/overview.mdx +++ b/fern/products/platform/pages/calling/voice/overview.mdx @@ -24,6 +24,9 @@ Whether building a UCaaS solution, modernizing a legacy IVR, augmenting CX with Dial from your backend or the browser, and choose what runs when someone answers + + Point a number at SWML, a Relay app, or the browser, and choose what runs when it rings + Get started with our Compatibility API From fb0bcfc782e678476a966407c25329ec66eff9e9 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 16 Sep 2026 08:31:26 -0400 Subject: [PATCH 02/14] docs(platform): expand Addresses guide and point inbound calling at it Turn the Addresses page from a one-snippet stub into the reference for how a Resource is reached: phone numbers, SIP addresses, and aliases; contexts and channels; how SignalWire resolves an address; one Resource with many addresses; and how to manage addresses in the Dashboard, the REST API, and the Browser SDK. Add a shared snippet for assigning a Resource to a phone number, use it from Resources, Addresses, and the inbound calling guide, and trim the guide's addressing section to a summary that links here. --- .../pages/calling/voice/inbound-calling.mdx | 69 ++---- .../pages/platform/call-fabric/addresses.mdx | 218 +++++++++++++++++- .../pages/platform/call-fabric/resources.mdx | 34 ++- .../common/call-fabric/resource-addresses.md | 56 ----- .../dashboard/_assign-resource-to-number.mdx | 16 ++ 5 files changed, 274 insertions(+), 119 deletions(-) delete mode 100644 fern/snippets/common/call-fabric/resource-addresses.md create mode 100644 fern/snippets/common/dashboard/_assign-resource-to-number.mdx diff --git a/fern/products/platform/pages/calling/voice/inbound-calling.mdx b/fern/products/platform/pages/calling/voice/inbound-calling.mdx index 9f40b506b4..d756afb92b 100644 --- a/fern/products/platform/pages/calling/voice/inbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/inbound-calling.mdx @@ -76,24 +76,12 @@ phone you'll call from, or upgrade the project, before you dial. ## How a call reaches your code Everything that can handle a call in your Space is a [Resource][resources]: a SWML Script, a -Relay Application, a Subscriber, an AI Agent. Callers never dial the Resource itself. They dial -one of its [addresses][addresses], and SignalWire hands the call to the Resource behind it. A -Resource can have several addresses, and you can move an address to a different Resource later -without changing any code. - -| Address type | Looks like | Who can reach it | -|---|---|---| -| Phone number | `+12025550123` | Anyone on the phone network. Each number has one call handler and one message handler, assigned separately. | -| SIP address | `sip:@-.dapp.signalwire.com` | SIP callers outside SignalWire. You choose the user and context parts, and can require a password or an IP allowlist. | -| Alias | `/public/support` or `/private/john-doe` | Your own SWML, Browser SDK clients, and REST dials. A `public` alias is reachable by anyone, a `private` one only by authenticated users. | - -SignalWire creates one alias from the Resource's name when you create it. Add more to expose the -Resource under other names, or to limit an alias to the audio, video, or messaging channels. - -In the Dashboard, a Resource's **Addresses & Phone Numbers** tab lists its addresses and adds new -ones, and the Space-wide **Addresses** page lists every address with its context, type, call -handler, and message handler. An unassigned phone number appears there with no call handler, -and nothing happens on an inbound call until you assign one. +Relay Application, a Subscriber, an AI Agent. Callers don't dial the Resource itself. They dial +one of its [addresses][addresses], a phone number, a SIP address, or an alias such as +`/public/support`, and SignalWire hands the call to the Resource assigned to that address. A +phone number's call handler is assigned separately from its message handler, and the number takes +no action until you assign one. The [Addresses guide][addresses] explains each address type, +contexts, and how to add or move addresses. The Resource type decides how you control the call: @@ -377,10 +365,7 @@ online so calls can reach them. Leave the page open while you test. ### Point your number at the handler Assign a Resource as the number's call handler. Create the Resource first if your handler needs -one. You can assign from either side: from the number's **Edit Settings** page as shown below, -or from the Resource by opening its **Addresses & Phone Numbers** tab, selecting **+ Add**, then -**Phone Number**, and choosing a number you own. The same menu adds a **SIP Address** or an -**Alias** to the Resource. +one. @@ -401,18 +386,9 @@ document instead, choose **Hosted Script** and paste it into **Primary Script**. -Then open **Phone Numbers**, select your number, and select **Edit Settings**. Under **Inbound -Call Settings**, select **Assign Resource**, choose the SWML Script you created, and save. - - - - +Then assign the SWML Script to your number. -![A phone number's settings page showing the Assign Resource option under Inbound Call Settings](/assets/images/dashboard/phone-numbers/assign-resource-voice.webp) - - - - + You can do the same with the REST API. [Update the phone number][rest-update-number] with the `relay_script` handler. This creates the SWML Script Resource for the URL and assigns it in one @@ -504,18 +480,9 @@ In the Dashboard, open **My Resources**, select **+ Add**, then **Relay Applicat a name, enter `inbound-calling` as the **Topic** to match the `contexts` value in your code, and select **Create**. -Then open **Phone Numbers**, select your number, and select **Edit Settings**. Under **Inbound -Call Settings**, select **Assign Resource**, choose the Relay Application, and save. - - - - - -![A phone number's settings page showing the Assign Resource option under Inbound Call Settings](/assets/images/dashboard/phone-numbers/assign-resource-voice.webp) - - +Then assign the Relay Application to your number. - + To assign with the REST API instead, [update the phone number][rest-update-number] with the `relay_topic` handler and the topic name, or use the [`set_relay_topic`][py-set-relay-topic] and @@ -566,19 +533,9 @@ await client.phoneNumbers.setRelayTopic("", { topic: "inbo -Assign the number to the Subscriber your token was issued for. In the Dashboard, open -**Phone Numbers**, select your number, and select **Edit Settings**. Under **Inbound Call -Settings**, select **Assign Resource**, choose the Subscriber, and save. - - - - - -![A phone number's settings page showing the Assign Resource option under Inbound Call Settings](/assets/images/dashboard/phone-numbers/assign-resource-voice.webp) +Assign the number to the Subscriber your token was issued for. - - - + To assign with the REST API instead, [link the Subscriber to the number][rest-link-number]. This endpoint is in beta and requires the number's calling channel to have no Resource assigned yet. diff --git a/fern/products/platform/pages/platform/call-fabric/addresses.mdx b/fern/products/platform/pages/platform/call-fabric/addresses.mdx index f2c231112d..21de928a70 100644 --- a/fern/products/platform/pages/platform/call-fabric/addresses.mdx +++ b/fern/products/platform/pages/platform/call-fabric/addresses.mdx @@ -2,6 +2,222 @@ id: c2342628-3776-42ed-91ab-386446fea03e title: Addresses slug: /addresses +description: How addresses connect callers to Resources. Phone numbers, SIP addresses, and aliases, their contexts and channels, and how to add, assign, and move them. +max-toc-depth: 3 --- - +[resources]: /docs/platform/resources +[subscribers]: /docs/platform/subscribers +[phone-numbers]: /docs/platform/phone-numbers +[inbound-calling]: /docs/platform/voice/inbound-calling +[outbound-calling]: /docs/platform/voice/outbound-calling +[sip-credentials]: /docs/platform/voice/sip/sip-credentials +[sip-gateways]: /docs/platform/voice/sip#sip-gateways +[byoc]: /docs/platform/voice/sip/bring-your-own-carrier +[swml-connect]: /docs/swml/reference/calling/connect +[browser-address-book]: /docs/browser-sdk/v4/guides/address-book +[browser-default-channel]: /docs/browser-sdk/v4/reference/address/default-channel +[rest-update-number]: /docs/apis/rest/phone-numbers/update-phone-number +[rest-phone-route]: /docs/apis/rest/phone-routes/assign-resource-phone-route +[rest-number-addresses]: /docs/apis/rest/phone-number-addresses/list-phone-number-addresses +[rest-link-number]: /docs/apis/rest/phone-number-addresses/create-phone-number-address +[rest-create-alias]: /docs/apis/rest/alias-addresses/create-alias-address +[rest-update-alias]: /docs/apis/rest/alias-addresses/update-alias-address +[rest-delete-alias]: /docs/apis/rest/alias-addresses/delete-alias-address +[rest-create-sip]: /docs/apis/rest/sip-addresses/create-sip-address +[rest-update-sip]: /docs/apis/rest/sip-addresses/update-sip-address +[rest-list-addresses]: /docs/apis/rest/addresses/list-resource-addresses +[rest-list-addresses-client]: /docs/apis/rest/addresses/list-resource-addresses-client +[subscriber-token]: /docs/apis/rest/subscribers/tokens/create-subscriber-token + +A [Resource][resources] is what handles a call or message. An address is how anyone reaches it. +Callers never dial a Resource directly: they dial a phone number, a SIP URI, or a name such as +`/public/support`, and SignalWire hands the call to the Resource assigned to that address. + +Every Resource gets one address when you create it, and most end up with several. The same +addresses are what your own code uses to name things: the `to` of a REST dial, the destination +of [`connect`][swml-connect] in SWML, or the argument to `dial()` in the Browser SDK. + +## Address types + +| Type | Looks like | Who dials it | +|---|---|---| +| [Phone number](#phone-numbers) | `+12025550123` | Anyone on the phone network | +| [SIP address](#sip-addresses) | `sip:*@acme-public.dapp.signalwire.com` | SIP devices, PBXs, and carriers outside SignalWire | +| [Alias](#aliases) | `/public/support`, `/private/john-doe` | Your scripts, Browser SDK clients, REST dials, and other Resources | + +### Phone numbers + +A [phone number you've bought or ported][phone-numbers] is an address in the `External` context. +It has two handlers, assigned independently: a call handler for inbound calls and a message +handler for inbound messages. The Dashboard shows them as **Inbound Call Settings** and **Inbound +Message Settings** on the number's **Edit Settings** page. A number with no call handler rings +nobody; nothing happens on an inbound call until you assign a Resource. + +Any Resource that can take a call can be a number's call handler: a SWML Script, an AI Agent, a +Call Flow, a Relay Application, a Subscriber, or a SIP Credential. Only messaging-capable +Resources can be its message handler. + +### SIP addresses + +A SIP address gives a Resource its own SIP URI so devices and systems outside SignalWire, such as +a PBX or your own carrier, can send it calls. SignalWire builds the host for you: + +```text +sip:@-.dapp.signalwire.com +``` + +You choose the user and the domain. The user defaults to `*`, which accepts any username, so +`sip:anything@acme-public.dapp.signalwire.com` reaches the same Resource. The domain groups +addresses and is one of your Space's contexts, `public` by default. You can also require a +registration password, restrict callers to an IP allowlist, and set the encryption, codecs, and +ciphers offered on the call. + +A SIP address routes inbound SIP only. It's different from a [SIP Credential][sip-credentials], +which is a Resource your own SIP devices register to and place calls from, and from a +[SIP gateway][sip-gateways], which forwards calls out to an external SIP destination. To bring +calls in from your own carrier, give the handling Resource a SIP address and point the carrier +at it; the [bring your own carrier][byoc] guide covers the full flow. + +### Aliases + +An alias is a name in the form `//`. SignalWire creates one from the Resource's +name when you create the Resource, so a Subscriber named `john.doe` is reachable at +`/private/john-doe` and an AI Agent named `Support Agent` at `/public/support-agent`. Names are +lowercase letters, numbers, underscores, and dashes. + +Add more aliases to expose the same Resource under other names, or to shape how it's reached: + +- **Channels** limit an alias to `audio`, `video`, `messaging`, or any combination. A + messaging-only public alias gives a chat entry point to an agent that also takes voice calls + through a private one. +- **Display As** controls how the alias appears to client applications that browse the + directory: as an app, a room, a call, or a subscriber. A script that dispatches callers to + your staff can present itself as a subscriber so callers feel they're dialing a person. + +An alias can be pointed at a different Resource later, which is the easiest way to swap a +handler without changing anything that dials it. In the Dashboard, open the alias and change +its assigned Resource. The REST API doesn't change an alias's Resource in place; delete the +alias and [create a new one][rest-create-alias] with the new `resource_id`. + +The Dashboard can also attach a WhatsApp business number to a Resource so it handles inbound +WhatsApp calls and messages. It appears alongside the other types in the Add an Address menu. + +## Contexts + +An alias lives in a context, and the context decides who can reach it: + +- **`public`** addresses are reachable by anyone, including unauthenticated callers. Use them for + entry points such as a support agent behind a click-to-call button. +- **`private`** addresses are reachable only by authenticated users, which makes them the home of + [Subscribers][subscribers] and anything only your own users should dial. + +SIP addresses use the same contexts as their domain, and phone numbers sit in the `External` +context. Your Space may show additional contexts in the Dashboard. + + +From inside a context, you can omit it. A Subscriber dialing from `private` reaches `/private/bob` +as `/bob`. + + +## How SignalWire resolves an address + +When a call or message arrives, SignalWire looks up the address it was sent to, checks that the +address accepts that channel, and hands the call to the Resource assigned as the handler for +that channel. The Resource type decides what happens next: a SWML Script runs its document, a +Relay Application delivers the call to your connected Server SDK client, and a Subscriber rings +that user's devices. + +```mermaid +flowchart LR + caller["Caller or message"] --> address["Address dialed
phone number, SIP URI, or alias"] + address --> channel{"Channel allowed?
calling or messaging"} + channel -- no --> rejected["Not delivered"] + channel -- yes --> handler["Handler Resource for that channel"] + handler --> swml["SWML Script: runs the document"] + handler --> relay["Relay Application: delivers to your Server SDK client"] + handler --> subscriber["Subscriber: rings the user's devices"] +``` + +Because the Resource is what handles the call, the same logic runs whether the caller dialed +the phone number, the SIP address, or an alias. See [inbound calling][inbound-calling] for the +handler code itself. + +## One Resource, many addresses + +Give one Resource every address its callers need rather than duplicating the Resource per +channel. A support agent might carry: + +| Address | Purpose | +|---|---| +| `+12025550123` | Customers calling from the phone network | +| `sip:*@acme-public.dapp.signalwire.com` | Your PBX or carrier sending SIP calls | +| `/public/support` | The click-to-call widget on your website | +| `/private/support` | Staff dialing from the Browser SDK or a SIP phone | + +When you ship a new version of the agent, point the phone number and aliases at the new Resource. +Every caller moves over, and nothing that dials `/public/support` changes. + +## Manage addresses + +### In the Dashboard + +**From the Resource.** Open the Resource from **My Resources** and select its **Addresses & +Phone Numbers** tab. It lists the Resource's addresses with their channels and type. Select +**+ Add** and choose **Phone Number**, **SIP Address**, or **Alias**. Phone Number lists the +numbers you own and offers to buy one; SIP Address and Alias open the forms described above. + + + + + +![The Add an Address dialog in the Dashboard with Phone Number, SIP Address, and Alias options](/assets/images/dashboard/resources/ai-agent/addresses/add-address.webp) + + + + + +**From the phone number.** When you're starting from a number rather than a Resource, assign the +handler from the number's settings. + + + +**Across the Space.** The **Addresses** page in the left sidebar lists every address in the +project with its context, type, call handler, and message handler. It's the quickest way to find +a number with no handler or to see which Resource an alias points at. + +### With the REST API + +| Task | Endpoint | +|---|---| +| Route a number to a handler by type, for example a SWML URL or a Relay topic | [Update phone number][rest-update-number] | +| Route a number to an existing Resource, from the Resource's side | [Assign Resource to phone route][rest-phone-route] | +| List a number's calling and messaging addresses, link a Resource to one, or re-point it | [Phone number addresses][rest-number-addresses] (beta) | +| Create, rename, re-scope, or delete an alias | [Create][rest-create-alias], [update][rest-update-alias], and [delete][rest-delete-alias] alias address | +| Create or change a SIP address | [Create][rest-create-sip] and [update][rest-update-sip] SIP address | +| List every address a project can reach | [List Resource Addresses][rest-list-addresses] | +| List the addresses one Subscriber can reach, with a Subscriber token | [List Resource Addresses from a Client][rest-list-addresses-client] | + +### In the Browser SDK + +A client authenticated with a [Subscriber token][subscriber-token] sees the addresses its user is +allowed to reach through `client.directory`. Each entry exposes a ready-to-dial URI per channel, +so the client dials what the directory returns instead of building `/context/name` strings by +hand. See the [address book guide][browser-address-book] and [`defaultChannel`][browser-default-channel]. + +## Next steps + + + + The handler types an address can point at, and how to create and manage them. + + + Assign a number and write the code that answers when it rings. + + + Users with private addresses that ring their browser, mobile, or SIP devices. + + + Buy, port, verify, and manage the numbers you assign to Resources. + + diff --git a/fern/products/platform/pages/platform/call-fabric/resources.mdx b/fern/products/platform/pages/platform/call-fabric/resources.mdx index 6a6f6e77e5..925021499b 100644 --- a/fern/products/platform/pages/platform/call-fabric/resources.mdx +++ b/fern/products/platform/pages/platform/call-fabric/resources.mdx @@ -47,11 +47,23 @@ Each of these Resource types use the same addressing and management patterns des ## Resource addresses -Every Resource is uniquely identified by one or more **Addresses** in the form `/context/name`. -For example, an AI Agent named `Sigmond` in the public context is reachable at `/public/Sigmond`. -The context is either `public` or `private`, and the name defaults to the Resource's name. Addresses are mutable, and a single Resource can have multiple addresses, including phone numbers and SIP URIs. +A Resource is what handles a call or message. An **address** is how anyone reaches it, and a +Resource can have several: -Learn more in the [Resource Addresses](/docs/platform/addresses) guide, including contexts, naming conventions, and routing options. +- A **phone number** you've bought or ported, for callers on the phone network. +- A **SIP address**, a SIP URI SignalWire assigns to the Resource, for PBXs and carriers outside + SignalWire. +- An **alias** in the form `/context/name`, such as `/public/support` or `/private/john-doe`, + for your own scripts, Browser SDK clients, and REST dials. SignalWire creates one from the + Resource's name when you create it. + +The context of an alias decides who can reach it: `public` addresses are open to anyone, `private` +ones only to authenticated users. Addresses are mutable, so you can point a phone number or an +alias at a different Resource later without changing the code that dials it. + +The [Addresses guide](/docs/platform/addresses) explains each type in depth, how SignalWire +resolves an address to a handler, and how to add, assign, and move addresses in the Dashboard +and the REST API. ## Manage Resources @@ -117,6 +129,16 @@ Alternatively, you can create new Resources from their subpages in the sidebar m After selecting a Resource, enter the details for the Resource on a new page, and select Create to confirm. +#### Give it an address + +A new Resource is reachable at the alias SignalWire creates from its name. To take calls from the +phone network, assign it as a phone number's call handler. + + + +To add a SIP address or another alias instead, open the Resource's **Addresses & Phone Numbers** +tab and select **+ Add**. The [Addresses guide](/docs/platform/addresses) covers every option. + If you don't see the **My Resources** tab, your SignalWire Space is on the **Legacy Dashboard**, which does not support the Resources experience. [Contact Support](https://support.signalwire.com/) @@ -148,8 +170,8 @@ The **My Resources** page lists each Resource's name, type, and last update time Manage the users in your communication system via subscribers. - - Understand contexts, naming, and how Resources are identified and called. + + Phone numbers, SIP addresses, and aliases: how a Resource is reached and how to assign them. List, retrieve, and delete Resources programmatically. diff --git a/fern/snippets/common/call-fabric/resource-addresses.md b/fern/snippets/common/call-fabric/resource-addresses.md deleted file mode 100644 index 93158b9a2a..0000000000 --- a/fern/snippets/common/call-fabric/resource-addresses.md +++ /dev/null @@ -1,56 +0,0 @@ -{/* Shared component: Resource Addresses */} - -Each **Resource** is uniquely identified by its **Address**, allowing for precise targeting and interaction within the Call Fabric ecosystem. -This simplifies the development process by providing a standardized way to handle different communication elements, and -enhances flexibility, as developers can interact with a wide range of communication tools using a unified approach. - - - -Resources can have **multiple addresses**, and addresses are **mutable**. -For instance, you can map a SWML script and a Video Room to the same Resource Address. -These addresses can be changed or deleted later as needed. - - - -## Types - -Call Fabric supports multiple address types to accommodate different communication protocols and use cases: - -- **Phone numbers**: Traditional phone numbers in E.164 format (e.g., `+14155551234`) that can be mapped to resources for PSTN connectivity. -- **SIP addresses**: SIP URIs for VoIP communications (e.g., `sip:user@domain.com`) enabling direct SIP endpoint connections. -- **Alias**: Custom names that provide alternative addressing for resources, making them easier to remember and use (e.g., `/support-queue` or `/main-conference`). - -Each address type follows the same context and naming conventions described below, allowing seamless integration across different communication channels. - -Each **Resource Address** has two components: - -- **Context**: Identifies the path of the address. Currently can be `public` or `private`. -- **Name**: By default the name of the address will be the name of the resource, however, - a user can also change or add an `alias` of an address. - -For example, the address for an `AI Agent` resource named `Alice-AI` in the `public` context would be `/public/Alice-AI`. -If you were to change the `alias` to `John-AI` the address would become `/public/John-AI`. - - -If you are interacting with a resource from within the same context, you can omit the context from the address. -For example, if you are interacting with a [Subscribers](/docs/platform/subscribers) resource named `Bob` from within the `private` context, -you can use the address `/Bob` instead of `/private/Bob`. - - -Once you have created a Resource, you can use the address to interact with it within the Call Fabric ecosystem. -Additionally, you can view the created resource in the `Resources` tab of the SignalWire Dashboard. -Here, you can view the address, type, and other details of the resource. - - - - - The Resources page of the SignalWire Dashboard. - - - - - - -The **My Resources** page lists each Resource's name, type, and last update time and provides search and type filters. - - diff --git a/fern/snippets/common/dashboard/_assign-resource-to-number.mdx b/fern/snippets/common/dashboard/_assign-resource-to-number.mdx new file mode 100644 index 0000000000..39e86aadd2 --- /dev/null +++ b/fern/snippets/common/dashboard/_assign-resource-to-number.mdx @@ -0,0 +1,16 @@ +{/* Shared component: assign a Resource as a phone number's call handler in the Dashboard */} + +Open **Phone Numbers**, select the number, and select **Edit Settings**. Under **Inbound Call +Settings**, select **Assign Resource**, choose the Resource, and save. To assign from the Resource +instead, open its **Addresses & Phone Numbers** tab, select **+ Add**, then **Phone Number**, and +pick a number you own. + + + + + +![A phone number's Edit page in the Dashboard showing the Assign Resource button under Inbound Call Settings](/assets/images/dashboard/phone-numbers/assign-resource-voice.webp) + + + + From 4b2b9db7a00702f1dbd7d46dd39ffffa94827217 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 16 Sep 2026 08:58:06 -0400 Subject: [PATCH 03/14] docs(voice): split inbound calling into four approaches Frame the guide around where the logic runs and how SignalWire reaches it: SWML over HTTP (Server SDK), serverless SWML (hosted in the Space), WebSocket from your server (Relay), and WebSocket from the browser. Give serverless SWML its own tab in the handler and assignment steps instead of an aside and accordion, compare all four in one table, and state that only Subscriber tokens receive calls in the browser while guest and embed tokens are outbound-only. --- .../pages/calling/voice/inbound-calling.mdx | 146 +++++++++++++----- 1 file changed, 104 insertions(+), 42 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/inbound-calling.mdx b/fern/products/platform/pages/calling/voice/inbound-calling.mdx index d756afb92b..87da0bfca0 100644 --- a/fern/products/platform/pages/calling/voice/inbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/inbound-calling.mdx @@ -104,23 +104,34 @@ Play a short announcement to anyone who dials your number. ### Choose how to handle the call -Choose the approach that fits how you want to control the call. - -| What you want to do | Where to start | -|---|---| -| Return call instructions from your server as a document, built with a Server SDK on every call | [SWML](#create-your-call-handler), served by a Server SDK | -| Control the call in real time, receiving events and sending commands over a persistent WebSocket connection | [WebSocket (Relay)](#create-your-call-handler), using a Server SDK | -| Let someone answer and speak on the call from your web app | [Browser SDK](#create-your-call-handler) | - -Each approach answers the same call, but they differ in how you follow and control it afterward. - -| Function | SWML | WebSocket (Relay) | Browser SDK | -|---|---|---|---| -| Return call instructions from your Server SDK app without holding a connection open | | | | -| Command a call already in progress from any process, by its call ID | | | | -| React to events in your own code while the call is live, with no public URL | | | | -| Answer from a web page, with the user speaking on the call | | | | -| Start an AI agent, record, stream, or bridge the call from code | | | | +Inbound handling comes down to two questions: where does your logic run, and how does SignalWire +reach it? Four approaches answer them differently. + +| Approach | How SignalWire reaches your logic | Where your logic runs | +|---|---|---| +| [SWML over HTTP](#create-your-call-handler) | Requests a SWML document from your server on every call, then runs it | Your server, built with a Server SDK | +| [Serverless SWML](#create-your-call-handler) | Runs a SWML document you host in your Space. No request reaches you. | SignalWire | +| [WebSocket (Relay)](#create-your-call-handler) | Delivers the call over a persistent WebSocket your server holds open. Your code answers it and controls it command by command. | Your server, with a Server SDK Relay client | +| [Browser SDK](#create-your-call-handler) | Delivers the call over the persistent connection a signed-in user's browser holds open. The user answers it. | The user's browser | + +The two SWML approaches run the same document and differ only in whether your server produces +it. The two WebSocket approaches share a transport but not a purpose: Relay puts your code on the +call, and the Browser SDK puts a person on it. + +| Function | SWML over HTTP | Serverless SWML | WebSocket (Relay) | Browser SDK | +|---|---|---|---|---| +| Nothing to run or expose on your side | | | | | +| Decide from your code what happens before the call is answered, such as looking the caller up | | | | | +| React to events in your own code while the call is live, with no public URL | | | | | +| A person answers and speaks on the call | | | | | +| Start an AI agent, record, stream, or bridge the call | | | | | +| Command the call from another process by its call ID | | | | | + + +A Browser SDK client receives calls only when it authenticates with a Subscriber token issued for +a specific Subscriber, and that Subscriber is the number's call handler. Guest tokens and embed +tokens, which power click-to-call widgets, are outbound-only and never ring. + The REST Calling API places calls; it doesn't answer them. To act on an inbound call from REST later, use the call ID that SWML or Relay gives you. @@ -143,7 +154,7 @@ Replace these values in the code sample you choose: Write the code SignalWire runs, or connects to, when the number rings. - + A SWML document tells SignalWire what to do with the call. When the number rings, SignalWire requests the document from your server and runs it. Build and serve it with a Server SDK, @@ -196,8 +207,7 @@ Anyone who learns the URL can fetch your document, so [verify the request signat before you serve anything sensitive. Both servers return this document. It plays the announcement, then hangs up when the document -ends. If you'd rather not run a server yet, you can paste it into a hosted SWML Script in the -next step instead, and the [SWML quickstart][swml-quickstart] covers that path. +ends. @@ -223,6 +233,46 @@ sections: + + + +Serverless SWML runs the same document as SWML over HTTP, hosted in your Space instead of on +your server. There's nothing to run: in the next step you paste the document into a hosted SWML +Script, and SignalWire executes it on every call. Use it when the call logic is fixed, or to try +a document before you build the server. The [SWML quickstart][swml-quickstart] covers hosted +scripts in depth, and the AI Agent and Call Flow Resources are serverless handlers you build in +the Dashboard rather than as SWML. + +This document plays an announcement, then hangs up when it ends: + + + +```yaml +version: 1.0.0 +sections: + main: + - play: + url: 'say:Hello, welcome to SignalWire!' +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "play": { "url": "say:Hello, welcome to SignalWire!" } } + ] + } +} +``` + + + +A hosted document still sees the call: variables such as `${call.from}` resolve to the caller's +details, so it can react to who's calling without a server. See +[Read the call details via SWML](#read-the-call-details-via-swml). + @@ -288,8 +338,10 @@ next step, and only a client subscribed to it receives the call. -A Browser SDK client registers as a Subscriber and receives the call in the page. Show the -caller, then let the user answer or decline. +A Browser SDK client authenticates as a Subscriber, registers, and receives the call in the +page. Show the caller, then let the user answer or decline. The token must be a Subscriber token +for the Subscriber you'll assign the number to; a guest or embed token comes online but never +rings. ```javascript // Install: npm install @signalwire/js@latest rxjs @@ -368,13 +420,11 @@ Assign a Resource as the number's call handler. Create the Resource first if you one. - + In the Dashboard, open **My Resources**, select **+ Add**, then **Script**, then **SWML Script**. Give it a name and leave **Used For** set to **Calling**. Under **Handle Calls Using**, choose -**External URL** and enter your server's URL in **Primary Script URL**. To use the hosted -document instead, choose **Hosted Script** and paste it into **Primary Script**. Select -**Create**. +**External URL** and enter your server's URL in **Primary Script URL**. Select **Create**. @@ -438,11 +488,20 @@ await client.phoneNumbers.setSwmlWebhook("", " - + + + +In the Dashboard, open **My Resources**, select **+ Add**, then **Script**, then **SWML Script**. +Give it a name and leave **Used For** set to **Calling**. Under **Handle Calls Using**, choose +**Hosted Script** and paste the document into **Primary Script**. Select **Create**. -[Create the SWML Script][rest-create-script] with the document as `contents`, then -[link it to the number][rest-link-number]. The link endpoint is in beta and requires the -number's calling channel to have no Resource assigned yet. +Then assign the SWML Script to your number. + + + +To do the same with the REST API, [create the SWML Script][rest-create-script] with the document +as `contents`, then [link it to the number][rest-link-number]. The link endpoint is in beta and +requires the number's calling channel to have no Resource assigned yet. ```bash curl -X POST "https://.signalwire.com/api/fabric/resources/swml_scripts" \ @@ -471,8 +530,6 @@ curl -X POST "https://.signalwire.com/api/fabric/phone_number_addres }' ``` - - @@ -559,8 +616,8 @@ Space can call the same browser client without a phone number. ### Call your number -Dial your SignalWire number from your phone. With SWML or Relay, you hear "Hello, welcome to -SignalWire!" and the call ends. With the browser example, the page shows the incoming call. +Dial your SignalWire number from your phone. With either SWML approach or with Relay, you hear +"Hello, welcome to SignalWire!" and the call ends. With the browser example, the page shows the incoming call. Select **Answer**, allow microphone access, and select **Hang up** when you finish. If the call rings without an answer or fails, check these first: @@ -568,8 +625,10 @@ If the call rings without an answer or fails, check these first: - The number's **Inbound Call Settings** show your Resource. An unassigned number takes no action. - You're calling from a verified number if the project is in trial mode. - For Relay, the Relay Application's **Topic** matches your client's `contexts` value and the client is running. -- For SWML served from your server, SignalWire could reach the URL and got a valid document. Check the - call in the Dashboard's **Logs**. +- For SWML over HTTP, SignalWire could reach the URL and got a valid document. Check the call in + the Dashboard's **Logs**. +- For the browser, the token is a Subscriber token, not a guest or embed token, and the number is + assigned to that same Subscriber. @@ -580,13 +639,12 @@ section for the approach you used for your first call. ### Read the call details via SWML -When SignalWire fetches your document, it sends your server a POST request whose `call` object -carries the `from` and `to` addresses, the `direction`, and the `call_id`. See the -[webhook payload reference][swml-webhook-payload] for every field. +With SWML over HTTP, SignalWire sends your server a POST request whose `call` object carries the +`from` and `to` addresses, the `direction`, and the `call_id`, so your code can decide the +document per caller. See the [webhook payload reference][swml-webhook-payload] for every field. -The same fields are available inside the document as [variables][swml-variables], so the -document itself can use them, whether your server built it or you hosted it. This document reads -the caller's number back to them: +The same fields are available inside the document as [variables][swml-variables], so a hosted +document can use them too. This document reads the caller's number back to them: @@ -768,6 +826,10 @@ After the user answers, `status$` moves through `connecting` and `connected`, th ## Examples +Each example shows the SWML document, which works both served from your server and hosted in +your Space, and the Relay handler for your Server SDK client. The last example answers in the +browser. + ### Run an AI agent Start an AI agent that welcomes the caller and answers basic questions about SignalWire. From 612e69e5c67ac8d54e1928b49507d786348168ba Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 16 Sep 2026 09:04:15 -0400 Subject: [PATCH 04/14] docs(voice): stop assuming a phone number in the inbound calling guide Add a step that chooses how callers reach the handler: phone number, SIP address, or alias, independent of the handler choice. Split the old number-assignment step into creating the Resource (per handler) and giving it an address (per address type), with Dashboard and REST paths for each, and make the test-call step follow the chosen address. --- .../pages/calling/voice/inbound-calling.mdx | 315 ++++++++++++++---- 1 file changed, 245 insertions(+), 70 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/inbound-calling.mdx b/fern/products/platform/pages/calling/voice/inbound-calling.mdx index 87da0bfca0..58a079315f 100644 --- a/fern/products/platform/pages/calling/voice/inbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/inbound-calling.mdx @@ -1,7 +1,7 @@ --- title: Inbound calling slug: /voice/inbound-calling -description: Answer your first inbound phone call, find out who is calling, and add an AI agent, a phone menu, or browser answering to your application. +description: Answer your first inbound call to a phone number, SIP address, or alias, find out who is calling, and add an AI agent, a phone menu, or browser answering. max-toc-depth: 3 --- @@ -40,6 +40,13 @@ max-toc-depth: 3 [rest-update-number]: /docs/apis/rest/phone-numbers/update-phone-number [rest-list-numbers]: /docs/apis/rest/phone-numbers/list-phone-numbers [rest-create-script]: /docs/apis/rest/swml-scripts/create-swml-script +[rest-create-webhook]: /docs/apis/rest/swml-webhook/create-swml-webhook +[rest-create-relay-app]: /docs/apis/rest/relay-application/create-relay-application +[rest-create-subscriber]: /docs/apis/rest/subscribers/create-subscriber +[rest-create-sip]: /docs/apis/rest/sip-addresses/create-sip-address +[rest-create-alias]: /docs/apis/rest/alias-addresses/create-alias-address +[rest-calling-dial]: /docs/apis/rest/calls/call-commands +[outbound-calling]: /docs/platform/voice/outbound-calling [rest-link-number]: /docs/apis/rest/phone-number-addresses/create-phone-number-address [subscriber-token]: /docs/apis/rest/subscribers/tokens/create-subscriber-token [browser-auth]: /docs/browser-sdk/v4/guides/authentication @@ -51,9 +58,10 @@ max-toc-depth: 3 [browser-call]: /docs/browser-sdk/v4/reference/interfaces/call [browser-transfer]: /docs/browser-sdk/v4/reference/webrtc-call/transfer -Answer a call to your SignalWire phone number and choose what happens when it rings. Start by -playing a short announcement to yourself, then find out who is calling, run an AI agent, build a -phone menu, or let a signed-in user answer from your web app. +Answer a call to one of your SignalWire addresses, whether that's a phone number, a SIP address, +or an alias another client dials, and choose what happens when it rings. Start by playing a short +announcement to yourself, then find out who is calling, run an AI agent, build a phone menu, or +let a signed-in user answer from your web app. ## Prepare for your first call @@ -63,14 +71,17 @@ Have these values ready: - Your Project ID and API token from the Dashboard's [API credentials][api-credentials] page. Enable the token's **Voice** permission, and its **Numbers** permission if you assign the handler with the REST API rather than the Dashboard. -- A voice-capable [phone number purchased in your Space][phone-numbers]. +- A way for callers to reach you. A voice-capable [phone number purchased in your Space][phone-numbers] + is the usual choice, but a SIP address or an alias works without one. You'll pick in + [Choose how callers reach you](#choose-how-callers-reach-you). +- Something to place the test call from: a phone, a SIP softphone, or another SignalWire client, + depending on the address you pick. - If answering in the browser, a [Subscriber token][subscriber-token] for the [Subscriber][subscribers] - the number rings. Guest and embed tokens can't receive calls. -- A phone you can call from. + that will take the calls. Guest and embed tokens can't receive calls. - -A [trial project][trial-mode] receives calls only from phone numbers it has verified. Verify the -phone you'll call from, or upgrade the project, before you dial. + +A [trial project][trial-mode] receives calls to its phone numbers only from phone numbers it has +verified. Verify the phone you'll call from, or upgrade the project, before you dial. ## How a call reaches your code @@ -136,6 +147,21 @@ tokens, which power click-to-call widgets, are outbound-only and never ring. The REST Calling API places calls; it doesn't answer them. To act on an inbound call from REST later, use the call ID that SWML or Relay gives you. +### Choose how callers reach you + +Your handler is a Resource, and callers reach it through an [address][addresses]. Pick the +address that matches who's calling. The choice is independent of the handler: any Resource can +carry any of these, and one Resource can carry all three. + +| Address | Who dials it | You need | +|---|---|---| +| Phone number | Anyone on the phone network | A voice-capable number in your Space, and a phone to test from | +| SIP address | SIP softphones, PBXs, and carriers outside SignalWire, dialing the URI SignalWire assigns | A SIP client to test from | +| Alias | Other SignalWire clients and Resources: a Browser SDK `dial()`, a SWML `connect`, or a REST dial to `/public/` | A Browser SDK client or another Resource to test from | + +The steps below show the address you pick in its own tab, alongside the handler tabs. Stay on the +same tabs through the guide. + ### Set your credentials Replace these values in the code sample you choose: @@ -145,7 +171,9 @@ Replace these values in the code sample you choose: | `` | Your Space's subdomain in `.signalwire.com` | | `` | Your Project ID | | `` | Your API token | +| `` | Your SignalWire number in E.164 format, such as `+12025550123` | | `` | The ID shown on the phone number's page in the Dashboard, or returned by [List phone numbers][rest-list-numbers] | +| `` | The ID shown on your handler's Resource page, or returned when you create it with the REST API | | `` | The public URL where your server serves the SWML document, including the basic-auth credentials for the Python example | | `` | A [Subscriber token][subscriber-token] created by your backend, for the Browser SDK examples | @@ -414,10 +442,10 @@ online so calls can reach them. Leave the page open while you test. -### Point your number at the handler +### Create the Resource -Assign a Resource as the number's call handler. Create the Resource first if your handler needs -one. +Every handler is a Resource in your Space. Create it, then note its ID from the Resource's page +or from the REST response; you'll attach the address to it next. @@ -436,25 +464,17 @@ Give it a name and leave **Used For** set to **Calling**. Under **Handle Calls U -Then assign the SWML Script to your number. - - - -You can do the same with the REST API. [Update the phone number][rest-update-number] with the -`relay_script` handler. This creates the SWML Script Resource for the URL and assigns it in one -call; the Server SDKs wrap it as [`set_swml_webhook`][py-set-swml-webhook] and -[`setSwmlWebhook`][ts-set-swml-webhook]. Find the number's ID on its Dashboard page or with -[List phone numbers][rest-list-numbers]. +With the REST API, [create a SWML webhook][rest-create-webhook] pointing at your URL. - + ```bash -curl -X PUT "https://.signalwire.com/api/relay/rest/phone_numbers/" \ +curl -X POST "https://.signalwire.com/api/fabric/resources/swml_webhooks" \ -u ":" \ -H "Content-Type: application/json" \ -d '{ - "call_handler": "relay_script", - "call_relay_script_url": "" + "name": "Inbound welcome", + "primary_request_url": "" }' ``` @@ -469,7 +489,10 @@ client = RestClient( host=".signalwire.com", ) -client.phone_numbers.set_swml_webhook("", "") +resource = client.fabric.swml_webhooks.create( + name="Inbound welcome", primary_request_url="", +) +print(resource["id"]) ``` @@ -483,7 +506,11 @@ const client = new RestClient({ host: ".signalwire.com", }); -await client.phoneNumbers.setSwmlWebhook("", ""); +const resource = await client.fabric.swmlWebhooks.create({ + name: "Inbound welcome", + primary_request_url: "", +}); +console.log(resource.id); ``` @@ -495,13 +522,7 @@ In the Dashboard, open **My Resources**, select **+ Add**, then **Script**, then Give it a name and leave **Used For** set to **Calling**. Under **Handle Calls Using**, choose **Hosted Script** and paste the document into **Primary Script**. Select **Create**. -Then assign the SWML Script to your number. - - - -To do the same with the REST API, [create the SWML Script][rest-create-script] with the document -as `contents`, then [link it to the number][rest-link-number]. The link endpoint is in beta and -requires the number's calling channel to have no Resource assigned yet. +With the REST API, [create the SWML Script][rest-create-script] with the document as `contents`. ```bash curl -X POST "https://.signalwire.com/api/fabric/resources/swml_scripts" \ @@ -518,35 +539,122 @@ curl -X POST "https://.signalwire.com/api/fabric/resources/swml_scri } } }' +``` -# Use the "id" from the response as the resource_id. -curl -X POST "https://.signalwire.com/api/fabric/phone_number_addresses" \ + + + +In the Dashboard, open **My Resources**, select **+ Add**, then **Relay Application**. Give it +a name, enter `inbound-calling` as the **Topic** to match the `contexts` value in your code, and +select **Create**. + +With the REST API, [create a Relay Application][rest-create-relay-app] with the same topic. + + + +```bash +curl -X POST "https://.signalwire.com/api/fabric/resources/relay_applications" \ -u ":" \ -H "Content-Type: application/json" \ -d '{ - "number": "", - "resource_id": "", - "handler_type": "calling" + "name": "Inbound welcome", + "topic": "inbound-calling" }' ``` + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +from signalwire.rest import RestClient + +client = RestClient( + project="", + token="", + host=".signalwire.com", +) + +resource = client.fabric.relay_applications.create( + name="Inbound welcome", topic="inbound-calling", +) +print(resource["id"]) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +import { RestClient } from "@signalwire/sdk"; + +const client = new RestClient({ + project: "", + token: "", + host: ".signalwire.com", +}); + +const resource = await client.fabric.relayApplications.create({ + name: "Inbound welcome", + topic: "inbound-calling", +}); +console.log(resource.id); +``` + + - + -In the Dashboard, open **My Resources**, select **+ Add**, then **Relay Application**. Give it -a name, enter `inbound-calling` as the **Topic** to match the `contexts` value in your code, and -select **Create**. +The Subscriber is the Resource. Creating its [Subscriber token][subscriber-token] creates the +Subscriber too when the `reference` is new, or you can [create one directly][rest-create-subscriber]. +Find it under **My Resources** and note its ID. The token you give the browser client must be for +this Subscriber. + + + + +### Give the Resource an address -Then assign the Relay Application to your number. +Attach the address you chose so calls to it reach the Resource. SignalWire already created one +alias from the Resource's name, so a phone number or SIP address is an addition, not a +replacement. + + + -To assign with the REST API instead, [update the phone number][rest-update-number] with the -`relay_topic` handler and the topic name, or use the [`set_relay_topic`][py-set-relay-topic] and -[`setRelayTopic`][ts-set-relay-topic] wrappers. +With the REST API, [link the Resource to the number][rest-link-number]. This endpoint is in beta +and requires the number's calling channel to have no Resource assigned yet. + +```bash +curl -X POST "https://.signalwire.com/api/fabric/phone_number_addresses" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "number": "", + "resource_id": "", + "handler_type": "calling" + }' +``` + + + +For SWML over HTTP and Relay, [Update phone number][rest-update-number] creates the Resource and +assigns it together, so you can skip the create step above. The Server SDKs wrap it as +[`set_swml_webhook`][py-set-swml-webhook] and [`setSwmlWebhook`][ts-set-swml-webhook], and as +[`set_relay_topic`][py-set-relay-topic] and [`setRelayTopic`][ts-set-relay-topic]. - + +```bash +curl -X PUT "https://.signalwire.com/api/relay/rest/phone_numbers/" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "call_handler": "relay_script", + "call_relay_script_url": "" + }' +``` + + ```bash curl -X PUT "https://.signalwire.com/api/relay/rest/phone_numbers/" \ -u ":" \ @@ -568,6 +676,9 @@ client = RestClient( host=".signalwire.com", ) +# SWML over HTTP +client.phone_numbers.set_swml_webhook("", "") +# WebSocket (Relay) client.phone_numbers.set_relay_topic("", "inbound-calling") ``` @@ -582,53 +693,116 @@ const client = new RestClient({ host: ".signalwire.com", }); +// SWML over HTTP +await client.phoneNumbers.setSwmlWebhook("", ""); +// WebSocket (Relay) await client.phoneNumbers.setRelayTopic("", { topic: "inbound-calling" }); ``` + + - + -Assign the number to the Subscriber your token was issued for. +Open the Resource from **My Resources**, select its **Addresses & Phone Numbers** tab, select +**+ Add**, then **SIP Address**. Leave **User** as `*` to accept any username, pick a **Domain**, +give the address a **Name**, and select **Create**. A password and an IP allowlist are optional; +leave both off for the first call. The tab then shows the URI SignalWire assigned, in the form +`sip:*@-.dapp.signalwire.com`. - +With the REST API, [create a SIP address][rest-create-sip] that names the Resource as its call +handler. The response includes the `uri`. + +```bash +curl -X POST "https://.signalwire.com/api/fabric/sip_addresses" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "inbound-welcome", + "user": "*", + "calling_handler_resource_id": "" + }' +``` + + + + +Nothing to add. SignalWire created an alias from the Resource's name when you created it: +`/public/` for a script or application, and `/private/` for a Subscriber. Open the +Resource's **Addresses & Phone Numbers** tab to copy it. -To assign with the REST API instead, [link the Subscriber to the number][rest-link-number]. This -endpoint is in beta and requires the number's calling channel to have no Resource assigned yet. +To reach the Resource under another name, or to limit it to audio, video, or messaging, select +**+ Add**, then **Alias**, and fill in the **Alias**, **Display Name**, **Context**, and +**Channels**. With the REST API, [create an alias address][rest-create-alias] with a `name`, the +`resource_id`, and a `context`. ```bash -curl -X POST "https://.signalwire.com/api/fabric/phone_number_addresses" \ +curl -X POST "https://.signalwire.com/api/fabric/alias_addresses" \ -u ":" \ -H "Content-Type: application/json" \ -d '{ - "number": "", - "resource_id": "", - "handler_type": "calling" + "name": "inbound-welcome", + "resource_id": "", + "context": "public" }' ``` -The Subscriber also keeps its `/private/` address, so another app or Resource in your -Space can call the same browser client without a phone number. +A `public` alias is reachable by any client, including one with a guest token. A `private` alias, +which is what a Subscriber gets, is reachable only by authenticated users and by your own +project's dials. -### Call your number +### Place your test call + + + + +Dial your SignalWire number from your phone. In a trial project, call from the number you +verified. + + + + +Dial the URI from a SIP softphone or your PBX, for example `sip:test@-.dapp.signalwire.com`. +With **User** set to `*`, any username reaches the Resource. If you set a password, configure it +in the client first. + + + + +Dial the alias from another SignalWire client. The quickest is the browser sample from the +[outbound calling guide][outbound-calling] with the alias as the destination: + +```javascript +const call = await client.dial("", { audio: true, video: false }); +``` + +A guest token can dial a `public` alias. To reach a `private` alias, such as a Subscriber's, the +caller must be an authenticated Subscriber, or place the call from your project with the +[REST Calling API][rest-calling-dial] using the alias as `to`. + + + -Dial your SignalWire number from your phone. With either SWML approach or with Relay, you hear -"Hello, welcome to SignalWire!" and the call ends. With the browser example, the page shows the incoming call. -Select **Answer**, allow microphone access, and select **Hang up** when you finish. +With either SWML approach or with Relay, you hear "Hello, welcome to SignalWire!" and the call +ends. With the browser handler, the page shows the incoming call. Select **Answer**, allow +microphone access, and select **Hang up** when you finish. If the call rings without an answer or fails, check these first: -- The number's **Inbound Call Settings** show your Resource. An unassigned number takes no action. -- You're calling from a verified number if the project is in trial mode. -- For Relay, the Relay Application's **Topic** matches your client's `contexts` value and the client is running. +- The address lists your Resource as its call handler. The Dashboard's **Addresses** page shows + every address with its handler; an unassigned phone number takes no action. +- For a phone number in a trial project, you're calling from a verified number. +- For Relay, the Relay Application's **Topic** matches your client's `contexts` value and the + client is running. - For SWML over HTTP, SignalWire could reach the URL and got a valid document. Check the call in the Dashboard's **Logs**. -- For the browser, the token is a Subscriber token, not a guest or embed token, and the number is - assigned to that same Subscriber. +- For the browser, the token is a Subscriber token, not a guest or embed token, and the address + belongs to that same Subscriber. @@ -641,7 +815,8 @@ section for the approach you used for your first call. With SWML over HTTP, SignalWire sends your server a POST request whose `call` object carries the `from` and `to` addresses, the `direction`, and the `call_id`, so your code can decide the -document per caller. See the [webhook payload reference][swml-webhook-payload] for every field. +document per caller. `from` is a phone number, a SIP URI, or an alias, depending on how the +caller reached you. See the [webhook payload reference][swml-webhook-payload] for every field. The same fields are available inside the document as [variables][swml-variables], so a hosted document can use them too. This document reads the caller's number back to them: From 414788c7aafcf454e589f015ea364c55e4507660 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 16 Sep 2026 09:28:26 -0400 Subject: [PATCH 05/14] docs(voice): phone number prerequisite applies only to PSTN inbound --- .../platform/pages/calling/voice/inbound-calling.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/inbound-calling.mdx b/fern/products/platform/pages/calling/voice/inbound-calling.mdx index 58a079315f..2ad1278a9d 100644 --- a/fern/products/platform/pages/calling/voice/inbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/inbound-calling.mdx @@ -71,9 +71,9 @@ Have these values ready: - Your Project ID and API token from the Dashboard's [API credentials][api-credentials] page. Enable the token's **Voice** permission, and its **Numbers** permission if you assign the handler with the REST API rather than the Dashboard. -- A way for callers to reach you. A voice-capable [phone number purchased in your Space][phone-numbers] - is the usual choice, but a SIP address or an alias works without one. You'll pick in - [Choose how callers reach you](#choose-how-callers-reach-you). +- A way for callers to reach you. Only calls from the phone network need a voice-capable + [phone number purchased in your Space][phone-numbers]; a SIP address or an alias needs no number. + You'll pick in [Choose how callers reach you](#choose-how-callers-reach-you). - Something to place the test call from: a phone, a SIP softphone, or another SignalWire client, depending on the address you pick. - If answering in the browser, a [Subscriber token][subscriber-token] for the [Subscriber][subscribers] From 427a5d8311f168177b121b0b63780389fb99370a Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 16 Sep 2026 09:29:09 -0400 Subject: [PATCH 06/14] docs(voice): tighten phone number prerequisite --- .../platform/pages/calling/voice/inbound-calling.mdx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/inbound-calling.mdx b/fern/products/platform/pages/calling/voice/inbound-calling.mdx index 2ad1278a9d..8cbb45d490 100644 --- a/fern/products/platform/pages/calling/voice/inbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/inbound-calling.mdx @@ -71,9 +71,8 @@ Have these values ready: - Your Project ID and API token from the Dashboard's [API credentials][api-credentials] page. Enable the token's **Voice** permission, and its **Numbers** permission if you assign the handler with the REST API rather than the Dashboard. -- A way for callers to reach you. Only calls from the phone network need a voice-capable - [phone number purchased in your Space][phone-numbers]; a SIP address or an alias needs no number. - You'll pick in [Choose how callers reach you](#choose-how-callers-reach-you). +- A voice-capable [phone number in your Space][phone-numbers], only if you're taking calls from + the phone network. SIP addresses and aliases need no number. - Something to place the test call from: a phone, a SIP softphone, or another SignalWire client, depending on the address you pick. - If answering in the browser, a [Subscriber token][subscriber-token] for the [Subscriber][subscribers] From 9c3c304af990446673512bc783ad5ef363fdc02b Mon Sep 17 00:00:00 2001 From: Devon-White Date: Wed, 16 Sep 2026 09:36:45 -0400 Subject: [PATCH 07/14] docs(voice): frame inbound approaches around the Server SDK and Browser SDK Name the four approaches by SDK and transport: Server SDK over HTTP (SWML), Server SDK over WebSocket (Relay), Browser SDK over WebSocket, and serverless SWML. Say outright that SWML is what the Server SDK serves and that the Browser SDK is Relay in the browser, so the comparison no longer sets SWML against a WebSocket it also uses. --- .../pages/calling/voice/inbound-calling.mdx | 204 +++++++++--------- 1 file changed, 103 insertions(+), 101 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/inbound-calling.mdx b/fern/products/platform/pages/calling/voice/inbound-calling.mdx index 8cbb45d490..ecd531ff34 100644 --- a/fern/products/platform/pages/calling/voice/inbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/inbound-calling.mdx @@ -115,31 +115,33 @@ Play a short announcement to anyone who dials your number. ### Choose how to handle the call Inbound handling comes down to two questions: where does your logic run, and how does SignalWire -reach it? Four approaches answer them differently. +reach it? The Server SDKs answer both ways. They can serve a SWML document over HTTP, or hold a +WebSocket open with Relay. The Browser SDK is Relay too, running in the user's browser instead +of on your server. Serverless SWML involves no code of yours at all. -| Approach | How SignalWire reaches your logic | Where your logic runs | +| Approach | How SignalWire reaches your logic | Where it runs | |---|---|---| -| [SWML over HTTP](#create-your-call-handler) | Requests a SWML document from your server on every call, then runs it | Your server, built with a Server SDK | -| [Serverless SWML](#create-your-call-handler) | Runs a SWML document you host in your Space. No request reaches you. | SignalWire | -| [WebSocket (Relay)](#create-your-call-handler) | Delivers the call over a persistent WebSocket your server holds open. Your code answers it and controls it command by command. | Your server, with a Server SDK Relay client | -| [Browser SDK](#create-your-call-handler) | Delivers the call over the persistent connection a signed-in user's browser holds open. The user answers it. | The user's browser | +| [Server SDK: HTTP (SWML)](#create-your-call-handler) | Requests a SWML document from your server on every call, then runs it | Your server | +| [Server SDK: WebSocket (Relay)](#create-your-call-handler) | Delivers the call over a persistent WebSocket your server holds open. Your code answers it and controls it command by command. | Your server | +| [Browser SDK: WebSocket](#create-your-call-handler) | Delivers the call over the persistent WebSocket a signed-in user's browser holds open. The user answers it. | The user's browser | +| [Serverless SWML](#create-your-call-handler) | Runs a SWML document hosted in your Space. No request reaches you. | SignalWire | -The two SWML approaches run the same document and differ only in whether your server produces -it. The two WebSocket approaches share a transport but not a purpose: Relay puts your code on the -call, and the Browser SDK puts a person on it. +SWML is the same document whether your Server SDK app serves it or you host it; the SDK builds +it with `SWMLService` or `SwmlBuilder`. Relay is the same protocol whether the Server SDK or the +Browser SDK holds the connection; the difference is whether your code or a person is on the call. -| Function | SWML over HTTP | Serverless SWML | WebSocket (Relay) | Browser SDK | +| Function | Server SDK: HTTP | Server SDK: WebSocket | Browser SDK | Serverless SWML | |---|---|---|---|---| -| Nothing to run or expose on your side | | | | | -| Decide from your code what happens before the call is answered, such as looking the caller up | | | | | -| React to events in your own code while the call is live, with no public URL | | | | | -| A person answers and speaks on the call | | | | | -| Start an AI agent, record, stream, or bridge the call | | | | | -| Command the call from another process by its call ID | | | | | +| Decide from your code what happens before the call is answered, such as looking the caller up | | | | | +| React to events in your own code while the call is live, with no public URL | | | | | +| A person answers and speaks on the call | | | | | +| Start an AI agent, record, stream, or bridge the call | | | | | +| Command the call from another process by its call ID | | | | | +| Nothing to run or expose on your side | | | | | A Browser SDK client receives calls only when it authenticates with a Subscriber token issued for -a specific Subscriber, and that Subscriber is the number's call handler. Guest tokens and embed +a specific Subscriber, and that Subscriber is the address's call handler. Guest tokens and embed tokens, which power click-to-call widgets, are outbound-only and never ring. @@ -181,7 +183,7 @@ Replace these values in the code sample you choose: Write the code SignalWire runs, or connects to, when the number rings. - + A SWML document tells SignalWire what to do with the call. When the number rings, SignalWire requests the document from your server and runs it. Build and serve it with a Server SDK, @@ -261,47 +263,7 @@ sections: - - -Serverless SWML runs the same document as SWML over HTTP, hosted in your Space instead of on -your server. There's nothing to run: in the next step you paste the document into a hosted SWML -Script, and SignalWire executes it on every call. Use it when the call logic is fixed, or to try -a document before you build the server. The [SWML quickstart][swml-quickstart] covers hosted -scripts in depth, and the AI Agent and Call Flow Resources are serverless handlers you build in -the Dashboard rather than as SWML. - -This document plays an announcement, then hangs up when it ends: - - - -```yaml -version: 1.0.0 -sections: - main: - - play: - url: 'say:Hello, welcome to SignalWire!' -``` - - -```json -{ - "version": "1.0.0", - "sections": { - "main": [ - { "play": { "url": "say:Hello, welcome to SignalWire!" } } - ] - } -} -``` - - - -A hosted document still sees the call: variables such as `${call.from}` resolve to the caller's -details, so it can react to who's calling without a server. See -[Read the call details via SWML](#read-the-call-details-via-swml). - - - + A [`RelayClient`][py-relay-client] subscribes to a topic and receives every call routed to it. The handler answers, plays the announcement, and hangs up. Keep the process running; it holds the connection open. @@ -363,7 +325,7 @@ The `contexts` value is the topic. You'll give the same name to the Relay Applic next step, and only a client subscribed to it receives the call. - + A Browser SDK client authenticates as a Subscriber, registers, and receives the call in the page. Show the caller, then let the user answer or decline. The token must be a Subscriber token @@ -438,6 +400,46 @@ hangupButton.onclick = () => { Constructing the client authenticates the user, and [`register()`][browser-register] brings them online so calls can reach them. Leave the page open while you test. + + + +Serverless SWML runs the same document the Server SDK serves over HTTP, hosted in your Space +instead of on your server. There's nothing to run: in the next step you paste the document into a hosted SWML +Script, and SignalWire executes it on every call. Use it when the call logic is fixed, or to try +a document before you build the server. The [SWML quickstart][swml-quickstart] covers hosted +scripts in depth, and the AI Agent and Call Flow Resources are serverless handlers you build in +the Dashboard rather than as SWML. + +This document plays an announcement, then hangs up when it ends: + + + +```yaml +version: 1.0.0 +sections: + main: + - play: + url: 'say:Hello, welcome to SignalWire!' +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "play": { "url": "say:Hello, welcome to SignalWire!" } } + ] + } +} +``` + + + +A hosted document still sees the call: variables such as `${call.from}` resolve to the caller's +details, so it can react to who's calling without a server. See +[Read the call details via SWML](#read-the-call-details-via-swml). + @@ -447,7 +449,7 @@ Every handler is a Resource in your Space. Create it, then note its ID from the or from the REST response; you'll attach the address to it next. - + In the Dashboard, open **My Resources**, select **+ Add**, then **Script**, then **SWML Script**. Give it a name and leave **Used For** set to **Calling**. Under **Handle Calls Using**, choose @@ -515,33 +517,7 @@ console.log(resource.id); - - -In the Dashboard, open **My Resources**, select **+ Add**, then **Script**, then **SWML Script**. -Give it a name and leave **Used For** set to **Calling**. Under **Handle Calls Using**, choose -**Hosted Script** and paste the document into **Primary Script**. Select **Create**. - -With the REST API, [create the SWML Script][rest-create-script] with the document as `contents`. - -```bash -curl -X POST "https://.signalwire.com/api/fabric/resources/swml_scripts" \ - -u ":" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Inbound welcome", - "contents": { - "version": "1.0.0", - "sections": { - "main": [ - { "play": { "url": "say:Hello, welcome to SignalWire!" } } - ] - } - } - }' -``` - - - + In the Dashboard, open **My Resources**, select **+ Add**, then **Relay Application**. Give it a name, enter `inbound-calling` as the **Topic** to match the `contexts` value in your code, and @@ -599,13 +575,39 @@ console.log(resource.id); - + The Subscriber is the Resource. Creating its [Subscriber token][subscriber-token] creates the Subscriber too when the `reference` is new, or you can [create one directly][rest-create-subscriber]. Find it under **My Resources** and note its ID. The token you give the browser client must be for this Subscriber. + + + +In the Dashboard, open **My Resources**, select **+ Add**, then **Script**, then **SWML Script**. +Give it a name and leave **Used For** set to **Calling**. Under **Handle Calls Using**, choose +**Hosted Script** and paste the document into **Primary Script**. Select **Create**. + +With the REST API, [create the SWML Script][rest-create-script] with the document as `contents`. + +```bash +curl -X POST "https://.signalwire.com/api/fabric/resources/swml_scripts" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Inbound welcome", + "contents": { + "version": "1.0.0", + "sections": { + "main": [ + { "play": { "url": "say:Hello, welcome to SignalWire!" } } + ] + } + } + }' +``` + @@ -636,7 +638,7 @@ curl -X POST "https://.signalwire.com/api/fabric/phone_number_addres -For SWML over HTTP and Relay, [Update phone number][rest-update-number] creates the Resource and +For the Server SDK paths, [Update phone number][rest-update-number] creates the Resource and assigns it together, so you can skip the create step above. The Server SDKs wrap it as [`set_swml_webhook`][py-set-swml-webhook] and [`setSwmlWebhook`][ts-set-swml-webhook], and as [`set_relay_topic`][py-set-relay-topic] and [`setRelayTopic`][ts-set-relay-topic]. @@ -675,9 +677,9 @@ client = RestClient( host=".signalwire.com", ) -# SWML over HTTP +# Server SDK: HTTP (SWML) client.phone_numbers.set_swml_webhook("", "") -# WebSocket (Relay) +# Server SDK: WebSocket (Relay) client.phone_numbers.set_relay_topic("", "inbound-calling") ``` @@ -692,9 +694,9 @@ const client = new RestClient({ host: ".signalwire.com", }); -// SWML over HTTP +// Server SDK: HTTP (SWML) await client.phoneNumbers.setSwmlWebhook("", ""); -// WebSocket (Relay) +// Server SDK: WebSocket (Relay) await client.phoneNumbers.setRelayTopic("", { topic: "inbound-calling" }); ``` @@ -798,7 +800,7 @@ If the call rings without an answer or fails, check these first: - For a phone number in a trial project, you're calling from a verified number. - For Relay, the Relay Application's **Topic** matches your client's `contexts` value and the client is running. -- For SWML over HTTP, SignalWire could reach the URL and got a valid document. Check the call in +- For the Server SDK over HTTP, SignalWire could reach the URL and got a valid document. Check the call in the Dashboard's **Logs**. - For the browser, the token is a Subscriber token, not a guest or embed token, and the address belongs to that same Subscriber. @@ -812,9 +814,9 @@ section for the approach you used for your first call. ### Read the call details via SWML -With SWML over HTTP, SignalWire sends your server a POST request whose `call` object carries the -`from` and `to` addresses, the `direction`, and the `call_id`, so your code can decide the -document per caller. `from` is a phone number, a SIP URI, or an alias, depending on how the +When your Server SDK app serves SWML over HTTP, SignalWire sends it a POST request whose `call` +object carries the `from` and `to` addresses, the `direction`, and the `call_id`, so your code +can decide the document per caller. `from` is a phone number, a SIP URI, or an alias, depending on how the caller reached you. See the [webhook payload reference][swml-webhook-payload] for every field. The same fields are available inside the document as [variables][swml-variables], so a hosted @@ -1000,9 +1002,9 @@ After the user answers, `status$` moves through `connecting` and `connected`, th ## Examples -Each example shows the SWML document, which works both served from your server and hosted in -your Space, and the Relay handler for your Server SDK client. The last example answers in the -browser. +Each example shows the SWML document, which your Server SDK app serves over HTTP or you host +serverless, and the Relay handler for the Server SDK over WebSocket. The last example answers in +the browser. ### Run an AI agent From 6701dbbeafe1944d2e4a6cdad65f9805940484e4 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 17 Sep 2026 13:50:37 -0400 Subject: [PATCH 08/14] docs(voice): address review on inbound calling guide - Default alias context for AI Agents, scripts, and applications is private, not public; note which types get a public alias instead - Aliases can't be re-pointed in the Dashboard or REST: delete and re-create - Resource picker offers Video Room, not Conference Room, with its sub-choices - Link the admin List Resource Addresses endpoint, not the client one - Lowercase external context; 'such as' on the call handler list; SIP Credentials card; client.directory$ observable --- .../pages/calling/voice/inbound-calling.mdx | 30 ++++++++-------- .../pages/platform/call-fabric/addresses.mdx | 35 +++++++++++-------- .../pages/platform/call-fabric/resources.mdx | 10 +++--- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/inbound-calling.mdx b/fern/products/platform/pages/calling/voice/inbound-calling.mdx index ecd531ff34..2481ee7d5d 100644 --- a/fern/products/platform/pages/calling/voice/inbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/inbound-calling.mdx @@ -158,7 +158,7 @@ carry any of these, and one Resource can carry all three. |---|---|---| | Phone number | Anyone on the phone network | A voice-capable number in your Space, and a phone to test from | | SIP address | SIP softphones, PBXs, and carriers outside SignalWire, dialing the URI SignalWire assigns | A SIP client to test from | -| Alias | Other SignalWire clients and Resources: a Browser SDK `dial()`, a SWML `connect`, or a REST dial to `/public/` | A Browser SDK client or another Resource to test from | +| Alias | Other SignalWire clients and Resources: a Browser SDK `dial()`, a SWML `connect`, or a REST dial to `//` | A Browser SDK client or another Resource to test from | The steps below show the address you pick in its own tab, alongside the handler tabs. Stay on the same tabs through the guide. @@ -730,14 +730,17 @@ curl -X POST "https://.signalwire.com/api/fabric/sip_addresses" \ -Nothing to add. SignalWire created an alias from the Resource's name when you created it: -`/public/` for a script or application, and `/private/` for a Subscriber. Open the -Resource's **Addresses & Phone Numbers** tab to copy it. +SignalWire created an alias from the Resource's name when you created it, `/private/` +for a script, an AI Agent, an application, or a Subscriber alike. Open the Resource's +**Addresses & Phone Numbers** tab to copy it. A `private` alias is reachable by authenticated +Subscribers and by your own project's dials, so it's enough to test with a Subscriber token or +a REST dial. -To reach the Resource under another name, or to limit it to audio, video, or messaging, select -**+ Add**, then **Alias**, and fill in the **Alias**, **Display Name**, **Context**, and -**Channels**. With the REST API, [create an alias address][rest-create-alias] with a `name`, the -`resource_id`, and a `context`. +To let any client reach the Resource, including one with a guest token, or to reach it under +another name or limit it to audio, video, or messaging, add a `public` alias: select **+ Add**, +then **Alias**, and fill in the **Alias**, **Display Name**, **Context**, and **Channels**. With +the REST API, [create an alias address][rest-create-alias] with a `name`, the `resource_id`, and +a `context`. ```bash curl -X POST "https://.signalwire.com/api/fabric/alias_addresses" \ @@ -750,9 +753,8 @@ curl -X POST "https://.signalwire.com/api/fabric/alias_addresses" \ }' ``` -A `public` alias is reachable by any client, including one with a guest token. A `private` alias, -which is what a Subscriber gets, is reachable only by authenticated users and by your own -project's dials. +A `public` alias is reachable by any client, including one with a guest token. A `private` alias +is reachable only by authenticated users and by your own project's dials. @@ -782,9 +784,9 @@ Dial the alias from another SignalWire client. The quickest is the browser sampl const call = await client.dial("", { audio: true, video: false }); ``` -A guest token can dial a `public` alias. To reach a `private` alias, such as a Subscriber's, the -caller must be an authenticated Subscriber, or place the call from your project with the -[REST Calling API][rest-calling-dial] using the alias as `to`. +A guest token can dial a `public` alias. To reach a `private` alias, such as the one SignalWire +created from the Resource's name, the caller must be an authenticated Subscriber, or place the +call from your project with the [REST Calling API][rest-calling-dial] using the alias as `to`.
diff --git a/fern/products/platform/pages/platform/call-fabric/addresses.mdx b/fern/products/platform/pages/platform/call-fabric/addresses.mdx index a59864afd4..88537a2c58 100644 --- a/fern/products/platform/pages/platform/call-fabric/addresses.mdx +++ b/fern/products/platform/pages/platform/call-fabric/addresses.mdx @@ -46,14 +46,14 @@ of [`connect`][swml-connect] in SWML, or the argument to `dial()` in the Browser ### Phone numbers -A [phone number you've bought or ported][phone-numbers] is an address in the `External` context. +A [phone number you've bought or ported][phone-numbers] is an address in the `external` context. It has two handlers, assigned independently: a call handler for inbound calls and a message handler for inbound messages. The Dashboard shows them as **Inbound Call Settings** and **Inbound Message Settings** on the number's **Edit Settings** page. A number with no call handler rings nobody; nothing happens on an inbound call until you assign a Resource. -Any Resource that can take a call can be a number's call handler: a SWML Script, an AI Agent, a -Call Flow, a Relay Application, a Subscriber, or a SIP Credential. Only messaging-capable +Any Resource that can take a call can be a number's call handler, such as a SWML Script, an AI +Agent, a Call Flow, a Relay Application, a Subscriber, or a SIP Credential. Only messaging-capable Resources can be its message handler. ### SIP addresses @@ -81,8 +81,12 @@ at it; the [bring your own carrier][byoc] guide covers the full flow. An alias is a name in the form `//`. SignalWire creates one from the Resource's name when you create the Resource, so a Subscriber named `john.doe` is reachable at -`/private/john-doe` and an AI Agent named `Support Agent` at `/public/support-agent`. Names are -lowercase letters, numbers, underscores, and dashes. +`/private/john-doe` and an AI Agent named `Support Agent` at `/private/support-agent`. Names are +lowercase letters, numbers, underscores, and dashes. Most types, including AI Agents, SWML +Scripts, Call Flows, Relay Applications, and Subscribers, get their first alias in the `private` +context; Video Rooms, SIP Credentials, SIP Gateways, cXML Scripts, and FreeSWITCH Connectors get a +`public` one. Add a `public` alias yourself when unauthenticated callers need to reach a Resource +that started out `private`. Add more aliases to expose the same Resource under other names, or to shape how it's reached: @@ -93,10 +97,10 @@ Add more aliases to expose the same Resource under other names, or to shape how directory: as an app, a room, a call, or a subscriber. A script that dispatches callers to your staff can present itself as a subscriber so callers feel they're dialing a person. -An alias can be pointed at a different Resource later, which is the easiest way to swap a -handler without changing anything that dials it. In the Dashboard, open the alias and change -its assigned Resource. The REST API doesn't change an alias's Resource in place; delete the -alias and [create a new one][rest-create-alias] with the new `resource_id`. +An alias can't be moved to a different Resource in place, in the Dashboard or with the REST API. +To swap the handler behind a name, [delete the alias][rest-delete-alias] and +[create a new one][rest-create-alias] with the same `name` and `context` and the new +`resource_id`. Nothing that dials the alias has to change. The Dashboard can also attach a WhatsApp business number to a Resource so it handles inbound WhatsApp calls and messages. It appears alongside the other types in the Add an Address menu. @@ -110,7 +114,7 @@ An alias lives in a context, and the context decides who can reach it: - **`private`** addresses are reachable only by authenticated users, which makes them the home of [Subscribers][subscribers] and anything only your own users should dial. -SIP addresses use the same contexts as their domain, and phone numbers sit in the `External` +SIP addresses use the same contexts as their domain, and phone numbers sit in the `external` context. Your Space may show additional contexts in the Dashboard. @@ -153,8 +157,9 @@ channel. A support agent might carry: | `/public/support` | The click-to-call widget on your website | | `/private/support` | Staff dialing from the Browser SDK or a SIP phone | -When you ship a new version of the agent, point the phone number and aliases at the new Resource. -Every caller moves over, and nothing that dials `/public/support` changes. +When you ship a new version of the agent, assign the phone number to the new Resource and +re-create the aliases on it. Every caller moves over, and nothing that dials `/public/support` +changes. ## Manage addresses @@ -199,9 +204,9 @@ a number with no handler or to see which Resource an alias points at. ### In the Browser SDK A client authenticated with a [Subscriber token][subscriber-token] sees the addresses its user is -allowed to reach through `client.directory`. Each entry exposes a ready-to-dial URI per channel, -so the client dials what the directory returns instead of building `/context/name` strings by -hand. See the [address book guide][browser-address-book] and [`defaultChannel`][browser-default-channel]. +allowed to reach through the `client.directory$` observable. Each entry exposes a ready-to-dial +URI per channel, so the client dials what the directory returns instead of building +`/context/name` strings by hand. See the [address book guide][browser-address-book] and [`defaultChannel`][browser-default-channel]. ## Next steps diff --git a/fern/products/platform/pages/platform/call-fabric/resources.mdx b/fern/products/platform/pages/platform/call-fabric/resources.mdx index 8bc9e26e0a..c1c1fd99c6 100644 --- a/fern/products/platform/pages/platform/call-fabric/resources.mdx +++ b/fern/products/platform/pages/platform/call-fabric/resources.mdx @@ -35,7 +35,7 @@ Call Fabric supports several Resource types. Some of the most common are: Twilio-compatible call logic for migrating existing applications. - + Register SIP devices and softphones to your Space. @@ -58,8 +58,8 @@ Resource can have several: Resource's name when you create it. The context of an alias decides who can reach it: `public` addresses are open to anyone, `private` -ones only to authenticated users. Addresses are mutable, so you can point a phone number or an -alias at a different Resource later without changing the code that dials it. +ones only to authenticated users. A phone number can be assigned to a different Resource later, +and an alias can be deleted and re-created on another, without changing the code that dials it. The [Addresses guide](/docs/platform/addresses) explains each type in depth, how SignalWire resolves an address to a handler, and how to add, assign, and move addresses in the Dashboard @@ -72,7 +72,7 @@ and the REST API. Use the Resources endpoints to work with any Resource programmatically, regardless of type. The generic Resources endpoint lets you [list](/docs/apis/rest/resources/list-resources) all Resources, [retrieve](/docs/apis/rest/resources/get-resource) or [delete](/docs/apis/rest/resources/delete-resource) a Resource by -ID, and [list a Resource's addresses](/docs/apis/rest/addresses/list-resource-addresses-client). +ID, and [list a Resource's addresses](/docs/apis/rest/addresses/list-resource-addresses). To **create** or **update** a Resource, use the endpoint for its specific type. For example, if you wanted to create a new SWML Script you would utilize the [Create SWML Script](/docs/apis/rest/swml-scripts/create-swml-script) endpoint. @@ -121,7 +121,7 @@ Click the **Add** button to select from the grid of available Resource types. -The Resource picker offers **Script**, **AI Agent**, **Conference Room**, **Relay Application**, **Subscriber**, **FreeSWITCH Connector**, **Call Flow**, and **SIP**. +The Resource picker offers **Script**, **AI Agent**, **Video Room**, **Relay Application**, **Subscriber**, **FreeSWITCH Connector**, **Call Flow**, and **SIP**. **Script** asks for a SWML or cXML Script, **AI Agent** for a Custom or Google DialogFlow Agent, and **SIP** for a SIP Credential or SIP Gateway. From f5b01bba9f4cea29b618d60b4ba4e883654a12e0 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Thu, 17 Sep 2026 15:07:11 -0400 Subject: [PATCH 09/14] docs(platform): fill gaps from second review of Resources and Addresses - Define handler at first use; state address/Resource relationship rules - Per-type table for the first alias's context; External URL scripts are public - Alias property list matching the REST object; display_type is derived - WhatsApp number as its own address type; deletion behavior - Contexts: public/private built in; only Subscriber dials may omit the context - Resources page hands off to Addresses instead of duplicating it; drop the self-referencing admonition; snippet says SIP Credentials --- .../pages/calling/voice/inbound-calling.mdx | 29 ++++----- .../pages/platform/call-fabric/addresses.mdx | 60 ++++++++++++++----- .../pages/platform/call-fabric/resources.mdx | 33 +++------- .../common/dashboard/_resource-admonition.mdx | 2 +- 4 files changed, 70 insertions(+), 54 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/inbound-calling.mdx b/fern/products/platform/pages/calling/voice/inbound-calling.mdx index 2481ee7d5d..5a2ddee774 100644 --- a/fern/products/platform/pages/calling/voice/inbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/inbound-calling.mdx @@ -730,17 +730,18 @@ curl -X POST "https://.signalwire.com/api/fabric/sip_addresses" \ -SignalWire created an alias from the Resource's name when you created it, `/private/` -for a script, an AI Agent, an application, or a Subscriber alike. Open the Resource's -**Addresses & Phone Numbers** tab to copy it. A `private` alias is reachable by authenticated -Subscribers and by your own project's dials, so it's enough to test with a Subscriber token or -a REST dial. - -To let any client reach the Resource, including one with a guest token, or to reach it under -another name or limit it to audio, video, or messaging, add a `public` alias: select **+ Add**, -then **Alias**, and fill in the **Alias**, **Display Name**, **Context**, and **Channels**. With -the REST API, [create an alias address][rest-create-alias] with a `name`, the `resource_id`, and -a `context`. +SignalWire created an alias from the Resource's name when you created it. Open the Resource's +**Addresses & Phone Numbers** tab to copy it. Its context depends on the Resource type: a SWML +Script served from an **External URL** starts in `public`, while a hosted SWML Script, an AI +Agent, a Relay Application, or a Subscriber starts in `private`. A `private` alias is reachable +by authenticated Subscribers and by your own project's dials, so it's enough to test with a +Subscriber token or a REST dial. + +To let any client reach a `private` Resource, including one with a guest token, or to reach it +under another name or limit it to audio, video, or messaging, add a `public` alias: select +**+ Add**, then **Alias**, and fill in the **Alias**, **Display Name**, **Context**, and +**Channels**. With the REST API, [create an alias address][rest-create-alias] with a `name`, the +`resource_id`, and a `context`. ```bash curl -X POST "https://.signalwire.com/api/fabric/alias_addresses" \ @@ -784,9 +785,9 @@ Dial the alias from another SignalWire client. The quickest is the browser sampl const call = await client.dial("", { audio: true, video: false }); ``` -A guest token can dial a `public` alias. To reach a `private` alias, such as the one SignalWire -created from the Resource's name, the caller must be an authenticated Subscriber, or place the -call from your project with the [REST Calling API][rest-calling-dial] using the alias as `to`. +A guest token can dial a `public` alias. To reach a `private` alias, the caller must be an +authenticated Subscriber, or place the call from your project with the +[REST Calling API][rest-calling-dial] using the full alias as `to`. diff --git a/fern/products/platform/pages/platform/call-fabric/addresses.mdx b/fern/products/platform/pages/platform/call-fabric/addresses.mdx index 88537a2c58..980a840232 100644 --- a/fern/products/platform/pages/platform/call-fabric/addresses.mdx +++ b/fern/products/platform/pages/platform/call-fabric/addresses.mdx @@ -27,10 +27,12 @@ slug: addresses [rest-list-addresses]: /docs/apis/rest/addresses/list-resource-addresses [rest-list-addresses-client]: /docs/apis/rest/addresses/list-resource-addresses-client [subscriber-token]: /docs/apis/rest/subscribers/tokens/create-subscriber-token +[whatsapp-onboarding]: /docs/platform/messaging/whatsapp/onboarding A [Resource][resources] is what handles a call or message. An address is how anyone reaches it. Callers never dial a Resource directly: they dial a phone number, a SIP URI, or a name such as -`/public/support`, and SignalWire hands the call to the Resource assigned to that address. +`/public/support`, and SignalWire hands the call to the Resource assigned to that address. That +assigned Resource is the address's **handler**. Every Resource gets one address when you create it, and most end up with several. The same addresses are what your own code uses to name things: the `to` of a REST dial, the destination @@ -43,6 +45,12 @@ of [`connect`][swml-connect] in SWML, or the argument to `dial()` in the Browser | [Phone number](#phone-numbers) | `+12025550123` | Anyone on the phone network | | [SIP address](#sip-addresses) | `sip:*@acme-public.dapp.signalwire.com` | SIP devices, PBXs, and carriers outside SignalWire | | [Alias](#aliases) | `/public/support`, `/private/john-doe` | Your scripts, Browser SDK clients, REST dials, and other Resources | +| [WhatsApp number](#whatsapp-numbers) | A WhatsApp business number connected to your Space | WhatsApp users | + +A few rules hold for every type. An address points at exactly one Resource, and a Resource can +carry any number of addresses. Alias names are unique within a context, so `/public/support` and +`/private/support` can point at different Resources. A phone number is two addresses, one for +calling and one for messaging, each with its own handler. ### Phone numbers @@ -82,44 +90,62 @@ at it; the [bring your own carrier][byoc] guide covers the full flow. An alias is a name in the form `//`. SignalWire creates one from the Resource's name when you create the Resource, so a Subscriber named `john.doe` is reachable at `/private/john-doe` and an AI Agent named `Support Agent` at `/private/support-agent`. Names are -lowercase letters, numbers, underscores, and dashes. Most types, including AI Agents, SWML -Scripts, Call Flows, Relay Applications, and Subscribers, get their first alias in the `private` -context; Video Rooms, SIP Credentials, SIP Gateways, cXML Scripts, and FreeSWITCH Connectors get a -`public` one. Add a `public` alias yourself when unauthenticated callers need to reach a Resource -that started out `private`. +lowercase letters, numbers, underscores, and dashes. + +The context of that first alias depends on the Resource type: + +| First alias context | Resource types | +|---|---| +| `private` | AI Agents, hosted SWML Scripts, Call Flows, Relay Applications, Subscribers | +| `public` | SWML Scripts served from an **External URL**, cXML Scripts, Video Rooms, SIP Credentials, SIP Gateways, FreeSWITCH Connectors | -Add more aliases to expose the same Resource under other names, or to shape how it's reached: +A new AI Agent is therefore unreachable from a public click-to-call widget until you add a +`public` alias for it. +Add more aliases to expose the same Resource under other names, or to shape how it's reached. +Each alias has these properties, which the REST API returns on the alias object: + +- **Name** is the URL-safe part of the address; **Display Name** is the label client applications + show and defaults to the name. +- **Context** is `public` or `private`. See [Contexts](#contexts). - **Channels** limit an alias to `audio`, `video`, `messaging`, or any combination. A messaging-only public alias gives a chat entry point to an agent that also takes voice calls through a private one. +- **Codecs** restrict the audio and video codecs offered on calls to the alias. Empty means no + restriction. - **Display As** controls how the alias appears to client applications that browse the directory: as an app, a room, a call, or a subscriber. A script that dispatches callers to - your staff can present itself as a subscriber so callers feel they're dialing a person. + your staff can present itself as a subscriber so callers feel they're dialing a person. The + REST API derives it from the Resource type and returns it as `display_type`. An alias can't be moved to a different Resource in place, in the Dashboard or with the REST API. To swap the handler behind a name, [delete the alias][rest-delete-alias] and [create a new one][rest-create-alias] with the same `name` and `context` and the new `resource_id`. Nothing that dials the alias has to change. -The Dashboard can also attach a WhatsApp business number to a Resource so it handles inbound -WhatsApp calls and messages. It appears alongside the other types in the Add an Address menu. +### WhatsApp numbers + +The Dashboard can attach a WhatsApp business number to a Resource so it handles inbound WhatsApp +calls and messages. It appears alongside the other types in the Add an Address menu. Connecting +the number itself is covered in [WhatsApp onboarding][whatsapp-onboarding]. ## Contexts -An alias lives in a context, and the context decides who can reach it: +An alias lives in a context, and the context's access type decides who can reach it. `public` and +`private` are built in and can't be changed: - **`public`** addresses are reachable by anyone, including unauthenticated callers. Use them for entry points such as a support agent behind a click-to-call button. - **`private`** addresses are reachable only by authenticated users, which makes them the home of [Subscribers][subscribers] and anything only your own users should dial. -SIP addresses use the same contexts as their domain, and phone numbers sit in the `external` -context. Your Space may show additional contexts in the Dashboard. +A SIP address's domain is a context too, and phone numbers sit in the `external` context. Your +Space may show additional contexts in the Dashboard. -From inside a context, you can omit it. A Subscriber dialing from `private` reaches `/private/bob` -as `/bob`. +Only a call placed by an authenticated Subscriber can omit the context: a Subscriber in `private` +reaches `/private/bob` as `/bob`. REST dials and SWML [`connect`][swml-connect] need the full +`//`. ## How SignalWire resolves an address @@ -189,6 +215,10 @@ handler from the number's settings. project with its context, type, call handler, and message handler. It's the quickest way to find a number with no handler or to see which Resource an alias points at. +**Removing.** Deleting an address removes only that way in: the Resource stays, and calls already +in progress continue. Removing a phone number's handler leaves the number in your Space, +unassigned, until you assign another Resource. + ### With the REST API | Task | Endpoint | diff --git a/fern/products/platform/pages/platform/call-fabric/resources.mdx b/fern/products/platform/pages/platform/call-fabric/resources.mdx index c1c1fd99c6..fa076889f0 100644 --- a/fern/products/platform/pages/platform/call-fabric/resources.mdx +++ b/fern/products/platform/pages/platform/call-fabric/resources.mdx @@ -47,23 +47,11 @@ Each of these Resource types use the same addressing and management patterns des ## Resource addresses -A Resource is what handles a call or message. An **address** is how anyone reaches it, and a -Resource can have several: - -- A **phone number** you've bought or ported, for callers on the phone network. -- A **SIP address**, a SIP URI SignalWire assigns to the Resource, for PBXs and carriers outside - SignalWire. -- An **alias** in the form `/context/name`, such as `/public/support` or `/private/john-doe`, - for your own scripts, Browser SDK clients, and REST dials. SignalWire creates one from the - Resource's name when you create it. - -The context of an alias decides who can reach it: `public` addresses are open to anyone, `private` -ones only to authenticated users. A phone number can be assigned to a different Resource later, -and an alias can be deleted and re-created on another, without changing the code that dials it. - -The [Addresses guide](/docs/platform/addresses) explains each type in depth, how SignalWire -resolves an address to a handler, and how to add, assign, and move addresses in the Dashboard -and the REST API. +Callers never dial a Resource directly. They dial one of its **addresses**: a phone number, a SIP +address, or an alias such as `/private/john-doe`, and SignalWire hands the call to the Resource. +SignalWire creates one alias from the Resource's name when you create it, and you can add more +addresses of any type. The [Addresses guide](/docs/platform/addresses) covers each type, +contexts, and how to add and assign addresses in the Dashboard and the REST API. ## Manage Resources @@ -91,7 +79,7 @@ You can create, manage, edit, delete, and test your Space's Resources all from y From there, you can select the type of Resource you would like to create by clicking on the + Add button. - + Choose the type, fill in its form, and select **Create**. The fields depend on the type. @@ -132,12 +120,9 @@ After selecting a Resource, enter the details for the Resource on a new page, an #### Give it an address A new Resource is reachable at the alias SignalWire creates from its name. To take calls from the -phone network, assign it as a phone number's call handler. - - - -To add a SIP address or another alias instead, open the Resource's **Addresses & Phone Numbers** -tab and select **+ Add**. The [Addresses guide](/docs/platform/addresses) covers every option. +phone network, assign it as a phone number's call handler, or add a SIP address or another alias +from the Resource's **Addresses & Phone Numbers** tab. The [Addresses guide](/docs/platform/addresses#manage-addresses) +walks through each. If you don't see the **My Resources** tab, your SignalWire Space is on the **Legacy Dashboard**, diff --git a/fern/snippets/common/dashboard/_resource-admonition.mdx b/fern/snippets/common/dashboard/_resource-admonition.mdx index 4c1c066863..c19ac68563 100644 --- a/fern/snippets/common/dashboard/_resource-admonition.mdx +++ b/fern/snippets/common/dashboard/_resource-admonition.mdx @@ -1,5 +1,5 @@ {/* Shared component: Resource admonition */} -[Resources](/docs/platform/resources) are the building blocks of SignalWire applications. They include AI Agents, SWML Scripts, cXML Scripts, SIP Endpoints, and more. +[Resources](/docs/platform/resources) are the building blocks of SignalWire applications. They include AI Agents, SWML Scripts, cXML Scripts, SIP Credentials, and more. From b798bdb01bfd04fbad02e14e6ee611d0437fe4b4 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Fri, 18 Sep 2026 10:22:49 -0400 Subject: [PATCH 10/14] docs(voice): fact-check and tighten the inbound calling guide - Browser SDK: register() resolves on guest and embed tokens (they never ring); answer() with no options mirrors the caller's offer; transfer() needs a destination; pin @signalwire/js@4.0.0-rc.2 rxjs@7.8.2 in both voice guides (@latest resolves to 3.x) - Drop unsourced claims: a token with a new reference creating the Subscriber, ringback during connect.confirm, record_call posting at call end - Phone menu: Relay sample dials phones only, so prose says E.164; type the TypeScript lookup table so it passes tsc --strict - Create the Resource: Dashboard plus one cURL per tab; the TypeScript swmlWebhooks.create is deprecated in favor of setSwmlWebhook, which the shortcut Accordion already shows - Move the approach choice out of Steps into "Pick the right product", fold the Resource/address model into it, add per-surface bullets - Remove duplicated alias, guest-token, and browser status prose; add missing placeholders and sample headers; highlight the lines the Relay tracking sample adds; drop the Next steps section - Bare slug; alias form field is Name, matching the Addresses guide - Outbound: attach media before the capture-failure subsection --- fern/llms.txt | 2 +- .../pages/calling/voice/inbound-calling.mdx | 292 ++++++------------ .../pages/calling/voice/outbound-calling.mdx | 48 +-- 3 files changed, 120 insertions(+), 222 deletions(-) diff --git a/fern/llms.txt b/fern/llms.txt index a5e235efba..f22caceb72 100644 --- a/fern/llms.txt +++ b/fern/llms.txt @@ -97,7 +97,7 @@ 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/platform/voice/outbound-calling), [answer calls to a phone number](/docs/platform/voice/inbound-calling), 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), [answer inbound calls](/docs/platform/voice/inbound-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 diff --git a/fern/products/platform/pages/calling/voice/inbound-calling.mdx b/fern/products/platform/pages/calling/voice/inbound-calling.mdx index 5a2ddee774..8af9725f9f 100644 --- a/fern/products/platform/pages/calling/voice/inbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/inbound-calling.mdx @@ -1,6 +1,6 @@ --- title: Inbound calling -slug: /voice/inbound-calling +slug: voice/inbound-calling description: Answer your first inbound call to a phone number, SIP address, or alias, find out who is calling, and add an AI agent, a phone menu, or browser answering. max-toc-depth: 3 --- @@ -63,6 +63,59 @@ or an alias another client dials, and choose what happens when it rings. Start b announcement to yourself, then find out who is calling, run an AI agent, build a phone menu, or let a signed-in user answer from your web app. +## Pick the right product for inbound calls + +Everything that can handle a call in your Space is a [Resource][resources]: a SWML Script, a +Relay Application, a Subscriber, an AI Agent, a Call Flow. Callers don't dial the Resource itself. +They dial one of its [addresses][addresses], a phone number, a [SIP address][sip-credentials], or +an alias such as `/public/support`, and SignalWire hands the call to the Resource assigned to that +address. The same handler runs whichever of its addresses the caller dialed. A phone number's call +handler is assigned separately from its message handler, and the number takes no action until you +assign one. The [Addresses guide][addresses] explains each address type, contexts, and how to add +or move addresses. + +The Resource type decides how you control the call, and this page covers the three you drive from +code. Inbound handling comes down to two questions: where does your logic run, and how does +SignalWire reach it? The Server SDKs answer both ways. They can serve a SWML document over HTTP, +or hold a WebSocket open with Relay. The Browser SDK is Relay too, running in the user's browser +instead of on your server. Serverless SWML involves no code of yours at all. + +| Approach | How SignalWire reaches your logic | Where it runs | Resource you create | +|---|---|---|---| +| [Server SDK: HTTP (SWML)](#create-your-call-handler) | Requests a SWML document from your server on every call, then runs it | Your server | SWML Script | +| [Server SDK: WebSocket (Relay)](#create-your-call-handler) | Delivers the call over a persistent WebSocket your server holds open. Your code answers it and controls it command by command. | Your server | Relay Application | +| [Browser SDK: WebSocket](#create-your-call-handler) | Delivers the call over the persistent WebSocket a signed-in user's browser holds open. The user answers it. | The user's browser | Subscriber | +| [Serverless SWML](#create-your-call-handler) | Runs a SWML document hosted in your Space. No request reaches you. | SignalWire | SWML Script | + +- **Server SDK: HTTP (SWML)** builds the document with [`SWMLService`][py-swml-service] in Python + or [`SwmlBuilder`][ts-swml-builder] in TypeScript and serves it from your server, so your code + decides what each call does before it's answered. +- **Server SDK: WebSocket (Relay)** receives the call in the [Python][py-relay-client] or + [TypeScript][ts-relay-client] Relay client and controls it command by command, with no public URL. +- **Browser SDK** is Relay running in a signed-in user's browser, so a person answers and speaks + on the call. See the [inbound calls guide][browser-inbound]. +- **Serverless SWML** runs the same document from a hosted SWML Script in your Space when the call + logic is fixed. The [SWML quickstart][swml-quickstart] covers hosted scripts; AI Agents and Call + Flows are other Dashboard-built handlers this page doesn't cover. + +| Function | Server SDK: HTTP | Server SDK: WebSocket | Browser SDK | Serverless SWML | +|---|---|---|---|---| +| Decide from your code what happens before the call is answered, such as looking the caller up | | | | | +| React to events in your own code while the call is live, with no public URL | | | | | +| A person answers and speaks on the call | | | | | +| Start an AI agent, record, stream, or bridge the call | | | | | +| Command the call from another process by its call ID | | | | | +| Nothing to run or expose on your side | | | | | + + +A Browser SDK client receives calls only when it authenticates with a Subscriber token issued for +a specific Subscriber, and that Subscriber is the address's call handler. Guest tokens and embed +tokens, which power click-to-call widgets, are outbound-only and never ring. + + +The REST Calling API places calls; it doesn't answer them. To act on an inbound call from REST +later, use the call ID that SWML or Relay gives you. + ## Prepare for your first call Have these values ready: @@ -83,71 +136,12 @@ A [trial project][trial-mode] receives calls to its phone numbers only from phon verified. Verify the phone you'll call from, or upgrade the project, before you dial. -## How a call reaches your code - -Everything that can handle a call in your Space is a [Resource][resources]: a SWML Script, a -Relay Application, a Subscriber, an AI Agent. Callers don't dial the Resource itself. They dial -one of its [addresses][addresses], a phone number, a SIP address, or an alias such as -`/public/support`, and SignalWire hands the call to the Resource assigned to that address. A -phone number's call handler is assigned separately from its message handler, and the number takes -no action until you assign one. The [Addresses guide][addresses] explains each address type, -contexts, and how to add or move addresses. - -The Resource type decides how you control the call: - -| Resource | What SignalWire does with the call | -|---|---| -| SWML Script | Runs a SignalWire Markup Language (SWML) document that it fetches from your server on every call, or that you host in your Space | -| Relay Application | Delivers the call over a persistent WebSocket to your Server SDK client subscribed to the application's topic | -| Subscriber | Rings that user's registered devices, including a Browser SDK client and any SIP endpoints | -| AI Agent, Call Flow | Runs the agent or flow you built in the Dashboard | - -The rest of this page covers the first three. Because the Resource is what handles the call, the -same handler runs whether the caller dialed your phone number, its [SIP address][sip-credentials], -or one of its aliases. - ## Answer your first call -Play a short announcement to anyone who dials your number. +Play a short announcement to anyone who dials your address. -### Choose how to handle the call - -Inbound handling comes down to two questions: where does your logic run, and how does SignalWire -reach it? The Server SDKs answer both ways. They can serve a SWML document over HTTP, or hold a -WebSocket open with Relay. The Browser SDK is Relay too, running in the user's browser instead -of on your server. Serverless SWML involves no code of yours at all. - -| Approach | How SignalWire reaches your logic | Where it runs | -|---|---|---| -| [Server SDK: HTTP (SWML)](#create-your-call-handler) | Requests a SWML document from your server on every call, then runs it | Your server | -| [Server SDK: WebSocket (Relay)](#create-your-call-handler) | Delivers the call over a persistent WebSocket your server holds open. Your code answers it and controls it command by command. | Your server | -| [Browser SDK: WebSocket](#create-your-call-handler) | Delivers the call over the persistent WebSocket a signed-in user's browser holds open. The user answers it. | The user's browser | -| [Serverless SWML](#create-your-call-handler) | Runs a SWML document hosted in your Space. No request reaches you. | SignalWire | - -SWML is the same document whether your Server SDK app serves it or you host it; the SDK builds -it with `SWMLService` or `SwmlBuilder`. Relay is the same protocol whether the Server SDK or the -Browser SDK holds the connection; the difference is whether your code or a person is on the call. - -| Function | Server SDK: HTTP | Server SDK: WebSocket | Browser SDK | Serverless SWML | -|---|---|---|---|---| -| Decide from your code what happens before the call is answered, such as looking the caller up | | | | | -| React to events in your own code while the call is live, with no public URL | | | | | -| A person answers and speaks on the call | | | | | -| Start an AI agent, record, stream, or bridge the call | | | | | -| Command the call from another process by its call ID | | | | | -| Nothing to run or expose on your side | | | | | - - -A Browser SDK client receives calls only when it authenticates with a Subscriber token issued for -a specific Subscriber, and that Subscriber is the address's call handler. Guest tokens and embed -tokens, which power click-to-call widgets, are outbound-only and never ring. - - -The REST Calling API places calls; it doesn't answer them. To act on an inbound call from REST -later, use the call ID that SWML or Relay gives you. - ### Choose how callers reach you Your handler is a Resource, and callers reach it through an [address][addresses]. Pick the @@ -176,6 +170,8 @@ Replace these values in the code sample you choose: | `` | The ID shown on the phone number's page in the Dashboard, or returned by [List phone numbers][rest-list-numbers] | | `` | The ID shown on your handler's Resource page, or returned when you create it with the REST API | | `` | The public URL where your server serves the SWML document, including the basic-auth credentials for the Python example | +| `` | The public hostname of your server or tunnel | +| `` | A password you choose; the Python server requires it on every request | | `` | A [Subscriber token][subscriber-token] created by your backend, for the Browser SDK examples | ### Create your call handler @@ -192,7 +188,7 @@ server in TypeScript, then expose the port on a public HTTPS URL. A tunnel such development. - + ```python # Install: python -m pip install signalwire-sdk==3.4.1 # Save as inbound_call.py and run: python inbound_call.py @@ -211,7 +207,7 @@ service.add_verb("play", {"url": "say:Hello, welcome to SignalWire!"}) service.serve() ``` - + ```typescript // Install: npm install @signalwire/sdk@2.0.5 // This sample also runs as JavaScript: save as inbound-call.mjs, @@ -329,11 +325,10 @@ next step, and only a client subscribed to it receives the call. A Browser SDK client authenticates as a Subscriber, registers, and receives the call in the page. Show the caller, then let the user answer or decline. The token must be a Subscriber token -for the Subscriber you'll assign the number to; a guest or embed token comes online but never -rings. +for the Subscriber you'll give the address to. ```javascript -// Install: npm install @signalwire/js@latest rxjs +// 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: //

Offline

//

@@ -397,8 +392,8 @@ hangupButton.onclick = () => { }; ``` -Constructing the client authenticates the user, and [`register()`][browser-register] brings them -online so calls can reach them. Leave the page open while you test. +Constructing the client authenticates and registers the user. Awaiting [`register()`][browser-register] +confirms they're online before the page says so. Leave the page open while you test. @@ -407,8 +402,7 @@ Serverless SWML runs the same document the Server SDK serves over HTTP, hosted i instead of on your server. There's nothing to run: in the next step you paste the document into a hosted SWML Script, and SignalWire executes it on every call. Use it when the call logic is fixed, or to try a document before you build the server. The [SWML quickstart][swml-quickstart] covers hosted -scripts in depth, and the AI Agent and Call Flow Resources are serverless handlers you build in -the Dashboard rather than as SWML. +scripts in depth. This document plays an announcement, then hangs up when it ends: @@ -459,16 +453,15 @@ Give it a name and leave **Used For** set to **Calling**. Under **Handle Calls U -![The New SWML Script form in the Dashboard with Handle Calls Using set to External URL](/assets/images/dashboard/resources/external-swml-script.webp) +![The New SWML Application form in the Dashboard with Handle Calls Using set to External URL](/assets/images/dashboard/resources/external-swml-script.webp) -With the REST API, [create a SWML webhook][rest-create-webhook] pointing at your URL. +With the REST API, [create a SWML webhook][rest-create-webhook] pointing at your URL. If the +address will be a phone number, the shortcut in the next step creates and assigns it in one call. - - ```bash curl -X POST "https://.signalwire.com/api/fabric/resources/swml_webhooks" \ -u ":" \ @@ -478,43 +471,6 @@ curl -X POST "https://.signalwire.com/api/fabric/resources/swml_webh "primary_request_url": "" }' ``` - - -```python -# Install: python -m pip install signalwire-sdk==3.4.1 -from signalwire.rest import RestClient - -client = RestClient( - project="", - token="", - host=".signalwire.com", -) - -resource = client.fabric.swml_webhooks.create( - name="Inbound welcome", primary_request_url="", -) -print(resource["id"]) -``` - - -```typescript -// Install: npm install @signalwire/sdk@2.0.5 -import { RestClient } from "@signalwire/sdk"; - -const client = new RestClient({ - project: "", - token: "", - host: ".signalwire.com", -}); - -const resource = await client.fabric.swmlWebhooks.create({ - name: "Inbound welcome", - primary_request_url: "", -}); -console.log(resource.id); -``` - - @@ -523,10 +479,9 @@ In the Dashboard, open **My Resources**, select **+ Add**, then **Relay Applicat a name, enter `inbound-calling` as the **Topic** to match the `contexts` value in your code, and select **Create**. -With the REST API, [create a Relay Application][rest-create-relay-app] with the same topic. +With the REST API, [create a Relay Application][rest-create-relay-app] with the same topic. If the +address will be a phone number, the shortcut in the next step creates and assigns it in one call. - - ```bash curl -X POST "https://.signalwire.com/api/fabric/resources/relay_applications" \ -u ":" \ @@ -536,51 +491,13 @@ curl -X POST "https://.signalwire.com/api/fabric/resources/relay_app "topic": "inbound-calling" }' ``` - - -```python -# Install: python -m pip install signalwire-sdk==3.4.1 -from signalwire.rest import RestClient - -client = RestClient( - project="", - token="", - host=".signalwire.com", -) - -resource = client.fabric.relay_applications.create( - name="Inbound welcome", topic="inbound-calling", -) -print(resource["id"]) -``` - - -```typescript -// Install: npm install @signalwire/sdk@2.0.5 -import { RestClient } from "@signalwire/sdk"; - -const client = new RestClient({ - project: "", - token: "", - host: ".signalwire.com", -}); - -const resource = await client.fabric.relayApplications.create({ - name: "Inbound welcome", - topic: "inbound-calling", -}); -console.log(resource.id); -``` - - -The Subscriber is the Resource. Creating its [Subscriber token][subscriber-token] creates the -Subscriber too when the `reference` is new, or you can [create one directly][rest-create-subscriber]. -Find it under **My Resources** and note its ID. The token you give the browser client must be for -this Subscriber. +The Subscriber is the Resource. [Create one][rest-create-subscriber], then issue its +[Subscriber token][subscriber-token] with the same `reference`. Find it under **My Resources** and +note its ID. The token you give the browser client must be for this Subscriber. @@ -669,6 +586,7 @@ curl -X PUT "https://.signalwire.com/api/relay/rest/phone_numbers/ ```python # Install: python -m pip install signalwire-sdk==3.4.1 +# Save as assign_number.py and run: python assign_number.py from signalwire.rest import RestClient client = RestClient( @@ -686,6 +604,8 @@ client.phone_numbers.set_relay_topic("", "inbound-calling" ```typescript // Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as assign-number.mjs, +// then run: node assign-number.mjs import { RestClient } from "@signalwire/sdk"; const client = new RestClient({ @@ -739,7 +659,7 @@ Subscriber token or a REST dial. To let any client reach a `private` Resource, including one with a guest token, or to reach it under another name or limit it to audio, video, or messaging, add a `public` alias: select -**+ Add**, then **Alias**, and fill in the **Alias**, **Display Name**, **Context**, and +**+ Add**, then **Alias**, and fill in the **Name**, **Display Name**, **Context**, and **Channels**. With the REST API, [create an alias address][rest-create-alias] with a `name`, the `resource_id`, and a `context`. @@ -754,9 +674,6 @@ curl -X POST "https://.signalwire.com/api/fabric/alias_addresses" \ }' ``` -A `public` alias is reachable by any client, including one with a guest token. A `private` alias -is reachable only by authenticated users and by your own project's dials. - @@ -886,11 +803,11 @@ sequenceDiagram The [`Call`][py-relay-call] object your handler receives carries the caller in `device`, together with `direction`, `context`, and `call_id`. Register a `calling.call.state` listener to see `answered`, `ending`, and `ended` as they happen, and wait for the call to end before your -handler returns. +handler returns. The sample extends the first-run Relay handler; the highlighted lines are new. -```python +```python {4,14-15,17-18,20,29} # Install: python -m pip install signalwire-sdk==3.4.1 # Save as inbound_call.py and run: python inbound_call.py from signalwire.relay import RelayClient @@ -925,7 +842,7 @@ client.run() ``` -```typescript +```typescript {13-14,16-18,26} // Install: npm install @signalwire/sdk@2.0.5 // This sample also runs as JavaScript: save as inbound-call.mjs, // then run: node inbound-call.mjs @@ -1139,8 +1056,8 @@ await client.run(); ### Build a phone menu Offer the caller a choice, then connect them to the matching destination. Replace -`` and `` with phone numbers, SIP URIs, or -Resource addresses. +`` and `` with phone numbers in E.164 format. +SWML `connect` also accepts SIP URIs and Resource addresses. #### Build a phone menu via SWML @@ -1256,11 +1173,10 @@ client.run() ```typescript // Install: npm install @signalwire/sdk@2.0.5 -// This sample also runs as JavaScript: save as inbound-call.mjs, -// then run: node inbound-call.mjs +// Save as inbound-call.mts and run: npx tsx inbound-call.mts import { RelayClient } from "@signalwire/sdk"; -const DESTINATIONS = { +const DESTINATIONS: Record = { "1": "", "2": "", }; @@ -1305,7 +1221,7 @@ to the caller. #### Forward the call via SWML Use `connect.confirm` to play the [whisper][swml-call-whisper] to the agent before bridging the -two legs. The caller keeps hearing ringing until the agent is connected. +two legs. @@ -1566,7 +1482,8 @@ Announce the recording where consent rules require it, as in the voicemail examp #### Record the call via SWML Start [`record_call`][swml-record-call] before [`connect`][swml-connect]. Recording continues -while the two legs talk, and SignalWire posts the result to `status_url` when the call ends. +while the two legs talk, and SignalWire posts the result to `status_url` when the recording +finishes. @@ -1854,7 +1771,7 @@ access the microphone. ```javascript -// Install: npm install @signalwire/js@latest rxjs +// Install: npm install @signalwire/js@4.0.0-rc.2 rxjs@7.8.2 // Save as answer.js next to the page above. // GET /api/subscriber-token is your own endpoint: it creates a Subscriber // token for the signed-in user with your Project API token and returns it @@ -1937,8 +1854,8 @@ comeOnline().catch((error) => { #### Choose what the user sends back -`answer()` takes the same `audio` and `video` options as an outbound `dial()`. Audio defaults -to on and video to off, so `answer()` with no options is a phone-style call. +`answer()` takes the same `audio` and `video` options as an outbound `dial()`. With no options +it mirrors the media the caller offered, so pass both explicitly to pin the call's shape. @@ -1975,31 +1892,12 @@ Joins on camera with the microphone muted, for a kiosk or a viewer who watches w To pin the microphone, camera, or speaker across every call, use the [device management APIs][browser-device-management]. -Watch the browser console as the call arrives: the call is `ringing` until the user acts, then -moves through `connecting` to `connected`, and through `disconnecting`, `disconnected`, and -`destroyed` once it ends. If the page comes online but never rings, the number is assigned to a -different Subscriber than the one the token was issued for. If `register()` rejects, the token -is a guest or embed token, which can't receive calls. See the [authentication guide][browser-auth] -for the token lifecycle. +If the page comes online but never rings, either the address belongs to a different Subscriber +than the one the token was issued for, or the token is a guest or embed token, which registers but +never receives calls. If `register()` rejects, the credentials themselves were refused. See the +[authentication guide][browser-auth] for the token lifecycle. -`hangup()` ends the call for everyone. To leave the page but keep the call alive on the platform, -use [`transfer()`][browser-transfer] instead. For the full receiver walkthrough, two callers +`hangup()` ends the call for everyone. To hand the caller to another destination and drop out +yourself, use [`transfer()`][browser-transfer] with a `destination`. For the full receiver walkthrough, two callers ringing at once, and a test dial from the REST API, see the [inbound calls guide][browser-inbound]; for mute, hold, and other in-call controls, see [call controls][browser-call-controls]. - -## Next steps - - - - Place calls from your backend or the browser, and choose what runs when someone answers. - - - Serve a different SWML document for every call and read the caller's details on your server. - - - Contexts, actions, events, and every call-control method in the Server SDKs. - - - Configure webhooks for phone numbers and understand callback reliability. - - diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index fe7b94fc11..8f72ac15a3 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -300,7 +300,7 @@ try { ```javascript -// Install: npm install @signalwire/js@latest rxjs +// 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: // // @@ -552,7 +552,7 @@ try { ```javascript -// Install: npm install @signalwire/js@latest rxjs +// 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

// @@ -1703,7 +1703,7 @@ Serve the page over HTTPS or `localhost` so the browser can access the microphon
```javascript -// Install: npm install @signalwire/js@latest rxjs +// 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": "..."}. @@ -1818,27 +1818,6 @@ directory, an `Address` exposes [`defaultChannel`][address-default-channel], a r you don't assemble the string yourself. To pin the microphone, camera, or speaker across every call instead of constraining each `dial()`, use the [device management APIs][device-management]. -#### When local media can't be captured - -If the local media can't be captured, the SDK falls back to receive-only: the call connects, remote media arrives on -`remoteStream$`, and the reason is reported on [`errors$`][call-errors] as a non-fatal -[`MediaAccessError`][media-access-error]. - -```javascript -import { MediaAccessError } from "@signalwire/js"; - -call.errors$.subscribe(({ error, fatal }) => { - if (error instanceof MediaAccessError && !fatal) { - banner.textContent = error.denied - ? `You joined without ${error.media}. Grant access and rejoin to send it.` - : `Couldn't open your ${error.media}. You're receiving only.`; - } -}); -``` - -Set [`fallbackToReceiveOnly`][media-options] to `false` to make an acquisition failure fatal -instead. - #### Attach the media to the page The call exposes `localStream$` (what the user sends) and `remoteStream$` (what the user receives). @@ -1869,3 +1848,24 @@ use [`transfer()`][browser-transfer] instead. For receiving calls in the browser, see the [inbound calls guide][browser-inbound]; for mute, hold, and other in-call controls, see [call controls][browser-call-controls]. + +#### When local media can't be captured + +If the local media can't be captured, the SDK falls back to receive-only: the call connects, remote media arrives on +`remoteStream$`, and the reason is reported on [`errors$`][call-errors] as a non-fatal +[`MediaAccessError`][media-access-error]. + +```javascript +import { MediaAccessError } from "@signalwire/js"; + +call.errors$.subscribe(({ error, fatal }) => { + if (error instanceof MediaAccessError && !fatal) { + banner.textContent = error.denied + ? `You joined without ${error.media}. Grant access and rejoin to send it.` + : `Couldn't open your ${error.media}. You're receiving only.`; + } +}); +``` + +Set [`fallbackToReceiveOnly`][media-options] to `false` to make an acquisition failure fatal +instead. From f5ddb3454d28b32fffcb4b28431b86679fb4340b Mon Sep 17 00:00:00 2001 From: Devon-White Date: Fri, 18 Sep 2026 13:19:20 -0400 Subject: [PATCH 11/14] docs(voice): restructure the inbound guide and correct its routing claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow the outbound guide's flow — prepare, choose, set up, test, track, examples — and keep each handler's code and Dashboard setup together. Correct the capability claims the old matrix got wrong: - SWML and Relay do reach a person, via `connect` to a phone number, SIP address, or queue. The old table's single "a person answers" row conflated how a call is routed with where a person picks it up. - Serverless SWML can look a caller up with `request` and branch on the result with `switch`. What it can't do is vary the document per call. - Drop wording implying a company number must be assigned directly to a Subscriber for a browser user to answer; a routing handler can forward. Replace the check/cross scorecards with descriptive comparisons of purpose and control model, so a missing direct SDK method isn't read as the whole surface being underpowered. Build SWML through `SWMLService` and `SwmlBuilder` wherever the pinned SDK versions support the instruction, and order examples Python, TypeScript, then cURL. The raw `stream` payload stays, with a comment naming the version gap that requires it. --- .../pages/calling/voice/inbound-calling.mdx | 1110 ++++++++++++----- .../pages/calling/voice/outbound-calling.mdx | 98 +- 2 files changed, 821 insertions(+), 387 deletions(-) diff --git a/fern/products/platform/pages/calling/voice/inbound-calling.mdx b/fern/products/platform/pages/calling/voice/inbound-calling.mdx index 8af9725f9f..7b4f76801e 100644 --- a/fern/products/platform/pages/calling/voice/inbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/inbound-calling.mdx @@ -1,7 +1,7 @@ --- title: Inbound calling slug: voice/inbound-calling -description: Answer your first inbound call to a phone number, SIP address, or alias, find out who is calling, and add an AI agent, a phone menu, or browser answering. +description: Answer your first inbound call, track its progress, and add an AI agent, call forwarding, a phone menu, or browser answering. max-toc-depth: 3 --- @@ -13,7 +13,6 @@ max-toc-depth: 3 [subscribers]: /docs/platform/subscribers [webhooks]: /docs/platform/webhooks [ai-best-practices]: /docs/platform/ai/best-practices -[sip-credentials]: /docs/platform/voice/sip/sip-credentials [swml-quickstart]: /docs/swml/guides [swml-webhook-security]: /docs/swml/guides/webhook-security [swml-ivr]: /docs/swml/guides/ivr @@ -24,12 +23,15 @@ max-toc-depth: 3 [swml-prompt]: /docs/swml/reference/calling/prompt [swml-switch]: /docs/swml/reference/calling/switch [swml-connect]: /docs/swml/reference/calling/connect +[swml-enter-queue]: /docs/swml/reference/calling/enter-queue +[swml-request]: /docs/swml/reference/calling/request [swml-record]: /docs/swml/reference/calling/record [swml-record-call]: /docs/swml/reference/calling/record-call [swml-stream]: /docs/swml/reference/calling/stream [py-relay-call]: /docs/server-sdks/reference/python/relay/call [py-relay-events]: /docs/server-sdks/reference/python/relay/events [py-swml-service]: /docs/server-sdks/reference/python/agents/swml-service +[py-swml-builder]: /docs/server-sdks/reference/python/agents/swml-builder [ts-swml-builder]: /docs/server-sdks/reference/typescript/agents/swml-builder [py-relay-client]: /docs/server-sdks/reference/python/relay/client [ts-relay-client]: /docs/server-sdks/reference/typescript/relay/client @@ -39,13 +41,20 @@ max-toc-depth: 3 [ts-set-relay-topic]: /docs/server-sdks/reference/typescript/rest/phone-numbers/set-relay-topic [rest-update-number]: /docs/apis/rest/phone-numbers/update-phone-number [rest-list-numbers]: /docs/apis/rest/phone-numbers/list-phone-numbers +[py-create-swml-webhooks]: /docs/server-sdks/reference/python/rest/fabric/swml-webhooks/create +[ts-create-swml-webhooks]: /docs/server-sdks/reference/typescript/rest/fabric/swml-webhooks/create +[py-create-relay-applications]: /docs/server-sdks/reference/python/rest/fabric/relay-applications/create +[ts-create-relay-applications]: /docs/server-sdks/reference/typescript/rest/fabric/relay-applications/create +[py-create-swml-scripts]: /docs/server-sdks/reference/python/rest/fabric/swml-scripts/create +[ts-create-swml-scripts]: /docs/server-sdks/reference/typescript/rest/fabric/swml-scripts/create +[py-create-subscribers]: /docs/server-sdks/reference/python/rest/fabric/subscribers/create +[ts-create-subscribers]: /docs/server-sdks/reference/typescript/rest/fabric/subscribers/create [rest-create-script]: /docs/apis/rest/swml-scripts/create-swml-script [rest-create-webhook]: /docs/apis/rest/swml-webhook/create-swml-webhook [rest-create-relay-app]: /docs/apis/rest/relay-application/create-relay-application [rest-create-subscriber]: /docs/apis/rest/subscribers/create-subscriber [rest-create-sip]: /docs/apis/rest/sip-addresses/create-sip-address [rest-create-alias]: /docs/apis/rest/alias-addresses/create-alias-address -[rest-calling-dial]: /docs/apis/rest/calls/call-commands [outbound-calling]: /docs/platform/voice/outbound-calling [rest-link-number]: /docs/apis/rest/phone-number-addresses/create-phone-number-address [subscriber-token]: /docs/apis/rest/subscribers/tokens/create-subscriber-token @@ -58,78 +67,22 @@ max-toc-depth: 3 [browser-call]: /docs/browser-sdk/v4/reference/interfaces/call [browser-transfer]: /docs/browser-sdk/v4/reference/webrtc-call/transfer -Answer a call to one of your SignalWire addresses, whether that's a phone number, a SIP address, -or an alias another client dials, and choose what happens when it rings. Start by playing a short -announcement to yourself, then find out who is calling, run an AI agent, build a phone menu, or -let a signed-in user answer from your web app. - -## Pick the right product for inbound calls - -Everything that can handle a call in your Space is a [Resource][resources]: a SWML Script, a -Relay Application, a Subscriber, an AI Agent, a Call Flow. Callers don't dial the Resource itself. -They dial one of its [addresses][addresses], a phone number, a [SIP address][sip-credentials], or -an alias such as `/public/support`, and SignalWire hands the call to the Resource assigned to that -address. The same handler runs whichever of its addresses the caller dialed. A phone number's call -handler is assigned separately from its message handler, and the number takes no action until you -assign one. The [Addresses guide][addresses] explains each address type, contexts, and how to add -or move addresses. - -The Resource type decides how you control the call, and this page covers the three you drive from -code. Inbound handling comes down to two questions: where does your logic run, and how does -SignalWire reach it? The Server SDKs answer both ways. They can serve a SWML document over HTTP, -or hold a WebSocket open with Relay. The Browser SDK is Relay too, running in the user's browser -instead of on your server. Serverless SWML involves no code of yours at all. - -| Approach | How SignalWire reaches your logic | Where it runs | Resource you create | -|---|---|---|---| -| [Server SDK: HTTP (SWML)](#create-your-call-handler) | Requests a SWML document from your server on every call, then runs it | Your server | SWML Script | -| [Server SDK: WebSocket (Relay)](#create-your-call-handler) | Delivers the call over a persistent WebSocket your server holds open. Your code answers it and controls it command by command. | Your server | Relay Application | -| [Browser SDK: WebSocket](#create-your-call-handler) | Delivers the call over the persistent WebSocket a signed-in user's browser holds open. The user answers it. | The user's browser | Subscriber | -| [Serverless SWML](#create-your-call-handler) | Runs a SWML document hosted in your Space. No request reaches you. | SignalWire | SWML Script | - -- **Server SDK: HTTP (SWML)** builds the document with [`SWMLService`][py-swml-service] in Python - or [`SwmlBuilder`][ts-swml-builder] in TypeScript and serves it from your server, so your code - decides what each call does before it's answered. -- **Server SDK: WebSocket (Relay)** receives the call in the [Python][py-relay-client] or - [TypeScript][ts-relay-client] Relay client and controls it command by command, with no public URL. -- **Browser SDK** is Relay running in a signed-in user's browser, so a person answers and speaks - on the call. See the [inbound calls guide][browser-inbound]. -- **Serverless SWML** runs the same document from a hosted SWML Script in your Space when the call - logic is fixed. The [SWML quickstart][swml-quickstart] covers hosted scripts; AI Agents and Call - Flows are other Dashboard-built handlers this page doesn't cover. - -| Function | Server SDK: HTTP | Server SDK: WebSocket | Browser SDK | Serverless SWML | -|---|---|---|---|---| -| Decide from your code what happens before the call is answered, such as looking the caller up | | | | | -| React to events in your own code while the call is live, with no public URL | | | | | -| A person answers and speaks on the call | | | | | -| Start an AI agent, record, stream, or bridge the call | | | | | -| Command the call from another process by its call ID | | | | | -| Nothing to run or expose on your side | | | | | - - -A Browser SDK client receives calls only when it authenticates with a Subscriber token issued for -a specific Subscriber, and that Subscriber is the address's call handler. Guest tokens and embed -tokens, which power click-to-call widgets, are outbound-only and never ring. - - -The REST Calling API places calls; it doesn't answer them. To act on an inbound call from REST -later, use the call ID that SWML or Relay gives you. +Answer an inbound call with SignalWire and choose what happens when someone calls you. +Start by calling your SignalWire number and playing a short announcement, then find out who's +calling, run an AI agent, forward to a person, or let users answer from your web app. +You can also receive calls through a SIP address or an alias. ## Prepare for your first call 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, and its **Numbers** permission if you assign the - handler with the REST API rather than the Dashboard. -- A voice-capable [phone number in your Space][phone-numbers], only if you're taking calls from - the phone network. SIP addresses and aliases need no number. -- Something to place the test call from: a phone, a SIP softphone, or another SignalWire client, - depending on the address you pick. -- If answering in the browser, a [Subscriber token][subscriber-token] for the [Subscriber][subscribers] - that will take the calls. Guest and embed tokens can't receive calls. +- A voice-capable [phone number in your Space][phone-numbers] and a phone to call it from. + If you're testing with SIP or another SignalWire client, you can use an address instead. +- For the Server SDK examples, your Space URL, Project ID, and API token from the Dashboard's + [API credentials][api-credentials] page. Enable the token's **Voice** permission, and + **Numbers** if you assign a phone number through the API. +- For browser answering, a [Subscriber][subscribers] and a [Subscriber token][subscriber-token] + issued by your backend for that Subscriber. A [trial project][trial-mode] receives calls to its phone numbers only from phone numbers it has @@ -138,61 +91,86 @@ verified. Verify the phone you'll call from, or upgrade the project, before you ## Answer your first call -Play a short announcement to anyone who dials your address. +Create a call handler, give it an address, and call that address to test it. The SWML and Relay +examples play a short announcement; the browser example lets you answer and speak on the call. -### Choose how callers reach you +### Choose how to handle your call -Your handler is a Resource, and callers reach it through an [address][addresses]. Pick the -address that matches who's calling. The choice is independent of the handler: any Resource can -carry any of these, and one Resource can carry all three. +Choose how your application handles the incoming call. The person who eventually speaks to +the caller can answer on a phone, a SIP device, or in a browser. -| Address | Who dials it | You need | +For example, a customer calls your company number, a SWML or Relay handler plays a greeting +and routes the call, and an agent answers at the destination. The company number stays assigned +to the routing handler. If the agent uses your web app, the Browser SDK receives the call as +that agent's Subscriber. These approaches can work together in the same call flow. + +| Approach | Useful when | How the call reaches your logic or user | |---|---|---| -| Phone number | Anyone on the phone network | A voice-capable number in your Space, and a phone to test from | -| SIP address | SIP softphones, PBXs, and carriers outside SignalWire, dialing the URI SignalWire assigns | A SIP client to test from | -| Alias | Other SignalWire clients and Resources: a Browser SDK `dial()`, a SWML `connect`, or a REST dial to `//` | A Browser SDK client or another Resource to test from | +| [Server SDK — HTTP (SWML)](#create-your-call-handler) | Your server chooses instructions for each incoming call | SignalWire requests a document from your public HTTPS endpoint and runs the SWML it returns. | +| [Server SDK — WebSocket (Relay)](#create-your-call-handler) | Your server makes decisions as call events arrive | A running Relay client receives calls for its topic and controls them over a persistent connection. | +| [Browser SDK](#create-your-call-handler) | An agent receives and handles calls in your web app | A browser client authenticated as the agent's Subscriber receives the call, provides media and in-call controls, and updates the page as its status changes. | +| [Hosted SWML](#create-your-call-handler) | You want SignalWire to host and run the call instructions | The assigned script runs in your Space, using call variables, branches, and HTTP requests as the flow needs them. | + +If you just want to hear your first call work, choose **Hosted SWML**. SWML is the document of +call instructions SignalWire runs, whether you host it in your Space or return it from a server. +Both SWML approaches can look up data with [`request`][swml-request], branch with +[`switch`][swml-switch], and [forward the call to a person or place it in a queue](#route-the-call-to-a-person-or-queue). +Relay can bridge the caller to a person's phone or SIP device with `connect()`. -The steps below show the address you pick in its own tab, alongside the handler tabs. Stay on the -same tabs through the guide. +In SignalWire, your call handler is a [Resource][resources]. You'll create a SWML Script for +either SWML approach, a Relay Application for Relay, or use a Subscriber for the Browser SDK. +An [address][addresses] tells SignalWire which Resource should receive the call. ### Set your credentials -Replace these values in the code sample you choose: +Replace the values used by your chosen example. Hosted SWML needs no credentials in the document. | Value | Replace with | |---|---| | `` | Your Space's subdomain in `.signalwire.com` | | `` | Your Project ID | -| `` | Your API token | -| `` | Your SignalWire number in E.164 format, such as `+12025550123` | -| `` | The ID shown on the phone number's page in the Dashboard, or returned by [List phone numbers][rest-list-numbers] | -| `` | The ID shown on your handler's Resource page, or returned when you create it with the REST API | -| `` | The public URL where your server serves the SWML document, including the basic-auth credentials for the Python example | -| `` | The public hostname of your server or tunnel | -| `` | A password you choose; the Python server requires it on every request | -| `` | A [Subscriber token][subscriber-token] created by your backend, for the Browser SDK examples | +| `` | Your API token, used only in server code | +| `` | A [Subscriber token][subscriber-token] created by your backend, for the Browser SDK example | + +The HTTP example also needs a public HTTPS URL for your server. Its Python sample uses +`` for a password you choose and `` for your +server or tunnel's hostname. + +### Choose how callers reach you + +For this walkthrough, use your SignalWire phone number and call it from your own phone. +If you're testing from a SIP client or another SignalWire application, choose its address type +instead. Each works with the handlers above. + +| Address | Who can call it | What to use for your test | +|---|---|---| +| Phone number | Anyone on the phone network, subject to trial restrictions | Your SignalWire number in E.164 format, such as `+12025550123` | +| SIP address | SIP softphones, PBXs, and carriers | The SIP URI assigned when you add the address | +| Alias | SignalWire clients and Resources with access to its context | The Resource's alias, such as `/private/inbound-welcome` | + +You'll attach or find this address after creating the handler. ### Create your call handler -Write the code SignalWire runs, or connects to, when the number rings. +Open the tab for your approach. Complete its setup, then continue to **Give the Resource an address**. - + -A SWML document tells SignalWire what to do with the call. When the number rings, SignalWire -requests the document from your server and runs it. Build and serve it with a Server SDK, -[`SWMLService`][py-swml-service] in Python or [`SwmlBuilder`][ts-swml-builder] behind an HTTP -server in TypeScript, then expose the port on a public HTTPS URL. A tunnel such as [ngrok](https://ngrok.com/) works for -development. +SignalWire requests a SWML document from your server when a call arrives. These examples return +instructions to play "Hello, welcome to SignalWire!" and end the call. + +Run either server below, then expose port 3000 at a public HTTPS URL. A tunnel such as +[ngrok](https://ngrok.com/) works for development. - + ```python # Install: python -m pip install signalwire-sdk==3.4.1 # Save as inbound_call.py and run: python inbound_call.py -from signalwire import SWMLService +from signalwire import SWMLBuilder, SWMLService service = SWMLService( name="inbound-call", @@ -200,14 +178,14 @@ service = SWMLService( port=3000, basic_auth=("signalwire", ""), ) -service.add_verb("play", {"url": "say:Hello, welcome to SignalWire!"}) +SWMLBuilder(service).say("Hello, welcome to SignalWire!") # Serves the document at /swml. Put the credentials in the URL you give # SignalWire: https://signalwire:@/swml service.serve() ``` - + ```typescript // Install: npm install @signalwire/sdk@2.0.5 // This sample also runs as JavaScript: save as inbound-call.mjs, @@ -228,41 +206,30 @@ createServer((request, response) => { -Anyone who learns the URL can fetch your document, so [verify the request signature][swml-webhook-security] -before you serve anything sensitive. +The Python example uses [`SWMLBuilder`][py-swml-builder] to add instructions to the +[`SWMLService`][py-swml-service] that serves them. It requires the basic-auth credentials +in its URL: `https://signalwire:@/swml`. +The TypeScript server uses [`SwmlBuilder`][ts-swml-builder] and serves the document at every +path on port 3000. See [webhook security][swml-webhook-security] to verify incoming requests. -Both servers return this document. It plays the announcement, then hangs up when the document -ends. +Once your server is reachable, create its Resource in the Dashboard: - - -```yaml -version: 1.0.0 -sections: - main: - - play: - url: 'say:Hello, welcome to SignalWire!' -``` - - -```json -{ - "version": "1.0.0", - "sections": { - "main": [ - { "play": { "url": "say:Hello, welcome to SignalWire!" } } - ] - } -} -``` - - +1. Open **My Resources**, select **+ Add**, then **Script**, then **SWML Script**. +2. Name it **Inbound welcome** and leave **Used For** set to **Calling**. +3. Under **Handle Calls Using**, choose **External URL** and enter your server's public URL in + **Primary Script URL**. Include the basic-auth credentials if you used the Python example. +4. Select **Create** and keep the server running. + +To create it in code, use the [Python][py-create-swml-webhooks] or +[TypeScript][ts-create-swml-webhooks] SDK. The [SWML webhook REST endpoint][rest-create-webhook] +is also available. - + -A [`RelayClient`][py-relay-client] subscribes to a topic and receives every call routed to it. -The handler answers, plays the announcement, and hangs up. Keep the process running; it holds the connection open. +Run either handler below and leave it running while you test. It receives calls on the +`inbound-calling` topic, answers, plays the announcement, and hangs up. The client holds a +WebSocket connection open, so you don't need a public HTTP endpoint. @@ -317,15 +284,36 @@ await client.run(); -The `contexts` value is the topic. You'll give the same name to the Relay Application in the -next step, and only a client subscribed to it receives the call. +Connect this handler to a Resource in the Dashboard: + +1. Open **My Resources**, select **+ Add**, then **Relay Application**. +2. Name it **Inbound welcome** and enter `inbound-calling` as the **Topic**. It must match the + `contexts` value in your code. +3. Select **Create**. + +To create it in code, use the [Python][py-create-relay-applications] or +[TypeScript][ts-create-relay-applications] SDK. The [Relay Application REST endpoint][rest-create-relay-app] +is also available. +See the [Python][py-relay-client] or [TypeScript][ts-relay-client] Relay client reference for +more configuration options. - + + +Use a [Subscriber][subscribers] as the Resource. Create one with the +[Python][py-create-subscribers] or [TypeScript][ts-create-subscribers] SDK, or the +[Subscribers REST endpoint][rest-create-subscriber], then issue a [Subscriber token][subscriber-token] with that Subscriber's +`reference`. Use the token in the example below and find the same Subscriber under **My Resources** +when you attach an address in the next step. + + +Guest and embed tokens are outbound-only. The browser must authenticate as the Subscriber +receiving the call. A number can ring that Subscriber directly, or a call handler can route +the call to the Subscriber's address. + -A Browser SDK client authenticates as a Subscriber, registers, and receives the call in the -page. Show the caller, then let the user answer or decline. The token must be a Subscriber token -for the Subscriber you'll give the address to. +Run this example on a web page served over HTTPS or `localhost`, with the elements listed in +its comments. The page shows who is calling and lets you answer, decline, or hang up. ```javascript // Install: npm install @signalwire/js@4.0.0-rc.2 rxjs@7.8.2 @@ -392,19 +380,23 @@ hangupButton.onclick = () => { }; ``` -Constructing the client authenticates and registers the user. Awaiting [`register()`][browser-register] -confirms they're online before the page says so. Leave the page open while you test. +Awaiting [`register()`][browser-register] confirms the client is online. Keep the page open +while you test. The [complete browser example](#answer-in-the-browser) includes the HTML page +and fetches the token from your backend. - + + +Create a SWML Script in the Dashboard. SignalWire hosts and runs the document, so you don't +need to start a server. -Serverless SWML runs the same document the Server SDK serves over HTTP, hosted in your Space -instead of on your server. There's nothing to run: in the next step you paste the document into a hosted SWML -Script, and SignalWire executes it on every call. Use it when the call logic is fixed, or to try -a document before you build the server. The [SWML quickstart][swml-quickstart] covers hosted -scripts in depth. +1. Open **My Resources**, select **+ Add**, then **Script**, then **SWML Script**. +2. Name it **Inbound welcome** and leave **Used For** set to **Calling**. +3. Under **Handle Calls Using**, choose **Hosted Script** and paste the document below into + **Primary Script**. +4. Select **Create**. -This document plays an announcement, then hangs up when it ends: +This document plays "Hello, welcome to SignalWire!" and ends the call: @@ -430,84 +422,66 @@ sections: -A hosted document still sees the call: variables such as `${call.from}` resolve to the caller's -details, so it can react to who's calling without a server. See -[Read the call details via SWML](#read-the-call-details-via-swml). + - - - -### Create the Resource - -Every handler is a Resource in your Space. Create it, then note its ID from the Resource's page -or from the REST response; you'll attach the address to it next. - - - +Use the [Python][py-create-swml-scripts] or [TypeScript][ts-create-swml-scripts] SDK to build the +SWML and create its hosted Resource in one program. This replaces the Dashboard creation steps. +Both programs print the Resource ID; use that Resource when you assign the number next. -In the Dashboard, open **My Resources**, select **+ Add**, then **Script**, then **SWML Script**. -Give it a name and leave **Used For** set to **Calling**. Under **Handle Calls Using**, choose -**External URL** and enter your server's URL in **Primary Script URL**. Select **Create**. - - - - - -![The New SWML Application form in the Dashboard with Handle Calls Using set to External URL](/assets/images/dashboard/resources/external-swml-script.webp) + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as inbound_script.py and run: python inbound_script.py +import json - +from signalwire import SWMLBuilder, SWMLService +from signalwire.rest import RestClient - +client = RestClient( + project="", + token="", + host=".signalwire.com", +) -With the REST API, [create a SWML webhook][rest-create-webhook] pointing at your URL. If the -address will be a phone number, the shortcut in the next step creates and assigns it in one call. +swml = ( + SWMLBuilder(SWMLService(name="inbound-welcome")) + .say("Hello, welcome to SignalWire!") + .build() +) -```bash -curl -X POST "https://.signalwire.com/api/fabric/resources/swml_webhooks" \ - -u ":" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Inbound welcome", - "primary_request_url": "" - }' +script = client.fabric.swml_scripts.create( + name="Inbound welcome", + contents=json.dumps(swml), +) +print(script["id"]) ``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as inbound-script.mjs, +// then run: node inbound-script.mjs +import { RestClient, SwmlBuilder } from "@signalwire/sdk"; - - - -In the Dashboard, open **My Resources**, select **+ Add**, then **Relay Application**. Give it -a name, enter `inbound-calling` as the **Topic** to match the `contexts` value in your code, and -select **Create**. +const client = new RestClient({ + project: "", + token: "", + host: ".signalwire.com", +}); -With the REST API, [create a Relay Application][rest-create-relay-app] with the same topic. If the -address will be a phone number, the shortcut in the next step creates and assigns it in one call. +const swml = new SwmlBuilder() + .say("Hello, welcome to SignalWire!") + .build(); -```bash -curl -X POST "https://.signalwire.com/api/fabric/resources/relay_applications" \ - -u ":" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Inbound welcome", - "topic": "inbound-calling" - }' +const script = await client.fabric.swmlScripts.create({ + name: "Inbound welcome", + contents: JSON.stringify(swml), +}); +console.log(script.id); ``` - - - - -The Subscriber is the Resource. [Create one][rest-create-subscriber], then issue its -[Subscriber token][subscriber-token] with the same `reference`. Find it under **My Resources** and -note its ID. The token you give the browser client must be for this Subscriber. - - - - -In the Dashboard, open **My Resources**, select **+ Add**, then **Script**, then **SWML Script**. -Give it a name and leave **Used For** set to **Calling**. Under **Handle Calls Using**, choose -**Hosted Script** and paste the document into **Primary Script**. Select **Create**. - -With the REST API, [create the SWML Script][rest-create-script] with the document as `contents`. - + + ```bash curl -X POST "https://.signalwire.com/api/fabric/resources/swml_scripts" \ -u ":" \ @@ -524,65 +498,92 @@ curl -X POST "https://.signalwire.com/api/fabric/resources/swml_scri } }' ``` + +
+ +The SDKs send `contents` as a JSON string; the cURL alternative sends a JSON object. The +[SWML Script REST endpoint][rest-create-script] accepts both. + + + +See the [SWML quickstart][swml-quickstart] for more on hosted scripts. ### Give the Resource an address -Attach the address you chose so calls to it reach the Resource. SignalWire already created one -alias from the Resource's name, so a phone number or SIP address is an addition, not a -replacement. +Use the Resource you just created as the address's **call handler**. For a phone number, the +call handler is separate from its message handler. -With the REST API, [link the Resource to the number][rest-link-number]. This endpoint is in beta -and requires the number's calling channel to have no Resource assigned yet. + -```bash -curl -X POST "https://.signalwire.com/api/fabric/phone_number_addresses" \ - -u ":" \ - -H "Content-Type: application/json" \ - -d '{ - "number": "", - "resource_id": "", - "handler_type": "calling" - }' -``` +For an HTTP SWML server or a Relay handler, the Server SDK can create and assign the Resource +in one request. Use this instead of the separate Resource creation and Dashboard assignment +steps above. Choose the example for your handler; assigning another handler replaces the +number's current call route. - +Replace `` with the number's ID from its Dashboard page or +[List phone numbers][rest-list-numbers]. For HTTP SWML, replace `` with your +server's public URL, including the basic-auth credentials for the Python server. -For the Server SDK paths, [Update phone number][rest-update-number] creates the Resource and -assigns it together, so you can skip the create step above. The Server SDKs wrap it as -[`set_swml_webhook`][py-set-swml-webhook] and [`setSwmlWebhook`][ts-set-swml-webhook], and as -[`set_relay_topic`][py-set-relay-topic] and [`setRelayTopic`][ts-set-relay-topic]. +For an HTTP SWML handler, use [`set_swml_webhook`][py-set-swml-webhook] in Python or +[`setSwmlWebhook`][ts-set-swml-webhook] in TypeScript. - -```bash -curl -X PUT "https://.signalwire.com/api/relay/rest/phone_numbers/" \ - -u ":" \ - -H "Content-Type: application/json" \ - -d '{ - "call_handler": "relay_script", - "call_relay_script_url": "" - }' + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as assign_number.py and run: python assign_number.py +from signalwire.rest import RestClient + +client = RestClient( + project="", + token="", + host=".signalwire.com", +) + +client.phone_numbers.set_swml_webhook("", "") ``` - + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as assign-number.mjs, +// then run: node assign-number.mjs +import { RestClient } from "@signalwire/sdk"; + +const client = new RestClient({ + project: "", + token: "", + host: ".signalwire.com", +}); + +await client.phoneNumbers.setSwmlWebhook("", ""); +``` + + ```bash curl -X PUT "https://.signalwire.com/api/relay/rest/phone_numbers/" \ -u ":" \ -H "Content-Type: application/json" \ -d '{ - "call_handler": "relay_topic", - "call_relay_topic": "inbound-calling" + "call_handler": "relay_script", + "call_relay_script_url": "" }' ``` + + +For a Relay handler, use [`set_relay_topic`][py-set-relay-topic] in Python or +[`setRelayTopic`][ts-set-relay-topic] in TypeScript. + + ```python # Install: python -m pip install signalwire-sdk==3.4.1 @@ -595,9 +596,6 @@ client = RestClient( host=".signalwire.com", ) -# Server SDK: HTTP (SWML) -client.phone_numbers.set_swml_webhook("", "") -# Server SDK: WebSocket (Relay) client.phone_numbers.set_relay_topic("", "inbound-calling") ``` @@ -614,123 +612,98 @@ const client = new RestClient({ host: ".signalwire.com", }); -// Server SDK: HTTP (SWML) -await client.phoneNumbers.setSwmlWebhook("", ""); -// Server SDK: WebSocket (Relay) await client.phoneNumbers.setRelayTopic("", { topic: "inbound-calling" }); ```
+ +```bash +curl -X PUT "https://.signalwire.com/api/relay/rest/phone_numbers/" \ + -u ":" \ + -H "Content-Type: application/json" \ + -d '{ + "call_handler": "relay_topic", + "call_relay_topic": "inbound-calling" + }' +``` +
+The cURL examples call [Update phone number][rest-update-number] directly. + +To attach an existing Resource by ID, use [Create a phone number address][rest-link-number] +with the phone number, the Resource's ID, and `handler_type: "calling"`. This endpoint is in +beta and requires the number's calling channel to have no Resource assigned yet. + -Open the Resource from **My Resources**, select its **Addresses & Phone Numbers** tab, select -**+ Add**, then **SIP Address**. Leave **User** as `*` to accept any username, pick a **Domain**, -give the address a **Name**, and select **Create**. A password and an IP allowlist are optional; -leave both off for the first call. The tab then shows the URI SignalWire assigned, in the form -`sip:*@-.dapp.signalwire.com`. - -With the REST API, [create a SIP address][rest-create-sip] that names the Resource as its call -handler. The response includes the `uri`. +1. Open the Resource from **My Resources**, then its **Addresses & Phone Numbers** tab. +2. Select **+ Add**, then **SIP Address**. +3. Leave **User** as `*`, pick a **Domain**, give the address a **Name**, and select **Create**. +4. Copy the assigned URI. With **User** set to `*`, you can dial it with any username, such as + `sip:test@-.dapp.signalwire.com`. -```bash -curl -X POST "https://.signalwire.com/api/fabric/sip_addresses" \ - -u ":" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "inbound-welcome", - "user": "*", - "calling_handler_resource_id": "" - }' -``` +A password and IP allowlist are optional. If you configure either, your test client must meet +those requirements. To add the address through the API, use [Create a SIP address][rest-create-sip] +with your Resource's ID as `calling_handler_resource_id`. -SignalWire created an alias from the Resource's name when you created it. Open the Resource's -**Addresses & Phone Numbers** tab to copy it. Its context depends on the Resource type: a SWML -Script served from an **External URL** starts in `public`, while a hosted SWML Script, an AI -Agent, a Relay Application, or a Subscriber starts in `private`. A `private` alias is reachable -by authenticated Subscribers and by your own project's dials, so it's enough to test with a -Subscriber token or a REST dial. +Open the Resource's **Addresses & Phone Numbers** tab and copy its automatically created alias. +Use the full address, including its context, such as `/private/inbound-welcome`. -To let any client reach a `private` Resource, including one with a guest token, or to reach it -under another name or limit it to audio, video, or messaging, add a `public` alias: select -**+ Add**, then **Alias**, and fill in the **Name**, **Display Name**, **Context**, and -**Channels**. With the REST API, [create an alias address][rest-create-alias] with a `name`, the -`resource_id`, and a `context`. +A `private` alias is reachable by authenticated Subscribers and your project's own dials. +A guest token needs a `public` alias. To add one, select **+ Add**, then **Alias**, and fill in +**Name**, **Display Name**, **Context**, and **Channels**. Choose `public` as the context and +enable the calling channel you need. -```bash -curl -X POST "https://.signalwire.com/api/fabric/alias_addresses" \ - -u ":" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "inbound-welcome", - "resource_id": "", - "context": "public" - }' -``` +To add an alias through the API, use [Create an alias address][rest-create-alias] with a `name`, +your `resource_id`, and a `context`. See the [Addresses guide][addresses] for access rules. ### Place your test call - - - -Dial your SignalWire number from your phone. In a trial project, call from the number you -verified. +Call the address you assigned: - - - -Dial the URI from a SIP softphone or your PBX, for example `sip:test@-.dapp.signalwire.com`. -With **User** set to `*`, any username reaches the Resource. If you set a password, configure it -in the client first. - - - - -Dial the alias from another SignalWire client. The quickest is the browser sample from the -[outbound calling guide][outbound-calling] with the alias as the destination: - -```javascript -const call = await client.dial("", { audio: true, video: false }); -``` +| Address | How to test | +|---|---| +| Phone number | Dial your SignalWire number from your phone. In a trial project, use a verified caller number. | +| SIP address | Dial the assigned URI from a SIP softphone or PBX. Supply the password if you configured one. | +| Alias | Use the [outbound browser example][outbound-calling] with the full alias as its destination. Use a Subscriber token for a private alias. | -A guest token can dial a `public` alias. To reach a `private` alias, the caller must be an -authenticated Subscriber, or place the call from your project with the -[REST Calling API][rest-calling-dial] using the full alias as `to`. +With either SWML approach or Relay, you hear "Hello, welcome to SignalWire!" and the call ends. +With the browser handler, the page shows the incoming call. Select **Answer**, allow microphone +access, and select **Hang up** when you finish. - - + -With either SWML approach or with Relay, you hear "Hello, welcome to SignalWire!" and the call -ends. With the browser handler, the page shows the incoming call. Select **Answer**, allow -microphone access, and select **Hang up** when you finish. +Check assignment and execution separately. A number can have the right Resource assigned while +its HTTP server, Relay client, or browser is offline. -If the call rings without an answer or fails, check these first: +- Check that the address lists your Resource as its call handler. An unassigned phone number + takes no action. +- In a trial project, call your SignalWire phone number from a verified number. +- For HTTP SWML, check that the server is running and SignalWire can reach its URL. Check the + call in the Dashboard's **Logs** for request errors. +- For Relay, keep the client running and match the Resource's **Topic** to `contexts` in your code. +- For browser answering, keep the page open and use a Subscriber token for the Subscriber + receiving the call. Check that the page says **Online** and that the number's handler or + forwarding logic routes to that Subscriber. -- The address lists your Resource as its call handler. The Dashboard's **Addresses** page shows - every address with its handler; an unassigned phone number takes no action. -- For a phone number in a trial project, you're calling from a verified number. -- For Relay, the Relay Application's **Topic** matches your client's `contexts` value and the - client is running. -- For the Server SDK over HTTP, SignalWire could reach the URL and got a valid document. Check the call in - the Dashboard's **Logs**. -- For the browser, the token is a Subscriber token, not a guest or embed token, and the address - belongs to that same Subscriber. +
-## Know who's calling and follow the call +## Track the call's progress -Each approach gives you the caller's details and the call's progress in its own way. Follow the -section for the approach you used for your first call. +Read the caller's details, then follow what happens after the call arrives. Use HTTP callbacks +with SWML or event subscriptions with Relay and the Browser SDK. Follow the section for the +approach you used for your first call. ### Read the call details via SWML @@ -740,9 +713,39 @@ can decide the document per caller. `from` is a phone number, a SIP URI, or an a caller reached you. See the [webhook payload reference][swml-webhook-payload] for every field. The same fields are available inside the document as [variables][swml-variables], so a hosted -document can use them too. This document reads the caller's number back to them: +document can use them too. The SDK examples below build a document that reads the caller's +address back to them. Run a builder example to print the document, then return it from your +HTTP handler or paste it into a hosted SWML Script. YAML and JSON show the same instructions. + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as build_inbound.py and run: python build_inbound.py +import json +from signalwire import SWMLBuilder, SWMLService + +swml = ( + SWMLBuilder(SWMLService(name="inbound-example-1")) + .say("Thanks for calling from ${call.from}.") + .build() +) +print(json.dumps(swml, indent=2)) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as build-inbound.mjs, +// then run: node build-inbound.mjs +import { SwmlBuilder } from "@signalwire/sdk"; + +const swml = new SwmlBuilder() + .say("Thanks for calling from ${call.from}.") + .build(); +console.log(JSON.stringify(swml, null, 2)); +``` + ```yaml version: 1.0.0 @@ -766,6 +769,9 @@ sections: +Both hosted and HTTP-served SWML can fetch data with [`request`][swml-request] and branch on +the result with [`switch`][swml-switch], for example to look up the caller before routing them. + Individual methods report their own progress. Set `status_url` on [`record`][swml-record], [`connect`][swml-connect], or [`stream`][swml-stream] to receive HTTP callbacks as that step runs. The [webhooks guide][webhooks] covers endpoint setup and callback reliability. @@ -922,9 +928,25 @@ After the user answers, `status$` moves through `connecting` and `connected`, th ## Examples -Each example shows the SWML document, which your Server SDK app serves over HTTP or you host -serverless, and the Relay handler for the Server SDK over WebSocket. The last example answers in -the browser. +Keep the Resource and address from your first call. Change its behavior using one of the examples +below: + +- **SWML:** start with the Python or TypeScript builder example. Each runs on its own and prints + the document. Use its builder code in your HTTP handler, or paste the generated document into + your hosted script. YAML and JSON follow the SDK examples. +- **Relay:** replace your server handler with the example and keep the `inbound-calling` topic. +- **Browser SDK:** use [Answer in the browser](#answer-in-the-browser) for a complete page. + +| What you want to do | Example | +|---|---| +| Let an AI agent greet callers and answer questions | [Run an AI agent](#run-an-ai-agent) | +| Send callers to an agent or let them wait in a queue | [Route the call to a person or queue](#route-the-call-to-a-person-or-queue) | +| Route callers based on a keypad choice | [Build a phone menu](#build-a-phone-menu) | +| Connect the caller to a person and play that person a private introduction | [Forward the call with a whisper](#forward-the-call-with-a-whisper) | +| Save a message from the caller | [Take a voicemail](#take-a-voicemail) | +| Record a conversation | [Record the call](#record-the-call) | +| Send live audio to your service | [Stream the call audio](#stream-the-call-audio) | +| Let a user answer from your web app | [Answer in the browser](#answer-in-the-browser) | ### Run an AI agent @@ -941,6 +963,41 @@ Put an [`ai`][swml-ai] method in the document. SignalWire answers the call and h agent, which runs until the caller hangs up. + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as build_inbound.py and run: python build_inbound.py +import json +from signalwire import SWMLBuilder, SWMLService + +swml = ( + SWMLBuilder(SWMLService(name="inbound-example-2")) + .ai( + params={"static_greeting": "Hello, welcome to SignalWire! This call uses an artificial voice.", "static_greeting_no_barge": True}, + 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.", + ) + .build() +) +print(json.dumps(swml, indent=2)) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as build-inbound.mjs, +// then run: node build-inbound.mjs +import { SwmlBuilder } from "@signalwire/sdk"; + +const swml = new SwmlBuilder() + .ai({ + params: {"static_greeting": "Hello, welcome to SignalWire! This call uses an artificial voice.", "static_greeting_no_barge": true}, + // @ts-expect-error SDK 2.0.5 types omit SWML's { text } prompt form. + 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." }, + }) + .build(); +console.log(JSON.stringify(swml, null, 2)); +``` + ```yaml version: 1.0.0 @@ -1065,13 +1122,98 @@ Use [`prompt`][swml-prompt] to collect a digit and [`switch`][swml-switch] on `p pick the destination. See the [IVR guide][swml-ivr] for a fuller menu with speech input. + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as build_inbound.py and run: python build_inbound.py +import json +from signalwire import SWMLBuilder, SWMLService + +sales = ( + SWMLBuilder(SWMLService(name="inbound-example-3-sales")) + .connect( + to="", + ) + .build()["sections"]["main"] +) + +support = ( + SWMLBuilder(SWMLService(name="inbound-example-3-support")) + .connect( + to="", + ) + .build()["sections"]["main"] +) + +invalid = ( + SWMLBuilder(SWMLService(name="inbound-example-3-invalid")) + .say("Sorry, that is not a valid choice. Goodbye.") + .hangup() + .build()["sections"]["main"] +) + +swml = ( + SWMLBuilder(SWMLService(name="inbound-example-3")) + .prompt( + play=["say:Thanks for calling SignalWire. Press 1 for sales or 2 for support."], + max_digits=1, + ) + .switch( + variable="prompt_value", + case={"1": sales, "2": support}, + default=invalid, + ) + .build() +) +print(json.dumps(swml, indent=2)) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as build-inbound.mjs, +// then run: node build-inbound.mjs +import { SwmlBuilder } from "@signalwire/sdk"; + +const sales = new SwmlBuilder() + .connect({ + to: "", + }) + .document.sections.main; + +const support = new SwmlBuilder() + .connect({ + to: "", + }) + .document.sections.main; + +const invalid = new SwmlBuilder() + .say("Sorry, that is not a valid choice. Goodbye.") + .hangup() + .document.sections.main; + +const swml = new SwmlBuilder() + .prompt({ + play: ["say:Thanks for calling SignalWire. Press 1 for sales or 2 for support."], + max_digits: 1, + }) + .switch({ + variable: "prompt_value", + case: {"1": sales, "2": support}, + default: invalid, + }) + .build(); +console.log(JSON.stringify(swml, null, 2)); +``` + ```yaml version: 1.0.0 sections: main: - prompt: - play: 'say:Thanks for calling SignalWire. Press 1 for sales or 2 for support.' + play: + - 'say:Thanks for calling SignalWire. Press 1 for sales or 2 for support.' max_digits: 1 - switch: variable: prompt_value @@ -1096,7 +1238,7 @@ sections: "main": [ { "prompt": { - "play": "say:Thanks for calling SignalWire. Press 1 for sales or 2 for support.", + "play": ["say:Thanks for calling SignalWire. Press 1 for sales or 2 for support."], "max_digits": 1 } }, @@ -1213,6 +1355,103 @@ await client.run(); +### Route the call to a person or queue + +Your company number can stay assigned to a SWML Script or Relay Application while an agent +answers the forwarded call. Choose the destination based on where the agent works: + +| Where the agent answers | How to route the call | +|---|---| +| Phone | Use SWML `connect` with the agent's phone number, or Relay `connect()` with a phone device. | +| SIP device or PBX | Use SWML `connect` with a SIP URI, or Relay `connect()` with a SIP device. | +| Your web app | Use SWML `connect` with the agent's Subscriber address. The agent's Browser SDK client receives the call. | + +For example, this document answers the company call, plays a greeting, and connects the caller +to an agent. Host it in your Space or return it from your HTTP server. Replace +`` with a phone number in E.164 format, a SIP URI, or a Subscriber +address such as `/private/support-agent`. + + + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as build_inbound.py and run: python build_inbound.py +import json +from signalwire import SWMLBuilder, SWMLService + +swml = ( + SWMLBuilder(SWMLService(name="inbound-example-4")) + .answer() + .say("Thanks for calling. Connecting you to our team.") + .connect( + to="", + ) + .build() +) +print(json.dumps(swml, indent=2)) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as build-inbound.mjs, +// then run: node build-inbound.mjs +import { SwmlBuilder } from "@signalwire/sdk"; + +const swml = new SwmlBuilder() + .answer() + .say("Thanks for calling. Connecting you to our team.") + .connect({ + to: "", + }) + .build(); +console.log(JSON.stringify(swml, null, 2)); +``` + + +```yaml +version: 1.0.0 +sections: + main: + - answer: {} + - play: + url: 'say:Thanks for calling. Connecting you to our team.' + - connect: + to: '' +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { + "answer": {} + }, + { + "play": { + "url": "say:Thanks for calling. Connecting you to our team." + } + }, + { + "connect": { + "to": "" + } + } + ] + } +} +``` + + + +To let callers wait for an agent, use [`enter_queue`][swml-enter-queue] in the caller's SWML +document. The agent's call uses [`connect`][swml-connect] with `to: "queue:support"` to take a +caller from the `support` queue. Both queue operations require `transfer_after_bridge` to +specify the SWML to run after the conversation ends. See the method references for complete +queue examples. + ### Forward the call with a whisper Ring ``, play a private message to whoever answers, then connect them @@ -1224,6 +1463,54 @@ Use `connect.confirm` to play the [whisper][swml-call-whisper] to the agent befo two legs. + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as build_inbound.py and run: python build_inbound.py +import json +from signalwire import SWMLBuilder, SWMLService + +whisper = ( + SWMLBuilder(SWMLService(name="inbound-example-5-whisper")) + .say("You are about to be connected to a caller from ${call.from}.") + .build()["sections"]["main"] +) + +swml = ( + SWMLBuilder(SWMLService(name="inbound-example-5")) + .say("Thanks for calling SignalWire. Connecting you now.") + .connect( + to="", + confirm=whisper, + confirm_timeout=20, + ) + .build() +) +print(json.dumps(swml, indent=2)) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as build-inbound.mjs, +// then run: node build-inbound.mjs +import { SwmlBuilder } from "@signalwire/sdk"; + +const whisper = new SwmlBuilder() + .say("You are about to be connected to a caller from ${call.from}.") + .document.sections.main; + +const swml = new SwmlBuilder() + .say("Thanks for calling SignalWire. Connecting you now.") + .connect({ + to: "", + confirm: whisper, + confirm_timeout: 20, + }) + .build(); +console.log(JSON.stringify(swml, null, 2)); +``` + ```yaml version: 1.0.0 @@ -1347,6 +1634,52 @@ SignalWire posts the result to `status_url`, and `record_url` holds the recordin the rest of the document. + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as build_inbound.py and run: python build_inbound.py +import json +from signalwire import SWMLBuilder, SWMLService + +swml = ( + SWMLBuilder(SWMLService(name="inbound-example-6")) + .say("Thanks for calling SignalWire. Leave a message after the beep, then press pound.") + .record( + format="mp3", + beep=True, + end_silence_timeout=3, + terminators="#", + status_url="", + ) + .say("Thanks, goodbye.") + .hangup() + .build() +) +print(json.dumps(swml, indent=2)) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as build-inbound.mjs, +// then run: node build-inbound.mjs +import { SwmlBuilder } from "@signalwire/sdk"; + +const swml = new SwmlBuilder() + .say("Thanks for calling SignalWire. Leave a message after the beep, then press pound.") + .record({ + format: "mp3", + beep: true, + end_silence_timeout: 3, + terminators: "#", + status_url: "", + }) + .say("Thanks, goodbye.") + .hangup() + .build(); +console.log(JSON.stringify(swml, null, 2)); +``` + ```yaml version: 1.0.0 @@ -1486,6 +1819,52 @@ while the two legs talk, and SignalWire posts the result to `status_url` when th finishes. + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as build_inbound.py and run: python build_inbound.py +import json +from signalwire import SWMLBuilder, SWMLService + +swml = ( + SWMLBuilder(SWMLService(name="inbound-example-7")) + .say("This call may be recorded. Connecting you now.") + .record_call( + format="mp3", + direction="both", + stereo=True, + status_url="", + ) + .connect( + to="", + ) + .build() +) +print(json.dumps(swml, indent=2)) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as build-inbound.mjs, +// then run: node build-inbound.mjs +import { SwmlBuilder } from "@signalwire/sdk"; + +const swml = new SwmlBuilder() + .say("This call may be recorded. Connecting you now.") + .record_call({ + format: "mp3", + direction: "both", + stereo: true, + status_url: "", + }) + .connect({ + to: "", + }) + .build(); +console.log(JSON.stringify(swml, null, 2)); +``` + ```yaml version: 1.0.0 @@ -1623,6 +2002,48 @@ The stream runs for as long as the rest of the document does, so follow it with that hold the call, such as `ai` or `connect`. + +```python +# Install: python -m pip install signalwire-sdk==3.4.1 +# Save as build_inbound.py and run: python build_inbound.py +import json +from signalwire import SWMLBuilder, SWMLService + +# signalwire-sdk==3.4.1 has no stream() builder method. +builder = SWMLBuilder(SWMLService(name="inbound-example-8", schema_validation=False)) +builder.service.add_verb("stream", {"url": "", "track": "both_tracks", "codec": "PCMU", "status_url": ""}) +swml = ( + builder + .say("Thanks for calling SignalWire. Connecting you now.") + .connect( + to="", + ) + .build() +) +print(json.dumps(swml, indent=2)) +``` + + +```typescript +// Install: npm install @signalwire/sdk@2.0.5 +// This sample also runs as JavaScript: save as build-inbound.mjs, +// then run: node build-inbound.mjs +import { SwmlBuilder } from "@signalwire/sdk"; + +// @signalwire/sdk@2.0.5 has no stream() builder method. +const builder = new SwmlBuilder(); +builder.setValidation(false); +builder.addVerb("stream", {"url": "", "track": "both_tracks", "codec": "PCMU", "status_url": ""}); +builder.setValidation(true); +const swml = builder + .say("Thanks for calling SignalWire. Connecting you now.") + .connect({ + to: "", + }) + .build(); +console.log(JSON.stringify(swml, null, 2)); +``` + ```yaml version: 1.0.0 @@ -1743,7 +2164,9 @@ await client.run(); ### Answer in the browser -Receive the call on a web page as the Subscriber the number is assigned to. The page fetches a +Receive the call on a web page as the agent's Subscriber. The company number can ring this +Subscriber directly or use a SWML handler that connects to the Subscriber's address, as shown +in [Route the call to a person or queue](#route-the-call-to-a-person-or-queue). The page fetches a [Subscriber token][subscriber-token] from your backend, comes online, and shows a ringing state until the user answers or declines. Serve the page over HTTPS or `localhost` so the browser can access the microphone. @@ -1852,6 +2275,9 @@ comeOnline().catch((error) => { +Once connected, the user can also [transfer the call][browser-transfer] to a SIP URI or another +Resource address if the call has the `transfer` capability. + #### Choose what the user sends back `answer()` takes the same `audio` and `video` options as an outbound `dial()`. With no options @@ -1892,9 +2318,9 @@ Joins on camera with the microphone muted, for a kiosk or a viewer who watches w To pin the microphone, camera, or speaker across every call, use the [device management APIs][browser-device-management]. -If the page comes online but never rings, either the address belongs to a different Subscriber -than the one the token was issued for, or the token is a guest or embed token, which registers but -never receives calls. If `register()` rejects, the credentials themselves were refused. See the +If the page comes online but never rings, check that the number's handler or forwarding logic +reaches the Subscriber the token was issued for. Guest and embed tokens never receive calls. +If `register()` rejects, the credentials themselves were refused. See the [authentication guide][browser-auth] for the token lifecycle. `hangup()` ends the call for everyone. To hand the caller to another destination and drop out diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx index 2b9a2e4122..47b64c430c 100644 --- a/fern/products/platform/pages/calling/voice/outbound-calling.mdx +++ b/fern/products/platform/pages/calling/voice/outbound-calling.mdx @@ -66,26 +66,17 @@ 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. +Choose where the call starts and how your application follows it. Server call logic and browser +participation can work together in the same flow. -| What you want to do | Where to start | -|---|---| -| Give SignalWire call instructions over HTTP, supplied inline or returned by your webhook | [REST Calling API](#place-the-call), using a Server SDK or cURL | -| 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 | | | | +| Approach | Useful when | How you control and follow the call | +|---|---|---| +| [Server SDK — REST](#place-the-call) | A backend request or background job starts a call | Send a request with SWML instructions, receive progress at a webhook, and use the call ID for later commands. | +| [Server SDK — WebSocket (Relay)](#place-the-call) | Your server makes decisions as call events arrive | Keep a Relay client running to receive events and send commands over its persistent connection. | +| [Browser SDK](#place-the-call) | A user calls a person or Resource from your web app | Use the browser client for dialing, microphone and camera access, in-call controls, and live status updates in the page. | -SWML doesn't place calls. It's the script the call runs once it connects, so you place the call -with REST or Relay and pass SWML in the `swml` field. +For the REST example, the SDK sends SWML instructions that SignalWire runs when the destination +answers. The cURL alternative sends the same request directly over HTTP. ### Set your credentials and caller ID @@ -117,8 +108,9 @@ For this walkthrough, choose a device you can answer and replace ### Place the call -Use the REST Calling API with a SignalWire Server SDK or any server-side HTTP client. For -WebSocket calling, use a Server SDK or the Browser SDK. +Use a SignalWire Server SDK to call the REST Calling API. The examples show Python and +TypeScript first, followed by cURL for direct HTTP access. For WebSocket calling, use a +Server SDK or the Browser SDK. @@ -693,7 +685,8 @@ const client = new RestClient({ const swml = new SwmlBuilder() .ai({ - 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.", + // @ts-expect-error SDK 2.0.5 types omit SWML's { text } prompt form. + 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, welcome to SignalWire! This call uses an artificial voice.", static_greeting_no_barge: true, @@ -846,12 +839,22 @@ Follow the consent and calling-hour requirements in the [TCPA guide][tcpa]. #### Leave a voicemail via REST Use [`detect_machine`][swml-detect-machine] and `switch` to choose the live or voicemail message. +Build each message with `say()`, then pass its `main` section to `switch`, which takes lists +of instructions for its branches. ```python -voicemail = "say:Hello, welcome to SignalWire! Visit signalwire.com to learn more." -live = "say:Hello, welcome to SignalWire!" +voicemail = ( + SWMLBuilder(SWMLService(name="voicemail-message")) + .say("Hello, welcome to SignalWire! Visit signalwire.com to learn more.") + .build()["sections"]["main"] +) +live = ( + SWMLBuilder(SWMLService(name="live-message")) + .say("Hello, welcome to SignalWire!") + .build()["sections"]["main"] +) swml = ( SWMLBuilder(SWMLService(name="outbound-voicemail")) @@ -863,14 +866,10 @@ swml = ( .switch( variable="detect_result", case={ - "machine": [{"play": {"url": voicemail}}], - "human": [{"play": {"url": live}}], + "machine": voicemail, + "human": live, }, - default=[{ - "play": { - "url": "say:Hello, welcome to SignalWire!" - } - }], + default=live, ) .hangup() .build() @@ -886,8 +885,12 @@ print(call["id"]) ```typescript -const voicemail = "say:Hello, welcome to SignalWire! Visit signalwire.com to learn more."; -const live = "say:Hello, welcome to SignalWire!"; +const voicemail = new SwmlBuilder() + .say("Hello, welcome to SignalWire! Visit signalwire.com to learn more.") + .document.sections.main; +const live = new SwmlBuilder() + .say("Hello, welcome to SignalWire!") + .document.sections.main; const swml = new SwmlBuilder() .detect_machine({ @@ -898,14 +901,10 @@ const swml = new SwmlBuilder() .switch({ variable: "detect_result", case: { - machine: [{ play: { url: voicemail } }], - human: [{ play: { url: live } }], + machine: voicemail, + human: live, }, - default: [{ - play: { - url: "say:Hello, welcome to SignalWire!", - }, - }], + default: live, }) .hangup() .build(); @@ -1107,11 +1106,16 @@ Play a private message to ``, then connect that call to #### Play a whisper via REST Use `connect.confirm` to play the [whisper][call-whisper] to the agent before bridging the calls. +Build the whisper with `say()` and pass its `main` section as the confirmation instructions. ```python -whisper = "say:You are about to be connected to the caller." +whisper = ( + SWMLBuilder(SWMLService(name="whisper-message")) + .say("You are about to be connected to the caller.") + .build()["sections"]["main"] +) swml = ( SWMLBuilder(SWMLService(name="outbound-whisper")) @@ -1119,7 +1123,7 @@ swml = ( .connect(**{ "from": "", "to": "", - "confirm": [{"play": {"url": whisper}}], + "confirm": whisper, "confirm_timeout": 20, }) .build() @@ -1135,14 +1139,16 @@ print(call["id"]) ```typescript -const whisper = "say:You are about to be connected to the caller."; +const whisper = new SwmlBuilder() + .say("You are about to be connected to the caller.") + .document.sections.main; const swml = new SwmlBuilder() .say("Hello, welcome to SignalWire!") .connect({ from: "", to: "", - confirm: [{ play: { url: whisper } }], + confirm: whisper, confirm_timeout: 20, }) .build(); @@ -1498,7 +1504,8 @@ Start [`stream`][swml-stream] in the background and send status events to your w ```python -# `stream` is not yet in the builder's bundled schema, so add it as raw SWML. +# signalwire-sdk==3.4.1 has no stream() builder method in its bundled schema. +# Add this verb directly; use the builder for the remaining instructions. swml_builder = SWMLBuilder( SWMLService(name="outbound-stream", schema_validation=False) ) @@ -1524,7 +1531,8 @@ print(call["id"]) ```typescript -// `stream` is not yet in the builder's bundled schema, so add it as raw SWML. +// @signalwire/sdk@2.0.5 has no stream() builder method in its bundled schema. +// Add this verb directly; use the builder for the remaining instructions. const swmlBuilder = new SwmlBuilder(); swmlBuilder.setValidation(false); swmlBuilder.addVerb("stream", { From ecef728070afd001f67b1266b6daf9d95f5696b4 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Fri, 18 Sep 2026 13:19:28 -0400 Subject: [PATCH 12/14] docs(rules): require SDK-first examples and purposeful approach comparisons Codify two conventions the calling guides now follow. Examples lead with the supported SDK: Python, then TypeScript, then cURL as the direct HTTP alternative. SWML instructions go through the builders, with YAML and JSON after. Any SDK or version gap that forces a raw request gets explained where it appears. Compare approaches by purpose and control model rather than by check/cross scorecard. A missing direct SDK method is not the same as something the whole flow can't do, since a browser client can reach a Resource that does it. State real restrictions at the scope the evidence supports. Link the capability-guides rule from CONTRIBUTING so the template is discoverable alongside the other authoring rules. --- .claude/rules/mdx-capability-guides.md | 42 ++++++++++++++++++-------- .claude/rules/mdx-style.md | 2 ++ CONTRIBUTING.md | 1 + 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/.claude/rules/mdx-capability-guides.md b/.claude/rules/mdx-capability-guides.md index fa28bd8014..1b5dfafa52 100644 --- a/.claude/rules/mdx-capability-guides.md +++ b/.claude/rules/mdx-capability-guides.md @@ -9,14 +9,14 @@ Applies to a how-to that teaches one platform capability across more than one su ## Skeleton -Every guide answers the same questions in the same order: which product, how to start, how to do the thing, how to know it worked, where to go next. +Every guide answers the same questions in the same order: which approach fits, how to start, how to do the thing, how to know it worked, where to go next. ```text frontmatter: title = capability as a noun phrase ("Call streaming"); slug //; description = one sentence naming the first-run outcome, then each extension in page order [reference-style link definitions] intro, 2–3 sentences, no heading: what the reader does, then "Start by , then ." -## Pick the right product for capability matrix + one bullet per surface +## Choose an approach for task-based comparison; can be the first step below ## Prepare for ## How works (optional — see below) ## (only for a reader starting from zero — see below) @@ -24,6 +24,11 @@ intro, 2–3 sentences, no heading: what the reader does, then "Start by ### (optional — "Choose a destination", "Run a WebSocket server") ### ("Place the call", "Start the stream") + SDK examples first; cURL after the SDKs + Python — + TypeScript — + cURL — keep the direct HTTP example as an alternative + ### ("Answer the call", "Answer and speak") — the verification step ## one per task the guide must cover @@ -34,7 +39,7 @@ intro, 2–3 sentences, no heading: what the reader does, then "Start by ` after the prerequisites. See "Approach comparisons" below. - **Prepare.** "Have these values ready:" then bullets: Space URL, Project ID and API token with the **Voice** permission, a purchased number or verified caller ID, a destination the reader can answer, then topic-specific items. Hard constraints follow as titled ``s (trial and international limits, `wss://` only, TCPA). Add one pointer sentence when a reader might want a sibling guide instead. - **How it works.** Put it before the first run only when the reader can't interpret the result without the model (machine detection's outcome table). Otherwise conceptual depth goes after the first run, as its own `##` ("Handle the audio stream"). - **First run.** A `` walkthrough is for a reader starting from zero: a first call, a first stream. A guide that picks up mid-stream (recording assumes you can already place a call) skips `` and opens with its first task section. The walkthrough targets the smallest thing the reader can confirm alone, usually calling their own phone: one code block per surface, no options, no error handling. @@ -44,15 +49,27 @@ intro, 2–3 sentences, no heading: what the reader does, then "Start by `: one-sentence goal, a titled `` when compliance applies, then a `####` per surface. - **Next steps** is required here as on every guide (`mdx-style`). Cards deepen this capability only. -## Capability matrix +## Approach comparisons -One table under `## Pick the right product`: `| Function | SWML | Relay | REST Calling API | Browser SDK | Call Flow Builder |`, keeping every column where the capability plausibly lives and adding an API-area column (`Recordings API`) when a REST resource is part of the story. Rows are things the reader wants to do ("Get the recording's URL back in your own code, without a webhook"), not feature names. Three cell states: +Use one short table that helps the reader make a concrete choice. Default to one row per approach: -- `` shipped -- `` not available -- `` planned, not shipped — add the legend line under the table only when a cell uses it +```markdown +| Approach | Useful when | How it handles the task | +|---|---|---| +| Server SDK — REST | A backend request or job starts the operation | Send requests from your server and receive progress at a webhook. | +| Server SDK — Relay | Your server reacts as events arrive | Keep a client running to receive events and send commands over its connection. | +| Browser SDK | A user participates from your web app | Provide media, user controls, and live status updates in the page. | +``` + +Adapt the rows to the task and verify each claim. For inbound calling, compare the handler, how calls reach it, and where its logic runs. For a narrow task such as recording, columns can instead explain how each approach starts the operation and delivers its result. Include a distinction only when it helps the reader choose or implement a path. -Follow the table with one bullet per column: the surface's canonical name linked to its reference, and one sentence on what it is for. Agents is never its own column: it generates SWML, so it can express anything the SWML column can. +- Use descriptive text instead of red crosses, green checks, availability scores, or blanket "can/can't" cells. Different control models should read as choices, not a ranking of product power. +- Keep the comparison's scope explicit: a direct SDK method, a call handler, a transport, and the complete application are different things. The absence of a convenience method in one SDK does not establish that the task is impossible in a flow involving that SDK. +- Account for composition. A browser client, a Resource, and server call logic can participate in the same flow. When this matters to the choice, explain their roles briefly or link to the relevant flow. Do not label the options as mutually exclusive. +- Describe how the reader accomplishes the task through the supported approach. If an operation belongs to a Resource or server, say so when relevant rather than marking the browser as incapable. Verify that composed path before claiming it works. +- Keep real restrictions precise and visible: name the affected operation, destination, token type, or SDK version. Neutral wording must not hide unsupported behavior or present planned support as available. +- Keep workaround details out of the introductory comparison unless they are necessary for the reader's decision. Do not add a catalog of indirect paths merely to fill every cell. +- Link each approach to its example or reference. SDK builders are ways of authoring SWML, not separate call capabilities; compare hosting and delivery when those are the actual choices. ## Surface names @@ -60,18 +77,19 @@ Canonical names come from the glossary (`/docs/platform/glossary`); the ones thi - **Headings** name the surface in full: `### Stop recording the call via Relay`, `### Place the call via the REST Calling API`. Two shapes: ` via ` when the surface is a transport; ` from ` when it is a caller context ("Hold an AI chat conversation from a browser"). Every heading stands alone in a search result with no page title above it, so it carries the keywords: "Recording options: format, stereo, and direction", not "Recording options". - **Tab titles** are short: `REST`, `WebSocket (Relay)`. -- **Code block titles** are ``: `Python — Relay client`, `TypeScript — Relay client`, `cURL — REST Calling API`, `Python — Agents`, `TypeScript — Agents`, `Python — REST client`, `TypeScript — REST client`, `JavaScript — Browser SDK`, `YAML`, `JSON`. +- **Code block titles** are ``: `Python — Relay client`, `TypeScript — Relay client`, `cURL — REST Calling API`, `Python — SWML builder`, `TypeScript — SWML builder`, `Python — REST client`, `TypeScript — REST client`, `JavaScript — Browser SDK`, `YAML`, `JSON`. ## Surfaces: headings by default, tabs only inside Steps - A task shows each surface under its own `###`, never in tabs. Headings appear in the table of contents, in search, and in the Markdown export; tab panes hide from all three. - Inside ``, `###` is already the step heading, so a step that differs by surface uses `` with `` and ``. That is the only place tabs belong. Guides under one overview share a single `groupId` so the reader's choice follows them between pages; the voice guides use `outbound-api`. - **Relay and the REST Calling API are the same commands over two transports.** When only the transport differs (pausing a recording, stopping a stream), one `### via Relay` heading holds a single `` of `Python — Relay client`, `TypeScript — Relay client`, and `cURL — REST Calling API`. Split them into separate headings only when the flow genuinely differs: placing a call over HTTP with inline SWML is not the same flow as dialing over a live socket. -- **SWML is one surface, authored several ways and delivered several ways.** Under `### via SWML`, show the document once, in the forms a reader authors it, in one ``: `Python — Agents` and `TypeScript — Agents` (`AgentBase`) first, then `YAML`, then `JSON`. Don't encourage hand-writing SWML where the SDK covers the verb. How the document reaches the call (served from your server, stored as a hosted script resource, passed inline in a REST `dial`) is a delivery choice: cover it once per guide, in the first-run section, and don't repeat it under every task. A REST `dial` that carries inline SWML is SWML delivery, not the REST Calling API surface. -- Where the reference already documents a REST request, use `EndpointRequestSnippet` and `EndpointResponseSnippet` with a link to the reference page instead of hand-writing the request; describe only the fields the task needs. A first-run REST step that dials still ends with the "returns a call `id` and status `queued` … save the `id`" paragraph and ``. +- **SWML is one surface, authored several ways and delivered several ways.** Under `### via SWML`, show the document once, in the forms a reader authors it, in one ``: `Python — SWML builder` (`SWMLBuilder`) and `TypeScript — SWML builder` (`SwmlBuilder`) first, then `YAML`, then `JSON`. Use builder methods for supported verbs, including nested branches and confirmation instructions. Use `AgentBase` when the example needs its agent features. How the document reaches the call (served from your server, stored as a hosted script resource, passed inline in a REST `dial`) is a delivery choice: cover it once per guide, in the first-run section, and don't repeat it under every task. A REST `dial` that carries inline SWML is SWML delivery, not the REST Calling API surface. +- Where the reference already documents a REST request, place `EndpointRequestSnippet` and `EndpointResponseSnippet` with a link to the reference page after the SDK examples; describe only the fields the task needs. Keep an existing cURL alternative after the SDK blocks. A first-run REST step that dials still ends with the "returns a call `id` and status `queued` … save the `id`" paragraph and ``. ## Sample conventions +- **SDK examples first.** For every API operation covered by a supported SDK, lead the explanation with the SDK method and make its example the first visible option. In a shared ``, order Python SDK, TypeScript SDK, then cURL. Keep cURL as the direct HTTP alternative; do not remove it to make SDKs primary. Apply this to setup, resource creation, authentication, call control, and follow-up tasks as well as the first call. Link to the SDK reference before the underlying REST endpoint. If no supported SDK method exists for the documented version, explain the gap and use direct HTTP for that operation. A hosted-script-only step can show YAML and JSON directly because the reader pastes a document into the Dashboard. - **Complete programs.** Every sample under `` and `## Examples` runs as pasted: imports, client setup, the call, and the wait or disconnect. A block under a task section may show only the changed lines, with line highlights (`{6-8}`), when its paragraph names the sample it extends. - **Header comments** on every complete sample. Python: `# Install: python -m pip install signalwire-sdk==3.4.1` then `# Save as .py and run: python .py`. TypeScript with no type annotations: `// Install: npm install @signalwire/sdk@2.0.5` then `// This sample also runs as JavaScript: save as .mjs,` / `// then run: node .mjs`. TypeScript that uses types: `// Save as .mts and run: npx tsx .mts`. Browser: `// Install: npm install @signalwire/js@4.0.0-rc.2 rxjs@7.8.2` plus the HTML elements the script expects, as comments. - **Pinned versions** are literal and identical across every guide: `signalwire-sdk==3.4.1`, `@signalwire/sdk@2.0.5`, `@signalwire/js@4.0.0-rc.2 rxjs@7.8.2`. Bump them in every guide in one PR, never one page at a time. diff --git a/.claude/rules/mdx-style.md b/.claude/rules/mdx-style.md index e38055529d..78b008c3e6 100644 --- a/.claude/rules/mdx-style.md +++ b/.claude/rules/mdx-style.md @@ -49,6 +49,8 @@ Tutorials and how-tos give the reader a way to confirm the result — a command - Customer-facing only: no internal implementation detail (backend endpoints, transports, engine-side params). Placeholder data only — no real names, emails, or domains. - Examples do real work: tool call → your server → system of record → response. No knowledge-trivia bots. - Examples run as written: real imports, every required parameter, and only placeholders the reader can obviously fill. Verify each against the spec or SDK source — and against an actual run when the page ships a whole flow. +- In tutorials and how-to guides, explain API usage through the supported SDK first. Show Python and TypeScript SDK examples before cURL, and retain cURL as the direct HTTP alternative. Use SWML builders for supported instructions; put equivalent YAML and JSON after the SDK examples. Explain any SDK/version gap that requires a raw request or verb. See `mdx-capability-guides` for the example ordering template. +- Compare approaches by their purpose and control model, using descriptive text. Distinguish a missing direct SDK method from what a complete flow can accomplish with Resources and server logic. Avoid check/cross scorecards that imply one approach is less capable overall; state actual restrictions at the scope the evidence supports. - A cluster of guides shares one running example, so cross-links compound instead of restarting context. The AI guides use Bayview Taxi and its dispatcher Ada across `platform/pages/ai/overview.mdx` and `platform/pages/ai/guides/**`, and the calling guides under `platform/pages/calling/voice/` borrow the same cast whenever a scenario has a business voice; adopt a cluster's existing cast when you add a page to it, and introduce a new one only for a new cluster. [SW] - Don't cite other vendors' docs as authority; never disparage anyone's product. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87269d72dc..0af1a91be9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,6 +68,7 @@ and the strict MDX v3 syntax rules are all documented in the repo's authoring gu - [`.claude/rules/mdx-mechanics.md`](.claude/rules/mdx-mechanics.md) — frontmatter, structure, links, the LLM view, and MDX rules that break the build. - [`.claude/rules/mdx-components.md`](.claude/rules/mdx-components.md) — the component catalog (house set, custom components, what not to use). - [`.claude/rules/mdx-reference-pages.md`](.claude/rules/mdx-reference-pages.md) — reference-page patterns (`ParamField`, section names, examples). +- [`.claude/rules/mdx-capability-guides.md`](.claude/rules/mdx-capability-guides.md) — guide template and SDK-first examples, with cURL after the SDKs. - [`.claude/rules/docs-conventions.md`](.claude/rules/docs-conventions.md) — which docs are hand-authored vs. generated. These are the single source of truth (and double as the guardrails your AI editor loads automatically), so follow them rather than any summary here. From a73fd42a12d6c24f58001a41061ed05a2b654c09 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Fri, 18 Sep 2026 13:19:28 -0400 Subject: [PATCH 13/14] docs(platform): move architecture pages under Core and reorder Core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flatten the Architecture section's pages directly into Platform > Core, and lead Core with Projects, Resources, Addresses, Subscribers, and Webhooks — the concepts the calling guides assume a reader already has. All 11 pages are preserved, each appearing exactly once. --- fern/products/platform/platform.yml | 30 +++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/fern/products/platform/platform.yml b/fern/products/platform/platform.yml index 85634a775b..7f839000c6 100644 --- a/fern/products/platform/platform.yml +++ b/fern/products/platform/platform.yml @@ -32,12 +32,30 @@ navigation: - folder: ./pages/platform/setup title: Setup title-source: frontmatter - - folder: ./pages/platform/core - title: Core - title-source: frontmatter - - folder: ./pages/platform/call-fabric - title: Architecture - title-source: frontmatter + - section: Core + contents: + - page: Projects + path: ./pages/platform/core/projects.mdx + - page: Resources + path: ./pages/platform/call-fabric/resources.mdx + - page: Addresses + path: ./pages/platform/call-fabric/addresses.mdx + - page: Subscribers + path: ./pages/platform/call-fabric/subscribers.mdx + - page: Webhooks + path: ./pages/platform/core/webhooks/index.mdx + - page: Allow SignalWire IPs through your firewall + path: ./pages/platform/core/allowing-signalwire-ips-through-your-firewall.mdx + - page: Default limits + path: ./pages/platform/core/rate-limits.mdx + - page: Glossary + path: ./pages/platform/core/glossary.mdx + - page: International support + path: ./pages/platform/core/how-to-enable-international-outbound-dialing-sms.mdx + - page: Media URL protection + path: ./pages/platform/core/media-protection.mdx + - page: Segment IDs + path: ./pages/platform/core/segment-ids.mdx - folder: ./pages/platform/phone-numbers title: Phone numbers title-source: frontmatter From 8475fb775819e6b414b5be71aa4b20b581ada937 Mon Sep 17 00:00:00 2001 From: Devon-White Date: Fri, 18 Sep 2026 13:19:33 -0400 Subject: [PATCH 14/14] docs(swml): retire the make-and-receive-calls guide for the calling guides The combined SWML walkthrough is now covered by the platform inbound and outbound calling guides, which carry the same setup in more depth. Delete the page, redirect its URL to the inbound guide, and resolve all seven references: the SWML recipes and the phone numbers overview point at the inbound sections that replace them, and the voice overview drops the duplicate card, since both new guides are already listed there. --- fern/docs.yml | 4 + .../platform/pages/calling/voice/overview.mdx | 7 +- .../pages/platform/phone-numbers/overview.mdx | 2 +- .../guides/recipes/creating-ivr-with-swml.mdx | 4 +- .../pages/guides/recipes/forwarding-calls.mdx | 9 +- .../recipes/making-and-receiving-calls.mdx | 184 ------------------ .../pages/guides/recipes/recording-calls.mdx | 9 +- 7 files changed, 18 insertions(+), 201 deletions(-) delete mode 100644 fern/products/swml/pages/guides/recipes/making-and-receiving-calls.mdx diff --git a/fern/docs.yml b/fern/docs.yml index 8b2295dbda..a180a5c641 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -196,6 +196,10 @@ css: - components/sigmond-card/styles.css redirects: + # The combined SWML calling walkthrough is now covered by the platform + # inbound and outbound guides. Default old links to inbound handler setup. + - source: /docs/swml/guides/make-and-receive-calls + destination: /docs/platform/voice/inbound-calling # Browser SDK outbound guide consolidated into the platform outbound calling # guide, which now carries the browser walkthrough and media options. - source: /docs/browser-sdk/guides/outbound-calls diff --git a/fern/products/platform/pages/calling/voice/overview.mdx b/fern/products/platform/pages/calling/voice/overview.mdx index ca8aa7e74e..3a6fd8a9bc 100644 --- a/fern/products/platform/pages/calling/voice/overview.mdx +++ b/fern/products/platform/pages/calling/voice/overview.mdx @@ -18,9 +18,6 @@ Whether building a UCaaS solution, modernizing a legacy IVR, augmenting CX with Build one agent, call it over a phone number, and send it a text turn through the AI Chat API - - The fundamentals of your first calling app - Dial from your backend or the browser, and choose what runs when someone answers @@ -75,8 +72,8 @@ SignalWire's advanced APIs and elastic cloud infrastructure make it a breeze to Route SIP traffic through the SignalWire platform to your PBX system. - - Guide that focuses on how to make and receive phone calls via a SWML script. + + Create a hosted or server-served SWML handler and route incoming calls to it. Take calls placed from WhatsApp on your business number and route them like any other call. diff --git a/fern/products/platform/pages/platform/phone-numbers/overview.mdx b/fern/products/platform/pages/platform/phone-numbers/overview.mdx index edf18a229d..27ceb642ab 100644 --- a/fern/products/platform/pages/platform/phone-numbers/overview.mdx +++ b/fern/products/platform/pages/platform/phone-numbers/overview.mdx @@ -68,7 +68,7 @@ The "⋯" menu has an Edit link to the same page to change the actions taken whe - [Webhooks](/docs/platform/webhooks) - [SWML Reference](/docs/swml/) -- [Making and Receiving Phone Calls](/docs/swml/guides/make-and-receive-calls) +- [Inbound calling](/docs/platform/voice/inbound-calling#give-the-resource-an-address) ## Verified diff --git a/fern/products/swml/pages/guides/recipes/creating-ivr-with-swml.mdx b/fern/products/swml/pages/guides/recipes/creating-ivr-with-swml.mdx index 7de02a22ce..55d4cec705 100644 --- a/fern/products/swml/pages/guides/recipes/creating-ivr-with-swml.mdx +++ b/fern/products/swml/pages/guides/recipes/creating-ivr-with-swml.mdx @@ -16,8 +16,8 @@ SWML enables you to create Relay applications using a descriptive format without ## Getting started -Before proceeding ahead with this tutorial, you should be familiar with the [Making and Receiving Phone Calls](/docs/swml/guides/make-and-receive-calls#swml) guide. -This resource will offer you step-by-step instructions on how to create a SWML script directly on your [SignalWire SWML Dashboard](https://my.signalwire.com?page=relay-bins). +Start with the [inbound calling guide](/docs/platform/voice/inbound-calling#create-your-call-handler) +to create a hosted SWML Script and assign it to a phone number. Use that setup for the menu below. The first crucial step is to have a SignalWire Space. Begin by [signing up now to get started](https://signalwire.com/signups/new). diff --git a/fern/products/swml/pages/guides/recipes/forwarding-calls.mdx b/fern/products/swml/pages/guides/recipes/forwarding-calls.mdx index aa6250ea62..e0683672b7 100644 --- a/fern/products/swml/pages/guides/recipes/forwarding-calls.mdx +++ b/fern/products/swml/pages/guides/recipes/forwarding-calls.mdx @@ -5,11 +5,10 @@ description: Use SWML to forward calls max-toc-depth: 3 --- -In [Making and receiving phone calls](/docs/swml/guides/make-and-receive-calls) we learned how to use SWML -to define what should happen when a call is received by one of your -SignalWire numbers. In that case, we just played some audio. In this article, -instead, we will use the same technique to forward the call to a different -number. +Use a hosted SWML Script to forward incoming calls to another number. Start with the +[inbound calling guide](/docs/platform/voice/inbound-calling#create-your-call-handler) to create +the script and assign it to your SignalWire number, then replace its greeting with forwarding +instructions. ## SWML for call forwarding diff --git a/fern/products/swml/pages/guides/recipes/making-and-receiving-calls.mdx b/fern/products/swml/pages/guides/recipes/making-and-receiving-calls.mdx deleted file mode 100644 index 6de67d04bd..0000000000 --- a/fern/products/swml/pages/guides/recipes/making-and-receiving-calls.mdx +++ /dev/null @@ -1,184 +0,0 @@ ---- -title: Making and receiving phone calls -slug: guides/make-and-receive-calls -description: Overview of the many ways you can make and receive calls using SignalWire products. -max-toc-depth: 3 ---- - -{/* Links */} -[swml]: /docs/swml "SignalWire Markup Language" -[sw-api]: /docs/apis "SignalWire API" - -Follow this guide to make and receive your first phone calls using -[SignalWire Markup Language (SWML)][swml] and -the [SignalWire API][sw-api]. - -## Prerequisites - -To be able to make calls, you will need: - -- a SignalWire phone number -- your API credentials (Space URL, Project ID, and API token) - -Also, note that if your account is in trial mode, -you'll need to upgrade it in order to call numbers other than your own verified numbers. - -To acquire a phone number, -[log in](https://signalwire.com/signin) to your SignalWire Space. -From the [Phone Numbers section](https://my.signalwire.com/?phone_numbers), -[buy a new phone number](/docs/platform/phone-numbers). -You will need at least one number to make and receive calls. - -Find your API tokens in the API section of your Space, as shown below. -You may need to create a new token if you never used one. -Make sure that your token has at least the "Voice" scope enabled. - - - - - -![The API page.](/assets/images/dashboard/credentials/api-credentials.webp) - - - - - - - -Open **API Credentials** in the Dashboard and copy the **Project ID** and an API token with the **Voice** scope. If necessary, select **+ New** to create a token. - - - - - -## SWML - -### Receiving incoming calls - -To handle incoming calls we need to configure the phone number in our -[SignalWire Space](https://signalwire.com/signin) to answer calls using a SWML Script. - - - -```yaml -version: 1.0.0 -sections: - main: - - play: say:Hello from SignalWire! -``` - -Take note of the Request URL for the SWML Script that we created. You can even create a new script if you want to define a different behavior for incoming calls. - -To configure your number to handle incoming calls with an SWML Script, click the [Phone Numbers](https://my.signalwire.com/?phone_numbers) section within your SignalWire Space, and edit the settings of the specific number you would like to use to answer calls. -Set **Handle calls using** to **a SWML Script**, then select your SWML Script from the dropdown - -That's it! Inbound calls to this SignalWire number will execute the specified SWML Script. - -### Making your first call - -Outbound calls can be made via the SignalWire REST API's [Create a Call endpoint](/docs/apis/rest/calls/call-commands). -This is accomplished by sending a POST request using SWML in the request body to handle the call. -The SWML can be served via URL or passed inline directly as an escaped JSON string. - - - - -```bash -curl -L -g 'https://Your_Space_Name.signalwire.com/api/calling/calls' \ --H 'Content-Type: application/json' \ --H 'Accept: application/json' \ --H "Authorization: Basic $(echo -n "${YOUR-PROJECT-ID}:${YOUR-API-TOKEN}" | base64)" \ ---data-raw '{ - "command": "dial", - "params": { - "from": "sip:from-sip@example-112233445566.sip.signalwire.com", - "to": "+1xxxxxxxxxx", - "caller_id": "+1234567890", - "fallback_url": "https://example.com/fallback", - "status_url": "https://example.com/status_callback", - "status_events": [ - "answered", - "ended" - ], - "url": "https://example.com/swml" - } -}' -``` - - - - -```bash -curl -L -g 'https://Your_Space_Name.signalwire.com/api/calling/calls' \ --H 'Content-Type: application/json' \ --H 'Accept: application/json' \ --H "Authorization: Basic $(echo -n "${YOUR-PROJECT-ID}:${YOUR-API-TOKEN}" | base64)" \ ---data-raw '{ - "command": "dial", - "params": { - "from": "sip:from-sip@example-112233445566.sip.signalwire.com", - "to": "+1xxxxxxxxxx", - "caller_id": "+1234567890", - "fallback_url": "https://example.com/fallback", - "status_url": "https://example.com/status_callback", - "status_events": [ - "answered", - "ended" - ] - }, - "swml": { - "version": "1.0.0", - "sections": { - "main": [ - { - "play": "say:Hello from SignalWire!" - } - ] - } - } -}' -``` - - - - - - -To pass SWML as an inline string, quotes must be escaped, as shown in the example below. - - - -```bash -curl -L -g 'https://Your_Space_Name.signalwire.com/api/calling/calls' \ --H 'Content-Type: application/json' \ --H 'Accept: application/json' \ --H "Authorization: Basic $(echo -n "${YOUR-PROJECT-ID}:${YOUR-API-TOKEN}" | base64)" \ ---data-raw '{ - "command": "dial", - "params": { - "from": "sip:from-sip@example-112233445566.sip.signalwire.com", - "to": "+1xxxxxxxxxx", - "caller_id": "+1234567890", - "fallback_url": "https://example.com/fallback", - "status_url": "https://example.com/status_callback", - "status_events": [ - "answered", - "ended" - ], - "swml": "{\"version\":\"1.0.0\",\"sections\":{\"main\":[{\"play\":\"say:Hello from SignalWire!\"}]}}" - } -}' -``` - - - - ---- - -## Next steps - -SWML scripts allow you to handle incoming phone calls, -and REST APIs make it easy to trigger outbound calls. - -If you need more flexibility and real-time control on your calls, -check out the [SDKs](/docs/server-sdks) for programmatic call handling. diff --git a/fern/products/swml/pages/guides/recipes/recording-calls.mdx b/fern/products/swml/pages/guides/recipes/recording-calls.mdx index fc42f4ed33..f04eccd59b 100644 --- a/fern/products/swml/pages/guides/recipes/recording-calls.mdx +++ b/fern/products/swml/pages/guides/recipes/recording-calls.mdx @@ -5,9 +5,9 @@ description: Using SWML to record ongoing calls. max-toc-depth: 3 --- -In [Making and receiving phone calls](/docs/swml/guides/make-and-receive-calls) we learned how to use SWML Scripts to define what should happen when a call is received by one of your -SignalWire numbers. In that case, we just played some audio. In this article, -instead, we will use the same technique to record the audio of the call. +Use a hosted SWML Script to record incoming calls. Start with the +[inbound calling guide](/docs/platform/voice/inbound-calling#create-your-call-handler) to create +the script and assign it to your SignalWire number, then add the recording instructions below. ## Recording calls with SignalWire @@ -62,7 +62,8 @@ In the Dashboard, open **Phone Numbers** > **Purchased**, select the number, and -Refer to [Making and receiving phone calls](/docs/swml/guides/make-and-receive-calls#receiving-incoming-calls) for more information about this step. +Follow [Give the Resource an address](/docs/platform/voice/inbound-calling#give-the-resource-an-address) +for the assignment steps. ## Conclusion