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.
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 @@
+
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/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/llms.txt b/fern/llms.txt
index 098660c7f0..a6fb4e4bcd 100644
--- a/fern/llms.txt
+++ b/fern/llms.txt
@@ -98,7 +98,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 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
new file mode 100644
index 0000000000..7b4f76801e
--- /dev/null
+++ b/fern/products/platform/pages/calling/voice/inbound-calling.mdx
@@ -0,0 +1,2329 @@
+---
+title: Inbound calling
+slug: voice/inbound-calling
+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
+---
+
+[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
+[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-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
+[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
+[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
+[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
+[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 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:
+
+- 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
+verified. Verify the phone you'll call from, or upgrade the project, before you dial.
+
+
+## Answer your first call
+
+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 to handle your call
+
+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.
+
+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 |
+|---|---|---|
+| [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()`.
+
+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 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, 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
+
+Open the tab for your approach. Complete its setup, then continue to **Give the Resource an address**.
+
+
+
+
+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 SWMLBuilder, SWMLService
+
+service = SWMLService(
+ name="inbound-call",
+ route="/swml",
+ port=3000,
+ basic_auth=("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,
+// 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);
+```
+
+
+
+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.
+
+Once your server is reachable, create its Resource in the Dashboard:
+
+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.
+
+
+
+
+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.
+
+
+
+```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();
+```
+
+
+
+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.
+
+
+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
+// 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);
+};
+```
+
+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.
+
+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 "Hello, welcome to SignalWire!" and ends the call:
+
+
+
+```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!" } }
+ ]
+ }
+}
+```
+
+
+
+
+
+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.
+
+
+
+```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",
+)
+
+swml = (
+ SWMLBuilder(SWMLService(name="inbound-welcome"))
+ .say("Hello, welcome to SignalWire!")
+ .build()
+)
+
+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";
+
+const client = new RestClient({
+ project: "",
+ token: "",
+ host: ".signalwire.com",
+});
+
+const swml = new SwmlBuilder()
+ .say("Hello, welcome to SignalWire!")
+ .build();
+
+const script = await client.fabric.swmlScripts.create({
+ name: "Inbound welcome",
+ contents: JSON.stringify(swml),
+});
+console.log(script.id);
+```
+
+
+```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!" } }
+ ]
+ }
+ }
+ }'
+```
+
+
+
+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
+
+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.
+
+
+
+
+
+
+
+
+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 an HTTP SWML handler, use [`set_swml_webhook`][py-set-swml-webhook] in Python or
+[`setSwmlWebhook`][ts-set-swml-webhook] in TypeScript.
+
+
+
+```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_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
+# 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_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({
+ project: "",
+ token: "",
+ host: ".signalwire.com",
+});
+
+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.
+
+
+
+
+
+
+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`.
+
+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`.
+
+
+
+
+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`.
+
+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.
+
+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
+
+Call the address you assigned:
+
+| 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. |
+
+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.
+
+
+
+Check assignment and execution separately. A number can have the right Resource assigned while
+its HTTP server, Relay client, or browser is offline.
+
+- 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.
+
+
+
+
+
+## Track the call's progress
+
+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
+
+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
+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
+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}." } }
+ ]
+ }
+}
+```
+
+
+
+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.
+
+This flow shows an inbound call handled by a SWML Script that fetches the document from your
+server:
+
+
+
+
+
+
+
+
+
+```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. The sample extends the first-run Relay handler; the highlighted lines are new.
+
+
+
+```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
+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 {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
+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:
+
+
+
+
+
+
+
+
+
+```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
+
+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
+
+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.
+
+
+
+```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
+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 in E.164 format.
+SWML `connect` also accepts SIP URIs and 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.
+
+
+
+```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.'
+ 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
+// Save as inbound-call.mts and run: npx tsx inbound-call.mts
+import { RelayClient } from "@signalwire/sdk";
+
+const DESTINATIONS: Record = {
+ "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();
+```
+
+
+
+### 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
+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.
+
+
+
+```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
+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.
+
+
+
+```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
+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 recording
+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
+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`.
+
+
+
+```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
+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 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.
+
+
+
+```html
+
+
+
+
+ Answer with SignalWire
+
+
+
Offline
+
+
+
+
+
+
+
+
+```
+
+
+```javascript
+// 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
+// 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);
+});
+```
+
+
+
+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
+it mirrors the media the caller offered, so pass both explicitly to pin the call's shape.
+
+
+
+
+```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].
+
+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
+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].
diff --git a/fern/products/platform/pages/calling/voice/outbound-calling.mdx b/fern/products/platform/pages/calling/voice/outbound-calling.mdx
index a50cde1be9..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.
@@ -300,7 +292,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 +544,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
//
@@ -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", {
@@ -1703,7 +1711,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 +1826,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 +1856,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.
diff --git a/fern/products/platform/pages/calling/voice/overview.mdx b/fern/products/platform/pages/calling/voice/overview.mdx
index 0cec2ac120..3a6fd8a9bc 100644
--- a/fern/products/platform/pages/calling/voice/overview.mdx
+++ b/fern/products/platform/pages/calling/voice/overview.mdx
@@ -18,12 +18,12 @@ 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
+
+ Point a number at SWML, a Relay app, or the browser, and choose what runs when it rings
+
Get started with our Compatibility API
@@ -72,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/call-fabric/addresses.mdx b/fern/products/platform/pages/platform/call-fabric/addresses.mdx
index b411b3b48b..980a840232 100644
--- a/fern/products/platform/pages/platform/call-fabric/addresses.mdx
+++ b/fern/products/platform/pages/platform/call-fabric/addresses.mdx
@@ -4,4 +4,253 @@ title: Addresses
slug: addresses
---
-
+[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
+[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. 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
+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 |
+| [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
+
+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, 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
+
+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 `/private/support-agent`. Names are
+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 |
+
+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. 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.
+
+### 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'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.
+
+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.
+
+
+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
+
+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, 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
+
+### 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.
+
+
+
+
+
+
+
+
+
+
+
+**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.
+
+**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 |
+|---|---|
+| 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 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
+
+
+
+ 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 45eff2d29a..fa076889f0 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.
@@ -47,11 +47,11 @@ 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.
-
-Learn more in the [Resource Addresses](/docs/platform/addresses) guide, including contexts, naming conventions, and routing options.
+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
@@ -60,7 +60,7 @@ Learn more in the [Resource Addresses](/docs/platform/addresses) guide, includin
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.
@@ -79,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.
@@ -109,7 +109,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.
@@ -117,6 +117,13 @@ 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, 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**,
which does not support the Resources experience. [Contact Support](https://support.signalwire.com/)
@@ -148,8 +155,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/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/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
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.
-
-
-
-
-
-
-
-
-
-
-
-
-
-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
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 **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.
+
+
+
+
+
+
+
+
+
+
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.