diff --git a/fern/assets/images/img/collect-relay-lifecycle-themed.svg b/fern/assets/images/img/collect-relay-lifecycle-themed.svg new file mode 100644 index 0000000000..6971eb930f --- /dev/null +++ b/fern/assets/images/img/collect-relay-lifecycle-themed.svg @@ -0,0 +1,99 @@ + + Collect result delivered to your Relay client over the WebSocket + The caller dials the resource. SignalWire hands the call to your Relay client on the default topic. Your client answers and sends play_and_collect. SignalWire plays the prompt and the caller says dispatch. SignalWire sends your client a calling.call.collect event with result type speech and the text. wait() returns and your code picks the branch, then plays the reply and hangs up. + + + + + + + + + + + + + + Caller + + + + + + SignalWire + + + + + + Your Relay client + + + calls + the resource address + + + call arrives + on topic default + + + + answer + play_and_collect + + + + prompt plays + say booking or dispatch + + + speaks + "dispatch" + + + + calling.call.collect + type speech, text + + + + wait() returns + your code branches + + + play reply + then hangup + + + diff --git a/fern/assets/images/img/collect-webhook-lifecycle-themed.svg b/fern/assets/images/img/collect-webhook-lifecycle-themed.svg new file mode 100644 index 0000000000..55a6c261c5 --- /dev/null +++ b/fern/assets/images/img/collect-webhook-lifecycle-themed.svg @@ -0,0 +1,91 @@ + + Collect result delivered to your server as a webhook + SignalWire plays the prompt to the caller. The caller says dispatch. SignalWire stores dispatch in prompt_value and switch runs the dispatch branch. SignalWire posts a calling.call.collect event to your status_url with result type speech, the text, and a confidence score. Your server returns 200 OK. The dispatch branch plays to the caller. + + + + + + + + + + + + + + Caller + + + + + + SignalWire + + + + + + Your server + + + prompt plays + say booking or dispatch + + + + speaks + "dispatch" + + + + + prompt_value: dispatch + switch runs the dispatch branch + + + calling.call.collect + POST to status_url + speech, text, confidence + + + 200 OK, not awaited + + + + branch plays + You said dispatch + + + diff --git a/fern/llms.txt b/fern/llms.txt index c3adbd8bb4..63ac7bca78 100644 --- a/fern/llms.txt +++ b/fern/llms.txt @@ -164,7 +164,7 @@ Feature pages are grouped by channel. SDK pages come first when available, follo - [Stream call audio](/docs/platform/voice/call-streaming): Stream a live call to your own WebSocket server with SWML, Relay, or REST, then save or process the audio as it arrives, and play audio back into the call on a bidirectional stream. - [Conferencing](/docs/server-sdks/reference/python/relay/call/join-conference): Join a call to an ad-hoc audio conference. - [Secure payments](/docs/server-sdks/reference/python/relay/call/pay): Collect payment information on a call. -- [Collect caller input](/docs/server-sdks/reference/python/relay/call/play-and-collect): Play audio and collect speech or key presses. +- [Collect caller input](/docs/platform/voice/collect-input): Ask the caller a question with SWML, Relay, or Call Flow Builder, collect speech or key presses, branch on the answer, handle silence and unmatched replies, and read the result in the call log or your own code. - [Detect machines](/docs/platform/voice/machine-detection): Detect people, voicemail, and fax machines with SWML or Relay. Follow an AMD example and a fax example, handle results, and tune detection. - [Noise reduction](/docs/server-sdks/reference/python/relay/call/denoise): Start noise reduction on a call. - [Queue calls](/docs/platform/voice/call-queues): Hold callers in a queue and connect each one to the next free agent with SWML or Relay, then track, remove, and manage waiting callers. diff --git a/fern/products/platform/pages/calling/voice/TTS/speak-text.mdx b/fern/products/platform/pages/calling/voice/TTS/speak-text.mdx index 24707c10a8..bab54cbbd6 100644 --- a/fern/products/platform/pages/calling/voice/TTS/speak-text.mdx +++ b/fern/products/platform/pages/calling/voice/TTS/speak-text.mdx @@ -25,6 +25,7 @@ position: 1 [swml-set]: /docs/swml/reference/calling/set [swml-ai-languages]: /docs/swml/reference/calling/ai/languages#use-voice-strings [call-commands]: /docs/apis/rest/calls/call-commands +[collect-input]: /docs/platform/voice/collect-input Text-to-speech (TTS) reads text aloud on a live call, such as a booking confirmation or a menu. You give SignalWire the text and a voice from the [TTS voice catalog][tts-voices], and it speaks @@ -405,3 +406,4 @@ or `calling.play.volume` with the same `control_id`. The REST Calling API can't speak and collect input in one step. To speak a menu, start the call with a SWML [`prompt`][swml-prompt] or control it from a Relay client. +[Collect caller input][collect-input] shows both and how to branch on the reply. diff --git a/fern/products/platform/pages/calling/voice/collect-input.mdx b/fern/products/platform/pages/calling/voice/collect-input.mdx new file mode 100644 index 0000000000..55c1bc1df2 --- /dev/null +++ b/fern/products/platform/pages/calling/voice/collect-input.mdx @@ -0,0 +1,1394 @@ +--- +title: Collect caller input +slug: voice/collect-input +description: Ask the caller a question with SWML, Relay, or Call Flow Builder, collect speech or key presses, branch on the answer, handle silence and unmatched replies, and read the result in the call log or your own code. +max-toc-depth: 3 +--- + +[answer-relay]: /docs/platform/voice/make-and-receive-calls#answer-a-call-via-websocket-relay +[answer-swml]: /docs/platform/voice/make-and-receive-calls#answer-a-call-via-swml +[api-credentials]: /docs/platform/your-signalwire-api-space +[cfb]: /docs/call-flow-builder +[cfb-answer]: /docs/call-flow-builder/reference/answer-call +[cfb-conditions]: /docs/call-flow-builder/reference/conditions +[cfb-forward]: /docs/call-flow-builder/reference/forward-to-phone +[cfb-gather]: /docs/call-flow-builder/reference/gather-input +[cfb-gather-loop]: /docs/call-flow-builder/reference/gather-input#input-with-infinite-loop-prevention +[cfb-hangup]: /docs/call-flow-builder/reference/hangup-call +[cfb-play]: /docs/call-flow-builder/reference/play-audio-or-tts +[cfb-set-variables]: /docs/call-flow-builder/reference/set-variables +[cfb-version]: /docs/call-flow-builder/guides/version +[collect-webhook]: /docs/apis/rest/webhooks/collect-status-callback +[control-flow]: /docs/swml/guides/goto-execute-transfer-disambiguation +[create-swml-script]: /docs/platform/voice/make-and-receive-calls#create-the-swml-script +[forward-relay]: /docs/platform/voice/forward-calls#forward-an-incoming-call-via-websocket-relay +[forward-swml]: /docs/platform/voice/forward-calls#forward-an-incoming-call-via-swml +[how-incoming]: /docs/platform/voice/make-and-receive-calls#how-an-incoming-call-reaches-your-code +[make-receive]: /docs/platform/voice/make-and-receive-calls +[rest-calling]: /docs/apis/rest/calls/call-commands +[server-sdks]: /docs/server-sdks +[speak-rest]: /docs/platform/voice/speak-text#speak-text-on-a-call-in-progress-via-rest +[speak-text]: /docs/platform/voice/speak-text +[swml]: /docs/swml +[swml-goto]: /docs/swml/reference/calling/goto +[swml-prompt]: /docs/swml/reference/calling/prompt +[swml-switch]: /docs/swml/reference/calling/switch +[swml-transfer]: /docs/swml/reference/calling/transfer + +Ask the caller a question, collect what they say or press, and send the call down a branch based +on the answer. That one pattern is a department menu, a yes-or-no confirmation, or a request for +an account number, and it works the same whether the call reached your flow from a phone number, a +SIP address, or a browser. Start by asking one question and branching on the reply, then handle +silence and unmatched replies, track what the flow heard, and extend the prompt to keypad digits +and a retry. + +## Pick the right product for collecting caller input + +| Function | SWML | WebSocket (Relay) | REST | Call Flow Builder | +|---|---|---|---|---| +| Ask a question and branch on the answer without running a server | | | | | +| Collect speech, keypad digits, or both on one prompt | | | | | +| Play the question and collect the reply in one command | | | | | +| Tell silence apart from speech the recognizer could not make out | | | | | +| Receive the reply in your own process without hosting a webhook | | | | | +| Collect input on a call another process answered, by its call ID | | | | | +| Build the menu visually, with no code | | | | | + +- [SWML][swml] is a JSON or YAML document that SignalWire runs step by step. Generate it with the + [Server SDKs][server-sdks] or write it by hand, then host it in your Space or serve it from your + own server. Its `prompt` method asks and collects in one step. To compare the reply on your own + server instead, `transfer` or `execute` to a URL: SignalWire posts the document's variables, + `prompt_value` among them, and runs the SWML you return. +- [Relay][server-sdks] is the WebSocket client in the Server SDKs. Your process receives the call, + plays the question, and gets the reply back as a value it can compare. +- The [REST Calling API][rest-calling] sends the same collect command over HTTP to a call you + already hold the ID for. The reply arrives as a webhook. +- [Call Flow Builder][cfb] is a visual editor in the Dashboard. Its Gather Input node asks, + collects, and branches without code. + +The Browser SDK places and receives calls from a web page. It is the caller or the callee, never +the flow, so it has no column here. + +## Prepare to collect caller input + +Start from working inbound call setup in [Make and receive calls][make-receive]: a SWML Script, +Relay Application, or Call Flow that a call can reach. This guide picks up once a call reaches your +flow. [How an incoming call reaches your code][how-incoming] explains why the same flow answers a +phone number, a SIP address, a browser call, or the Dashboard's **Click-to-Test** button. To pick a +voice for the questions, see [Speak text on a call][speak-text]. + +Have these values ready: + +- Your Space URL, such as `.signalwire.com`. +- For the Relay and REST paths, your Project ID and an API token from the Dashboard's + [API credentials][api-credentials] page, with the **Voice** permission enabled. A hosted SWML + script or a Call Flow needs no credentials. +- Python 3 or Node.js for the served SWML and Relay samples. A served document also needs the + tunnel and basic auth password set up in [Answer a call via SWML][answer-swml]. + +Replace these placeholders in the samples. The hosted SWML and Call Flow Builder paths use none of +them. + +| Value | Replace with | +|---|---| +| `` | Your Space's subdomain in `.signalwire.com` | +| `` | Your Project ID | +| `` | Your API token, used only in server code | +| `` | A password you choose; SignalWire presents it when it fetches a served SWML document | +| `` | The ID of a call in progress, for the REST samples | +| `` | A public HTTPS URL on your server that accepts POST requests | + +## Ask a question and branch on the answer + +In every surface the pattern is the same: play a question, wait for input, compare the result, and +continue down one path. Bayview Taxi asks callers to say "booking" or "dispatch". Speech hints tell +the recognizer which words to expect, which makes short single-word replies reliable. + +### Ask a question and branch via SWML + +Three methods do the work. [`prompt`][swml-prompt] plays the question and waits. Setting any +speech parameter, such as `speech_hints`, switches it from keypad digits to speech; the recognized +text lands in the `prompt_value` variable and the outcome in `prompt_result`. +[`switch`][swml-switch] compares a variable against case keys and runs the matching list, or +`default` when nothing matches. [`transfer`][swml-transfer] jumps to another section and does not +return. + +The `booking` and `dispatch` sections each speak a different line, and `no_match` catches +everything else. The [Server SDKs][server-sdks] build the document with the SWML builder and serve +it over HTTP; the YAML and JSON forms are what you paste into a hosted script. The builder's +methods append to `main`; the named sections go through the service. + + + +```python +# Install: python -m pip install signalwire-sdk +# Save as collect_input.py and run: python collect_input.py +from signalwire import SWMLBuilder, SWMLService + +service = SWMLService( + name="collect-input", + route="/swml", + port=3000, + basic_auth=("signalwire", ""), +) +( + SWMLBuilder(service) + .answer() + .prompt( + play=["say:Thanks for calling Bayview Taxi. Say booking or dispatch."], + speech_hints=["booking", "dispatch"], + ) + .switch( + variable="prompt_value", + case={ + "booking": [{"transfer": {"dest": "booking"}}], + "dispatch": [{"transfer": {"dest": "dispatch"}}], + }, + default=[{"transfer": {"dest": "no_match"}}], + ) +) + +# The builder appends to the main section. Named sections go through the service. +service.add_verb_to_section("booking", "play", {"url": "say:You said booking."}) +service.add_verb_to_section("booking", "hangup", {}) + +service.add_verb_to_section("dispatch", "play", {"url": "say:You said dispatch."}) +service.add_verb_to_section("dispatch", "hangup", {}) + +service.add_verb_to_section("no_match", "play", {"url": "say:Sorry, I didn't catch that. Goodbye."}) +service.add_verb_to_section("no_match", "hangup", {}) + +service.serve() +``` + + +```typescript +// Install: npm install @signalwire/sdk +// This sample also runs as JavaScript: save as collect-input.mjs, +// then run: node collect-input.mjs +import { SWMLService } from "@signalwire/sdk"; + +const service = new SWMLService({ + name: "collect-input", + route: "/swml", + port: 3000, + basicAuth: ["signalwire", ""], +}); +const swml = service.getBuilder(); + +swml + .answer() + .prompt({ + play: ["say:Thanks for calling Bayview Taxi. Say booking or dispatch."], + speech_hints: ["booking", "dispatch"], + }) + .switch({ + variable: "prompt_value", + case: { + booking: [{ transfer: { dest: "booking" } }], + dispatch: [{ transfer: { dest: "dispatch" } }], + }, + default: [{ transfer: { dest: "no_match" } }], + }); + +// The builder's verb methods append to the main section. Named sections use addVerbToSection. +swml.addVerbToSection("booking", "play", { url: "say:You said booking." }); +swml.addVerbToSection("booking", "hangup", {}); + +swml.addVerbToSection("dispatch", "play", { url: "say:You said dispatch." }); +swml.addVerbToSection("dispatch", "hangup", {}); + +swml.addVerbToSection("no_match", "play", { url: "say:Sorry, I didn't catch that. Goodbye." }); +swml.addVerbToSection("no_match", "hangup", {}); + +await service.serve(); +``` + + +```yaml +version: 1.0.0 +sections: + main: + - answer: {} + - prompt: + play: + - "say:Thanks for calling Bayview Taxi. Say booking or dispatch." + speech_hints: + - booking + - dispatch + - switch: + variable: prompt_value + case: + booking: + - transfer: + dest: booking + dispatch: + - transfer: + dest: dispatch + default: + - transfer: + dest: no_match + booking: + - play: + url: "say:You said booking." + - hangup: {} + dispatch: + - play: + url: "say:You said dispatch." + - hangup: {} + no_match: + - play: + url: "say:Sorry, I didn't catch that. Goodbye." + - hangup: {} +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "answer": {} }, + { + "prompt": { + "play": ["say:Thanks for calling Bayview Taxi. Say booking or dispatch."], + "speech_hints": ["booking", "dispatch"] + } + }, + { + "switch": { + "variable": "prompt_value", + "case": { + "booking": [{ "transfer": { "dest": "booking" } }], + "dispatch": [{ "transfer": { "dest": "dispatch" } }] + }, + "default": [{ "transfer": { "dest": "no_match" } }] + } + } + ], + "booking": [ + { "play": { "url": "say:You said booking." } }, + { "hangup": {} } + ], + "dispatch": [ + { "play": { "url": "say:You said dispatch." } }, + { "hangup": {} } + ], + "no_match": [ + { "play": { "url": "say:Sorry, I didn't catch that. Goodbye." } }, + { "hangup": {} } + ] + } +} +``` + + + +`switch` compares the whole value exactly. SignalWire lowercases recognized speech and strips its +punctuation before storing it in `prompt_value`, so "Booking." still matches the `booking` case, +but "booking please" does not. + +Run the program and point an External URL SWML Script at it through your tunnel, or paste the YAML +or JSON into a Hosted Script, as in [Create the SWML Script][create-swml-script]. To put dispatch +callers through to a person instead of playing a line, the `dispatch` section can +[connect the call to a phone][forward-swml]. + +Then open the Resource under **My Resources** and select **Click-to-Test**, under the Resource's +name. A new tab opens, shows "Connecting", and places a browser call to the Resource. Allow +microphone access if the browser asks. Call three times: say "booking", say "dispatch", and say +nothing. You hear a different line each time. + +If you don't, match the symptom: + +- **The call connects but stays silent.** The flow never ran. [Answer a call via SWML][answer-swml] + shows how to confirm that SignalWire can fetch a served document and that the Resource points at + it. +- **Every call ends in the `no_match` line.** The recognized text did not equal a case key. + [Track what the flow heard](#track-what-the-flow-heard) shows the text SignalWire stored. +- **Click-to-Test is disabled.** It appears only on Resources that handle calls and have an + address. Open the Resource's **Addresses & Phone Numbers** tab and add an alias. + +### Ask a question and branch via WebSocket (Relay) + +The [Server SDKs][server-sdks] expose the prompt as `play_and_collect` on the call. It plays the +question and resolves when input arrives, times out, or fails. The result carries a `type` of +`speech`, `digit`, `no_input`, `no_match`, or `error`, and for speech the recognized `text` with a +`confidence` score. Your code compares the text and continues. + + + +```python +# Install: python -m pip install signalwire-sdk +# Save as collect_input_relay.py and run: python collect_input_relay.py +from signalwire.relay import RelayClient + +client = RelayClient( + project="", + token="", + host=".signalwire.com", + contexts=["default"], +) + + +@client.on_call +async def handle_call(call): + await call.answer() + + action = await call.play_and_collect( + media=[{"type": "tts", "params": {"text": "Thanks for calling Bayview Taxi. Say booking or dispatch."}}], + collect={"speech": {"hints": ["booking", "dispatch"], "language": "en-US"}}, + ) + event = await action.wait() + result = event.params.get("result", {}) + + heard = "" + if result.get("type") == "speech": + heard = result.get("params", {}).get("text", "").lower() + + if "booking" in heard: + reply = "You said booking." + elif "dispatch" in heard: + reply = "You said dispatch." + else: + reply = "Sorry, I didn't catch that. Goodbye." + + async def hang_up_after_playback(_event): + if call.state != "ended": + await call.hangup() + + await call.play( + [{"type": "tts", "params": {"text": reply}}], + on_completed=hang_up_after_playback, + ) + await call.wait_for_ended() + + +client.run() +``` + + +```typescript +// Install: npm install @signalwire/sdk +// Save as collect-input-relay.mts and run: npx tsx collect-input-relay.mts +import { RelayClient } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "", + token: "", + host: ".signalwire.com", + contexts: ["default"], +}); + +client.onCall(async (call) => { + await call.answer(); + + const action = await call.playAndCollect( + [{ type: "tts", text: "Thanks for calling Bayview Taxi. Say booking or dispatch." }], + { speech: { hints: ["booking", "dispatch"], language: "en-US" } }, + ); + const event = await action.wait(); + const result = (event.params.result ?? {}) as { + type?: string; + params?: { text?: string }; + }; + + const heard = result.type === "speech" ? (result.params?.text ?? "").toLowerCase() : ""; + + let reply = "Sorry, I didn't catch that. Goodbye."; + if (heard.includes("booking")) reply = "You said booking."; + else if (heard.includes("dispatch")) reply = "You said dispatch."; + + await call.play([{ type: "tts", text: reply }], { + onCompleted: async () => { + if (call.state !== "ended") await call.hangup(); + }, + }); + await call.waitForEnded(); +}); + +await client.run(); +``` + + + +Because the comparison runs in your code, you can match loosely: `"booking" in heard` accepts +"booking please" as well as "booking". To put dispatch callers through to a person, +[connect the call to a phone][forward-relay] instead of playing the reply. + +Run the client and create a Relay Application Resource whose **Topic** is `default`, the value in +the client's `contexts`, as in [Answer a call via WebSocket (Relay)][answer-relay]. Then open the +Resource under **My Resources**, select **Click-to-Test**, and call three times to hear the three +outcomes. If the client logs an authentication error at startup, the project, token, and Space do +not belong together. If the call connects but stays silent, check that the client process is still +running and that the Resource's **Topic** matches its `contexts`. + +Over REST, the same collect runs as the `calling.collect` command on a call you already hold the +ID for, such as one a hosted script answered. REST can't play the question and collect in one +command, so [speak the question with `calling.play`][speak-rest] first, and send the collect once +playback finishes with `start_input_timers` set to `true`. Without it, the wait for input doesn't +start until you send `calling.collect.start_input_timers`. The reply arrives at `status_url` as a +`calling.call.collect` event instead of a return value, and `control_id` names the collect so you +can match the event to it. + + + +```python +# Install: python -m pip install signalwire-sdk +# Save as collect_rest.py and run: python collect_rest.py +from signalwire.rest import RestClient + +client = RestClient( + project="", + token="", + host=".signalwire.com", +) + +# collect() has no status_url or start_input_timers argument; extras adds them to the command's params. +client.calling.collect( + "", + control_id="collect-1", + speech={"hints": ["booking", "dispatch"], "language": "en-US"}, + extras={"status_url": "", "start_input_timers": True}, +) +``` + + +```typescript +// Install: npm install @signalwire/sdk +// This sample also runs as JavaScript: save as collect-rest.mjs, +// then run: node collect-rest.mjs +import { RestClient } from "@signalwire/sdk"; + +const client = new RestClient({ + project: "", + token: "", + host: ".signalwire.com", +}); + +// collect() has no status_url or start_input_timers option; extras adds them to the command's params. +await client.calling.collect("", { + control_id: "collect-1", + speech: { hints: ["booking", "dispatch"], language: "en-US" }, + extras: { status_url: "", start_input_timers: true }, +}); +``` + + +```bash +# Runs against a call that is already answered; play the question first with calling.play. +curl --request POST "https://.signalwire.com/api/calling/calls" \ + --user ":" \ + --header "Content-Type: application/json" \ + --data '{ + "id": "", + "command": "calling.collect", + "params": { + "control_id": "collect-1", + "speech": { "hints": ["booking", "dispatch"], "language": "en-US" }, + "start_input_timers": true, + "status_url": "" + } + }' +``` + + + +The [Calling API reference][rest-calling] documents every `calling.collect` field, and the +[collect status callback reference][collect-webhook] the event that reaches `status_url`. + +### Ask a question and branch via Call Flow Builder + +Open **Tools** > **Call Flow Builder** in the Dashboard, select **Add New**, name the flow, and +select **Save**. Open the flow's **More Options** menu and select **Edit** to reach the canvas. +Every flow starts from the **Handle Call** node. Connect these nodes in order: + +1. [**Answer Call**][cfb-answer]. +2. [**Gather Input**][cfb-gather], with "Thanks for calling Bayview Taxi. Say booking or dispatch." + as its Text to Speech. Every input option pairs a key with a word: add one option with + **Caller presses** `1` and **Or says** `booking`, and a second with `2` and `dispatch`. The node + grows one output connector per option, plus **Unknown** and **No Input**. +3. A [**Play Audio or TTS**][cfb-play] node on each option's connector, with the matching line, + and a [**Hangup Call**][cfb-hangup] node after each. To put dispatch callers through to a + person, use a [**Forward to Phone**][cfb-forward] node instead. + +Leave **Unknown** and **No Input** for the next task. Select **Deploy** to make this version the +live one; the [versioning guide][cfb-version] explains how deployed and draft versions relate. A +deployed Call Flow is a Resource under **My Resources**, so **Click-to-Test** works here too: call +three times and hear the three outcomes. + +## Handle silence and unmatched speech + +A caller who says nothing, or whose words the recognizer could not make out, must still get an +answer. Each surface reports the two cases separately, so the flow can say something different for +each. A reply the recognizer did understand comes back as text whether or not it is one of your +words, so an off-menu reply is caught by your comparison, not by these cases. + +### Handle silence and unmatched speech via SWML + +After `prompt`, `prompt_result` is `match_speech`, `no_input`, `no_match`, or `failed`, and +`match_digits` once keypad input is enabled. `match_speech` means the recognizer returned text, not +that the text is one of your cases. The `default` case already routes everything but a +match to `no_match`; this change makes that section say which one happened, using a JavaScript +expression inside the spoken text. Only the `no_match` section changes. + + + +```python {1-6} +service.add_verb_to_section( + "no_match", + "play", + {"url": "say:${prompt_result == 'no_input' ? \"I didn't hear anything.\" : \"I didn't understand that.\"} Goodbye."}, +) +service.add_verb_to_section("no_match", "hangup", {}) +``` + + +```typescript {1-4} +swml.addVerbToSection("no_match", "play", { + url: "say:${prompt_result == 'no_input' ? \"I didn't hear anything.\" : \"I didn't understand that.\"} Goodbye.", +}); +swml.addVerbToSection("no_match", "hangup", {}); +``` + + +```yaml {3} + no_match: + - play: + url: "say:${prompt_result == 'no_input' ? \"I didn't hear anything.\" : \"I didn't understand that.\"} Goodbye." + - hangup: {} +``` + + +```json {2} + "no_match": [ + { "play": { "url": "say:${prompt_result == 'no_input' ? \"I didn't hear anything.\" : \"I didn't understand that.\"} Goodbye." } }, + { "hangup": {} } + ] +``` + + + +The wait for input before `no_input` is five seconds by default; `prompt.initial_timeout` changes +it. + +### Handle silence and unmatched speech via WebSocket (Relay) + +The collect result's `type` is `no_input` when the caller stayed silent. Anything the recognizer +transcribes comes back as `speech`, whether or not it's one of your words, so your comparison +catches an off-menu reply. `no_match` means the recognizer returned no text at all. Replace the +reply selection in the Relay sample: + + + +```python {1-8} + if "booking" in heard: + reply = "You said booking." + elif "dispatch" in heard: + reply = "You said dispatch." + elif result.get("type") == "no_input": + reply = "I didn't hear anything. Goodbye." + else: + reply = "I didn't understand that. Goodbye." +``` + + +```typescript {1-4} + let reply = "I didn't understand that. Goodbye."; + if (heard.includes("booking")) reply = "You said booking."; + else if (heard.includes("dispatch")) reply = "You said dispatch."; + else if (result.type === "no_input") reply = "I didn't hear anything. Goodbye."; +``` + + + +### Handle silence and unmatched speech via Call Flow Builder + +Connect the **Gather Input** node's **No Input** connector to a **Play Audio or TTS** node that +says "I didn't hear anything. Goodbye.", and its **Unknown** connector to one that says "I didn't +understand that. Goodbye." Connect both to a **Hangup Call** node and select **Deploy**. + +## Track what the flow heard + +Three places show what happened on the call: the Dashboard's call log, a webhook you host, and the +Relay client's own event stream. + +### See the flow's steps in the Dashboard + +Open **Logs** > **Voice** in the Dashboard and select the most recent call. The call's timeline +lists each step the flow ran in order: the call state changes, a **Play** entry for each spoken +line, and a **Collect** entry for the prompt. Select an entry to see its details. A document that +fails to load or contains an invalid method shows a **Script Warning** or **Error** entry with the +reason, so a broken flow is visible here without a support ticket. + +The timeline records that input was collected, not what the caller said. For the recognized text +itself, use one of the two paths below. + +### Receive the result via SWML + +Add a `status_url` to the `prompt` method. SignalWire sends a `calling.call.collect` event to that +URL when input arrives, and `calling.call.play` events to the same URL as the question plays, so +check `event_type` before reading `params.result`. The same collect event reaches the `status_url` +on a REST `calling.collect` command. + + + +```python {4} + .prompt( + play=["say:Thanks for calling Bayview Taxi. Say booking or dispatch."], + speech_hints=["booking", "dispatch"], + status_url="", + ) +``` + + +```typescript {4} + .prompt({ + play: ["say:Thanks for calling Bayview Taxi. Say booking or dispatch."], + speech_hints: ["booking", "dispatch"], + status_url: "", + }) +``` + + +```yaml {5} + - prompt: + play: + - "say:Thanks for calling Bayview Taxi. Say booking or dispatch." + speech_hints: [booking, dispatch] + status_url: "" +``` + + +```json {5} + { + "prompt": { + "play": ["say:Thanks for calling Bayview Taxi. Say booking or dispatch."], + "speech_hints": ["booking", "dispatch"], + "status_url": "" + } + } +``` + + + +The POST body for a recognized reply looks like this. `params.result.type` is `no_input` or +`no_match` for the failure cases, with no `params.result.params`. + +```json +{ + "event_type": "calling.call.collect", + "event_channel": "swml:...", + "timestamp": 1757971200.123, + "project_id": "", + "space_id": "...", + "params": { + "call_id": "", + "node_id": "...", + "control_id": "...", + "result": { + "type": "speech", + "params": { + "text": "dispatch", + "confidence": 0.94 + } + } + } +} +``` + +The [collect status callback reference][collect-webhook] documents every field of the event, and +the [`prompt` reference][swml-prompt] the play events that reach the same URL. + + + +SignalWire plays the prompt to the caller. The caller says dispatch. SignalWire stores dispatch in prompt_value and switch runs the dispatch branch. SignalWire posts a calling.call.collect event to your status_url with result type speech, the text, and a confidence score, and your server returns 200 OK. The flow does not wait for that reply: the dispatch branch plays to the caller meanwhile. + + + + + +```mermaid +sequenceDiagram + participant Caller + participant SW as SignalWire + participant Srv as Your server + + SW->>Caller: prompt plays: say booking or dispatch + Caller->>SW: says "dispatch" + Note over SW: prompt_value is dispatch, switch runs the dispatch branch + par The flow does not wait for the webhook + SW->>Srv: POST status_url: calling.call.collect, type speech, text, confidence + Srv-->>SW: 200 OK + and + SW->>Caller: dispatch branch plays + end +``` + + + +### Receive the result via WebSocket (Relay) + +The Relay client already holds the result: it is the event that `action.wait()` returned. To log +every collect event as it happens, register a handler on the call before starting the prompt. The +[Server SDKs][server-sdks] document the fields of every call event. + + + +```python {1-4} + def log_collect(event): + print("collect:", event.params.get("result")) + + call.on("calling.call.collect", log_collect) +``` + + +```typescript {1-3} + call.on("calling.call.collect", (event) => { + console.log("collect:", event.params.result); + }); +``` + + + + + +The caller dials the Resource. SignalWire hands the call to your Relay client on the default topic. Your client answers and sends play_and_collect. SignalWire plays the prompt and the caller says dispatch. SignalWire sends your client a calling.call.collect event with result type speech and the text. wait() returns and your code picks the branch, then plays the reply and hangs up. + + + + + +```mermaid +sequenceDiagram + participant Caller + participant SW as SignalWire + participant App as Your Relay client + + Caller->>SW: calls the Resource address + SW->>App: call arrives on topic default + App->>SW: answer, play_and_collect + SW->>Caller: prompt plays: say booking or dispatch + Caller->>SW: says "dispatch" + SW-->>App: calling.call.collect: type speech, text + Note over App: wait() returns, your code branches + App->>SW: play reply, then hangup +``` + + + +### Compare the surfaces + +| Surface | Where the recognized text shows up | How silence and unrecognized speech are reported | +|---|---|---| +| SWML | `prompt_value` inside the document; the `status_url` webhook | `prompt_result` is `no_input` or `no_match` | +| WebSocket (Relay) | The `result` on the event `action.wait()` returns; `call.on` handlers | `result.type` is `no_input` or `no_match` | +| REST | The `status_url` webhook only | `params.result.type` is `no_input` or `no_match` | +| Call Flow Builder | The `%{prompt_value}` variable in any later node | The **No Input** and **Unknown** connectors | + +The Dashboard's call log shows that a Collect step ran, for every surface, but not its outcome and +never the text. + +## Examples + +### Accept a keypad digit as well as speech + +Some callers are in a noisy place or on a handset that makes speaking awkward. Offer keys and +words at once, and treat "press 1" and "say booking" as the same answer. + +#### Accept a keypad digit as well as speech via SWML + +Setting one digit parameter alongside the speech parameters enables both inputs. `max_digits` +joins the prompt, and the digit keys join the switch. + + + +```python +# Install: python -m pip install signalwire-sdk +# Save as collect_input.py and run: python collect_input.py +from signalwire import SWMLBuilder, SWMLService + +service = SWMLService( + name="collect-input", + route="/swml", + port=3000, + basic_auth=("signalwire", ""), +) +( + SWMLBuilder(service) + .answer() + .prompt( + play=["say:Thanks for calling Bayview Taxi. Say booking or press 1. Say dispatch or press 2."], + max_digits=1, + speech_hints=["booking", "dispatch"], + ) + .switch( + variable="prompt_value", + case={ + "booking": [{"transfer": {"dest": "booking"}}], + "1": [{"transfer": {"dest": "booking"}}], + "dispatch": [{"transfer": {"dest": "dispatch"}}], + "2": [{"transfer": {"dest": "dispatch"}}], + }, + default=[{"transfer": {"dest": "no_match"}}], + ) +) + +# The builder appends to the main section. Named sections go through the service. +service.add_verb_to_section("booking", "play", {"url": "say:You said booking."}) +service.add_verb_to_section("booking", "hangup", {}) + +service.add_verb_to_section("dispatch", "play", {"url": "say:You said dispatch."}) +service.add_verb_to_section("dispatch", "hangup", {}) + +service.add_verb_to_section("no_match", "play", {"url": "say:Sorry, I didn't catch that. Goodbye."}) +service.add_verb_to_section("no_match", "hangup", {}) + +service.serve() +``` + + +```typescript +// Install: npm install @signalwire/sdk +// This sample also runs as JavaScript: save as collect-input.mjs, +// then run: node collect-input.mjs +import { SWMLService } from "@signalwire/sdk"; + +const service = new SWMLService({ + name: "collect-input", + route: "/swml", + port: 3000, + basicAuth: ["signalwire", ""], +}); +const swml = service.getBuilder(); + +swml + .answer() + .prompt({ + play: ["say:Thanks for calling Bayview Taxi. Say booking or press 1. Say dispatch or press 2."], + max_digits: 1, + speech_hints: ["booking", "dispatch"], + }) + .switch({ + variable: "prompt_value", + case: { + booking: [{ transfer: { dest: "booking" } }], + "1": [{ transfer: { dest: "booking" } }], + dispatch: [{ transfer: { dest: "dispatch" } }], + "2": [{ transfer: { dest: "dispatch" } }], + }, + default: [{ transfer: { dest: "no_match" } }], + }); + +// The builder's verb methods append to the main section. Named sections use addVerbToSection. +swml.addVerbToSection("booking", "play", { url: "say:You said booking." }); +swml.addVerbToSection("booking", "hangup", {}); + +swml.addVerbToSection("dispatch", "play", { url: "say:You said dispatch." }); +swml.addVerbToSection("dispatch", "hangup", {}); + +swml.addVerbToSection("no_match", "play", { url: "say:Sorry, I didn't catch that. Goodbye." }); +swml.addVerbToSection("no_match", "hangup", {}); + +await service.serve(); +``` + + +```yaml +version: 1.0.0 +sections: + main: + - answer: {} + - prompt: + play: + - "say:Thanks for calling Bayview Taxi. Say booking or press 1. Say dispatch or press 2." + max_digits: 1 + speech_hints: + - booking + - dispatch + - switch: + variable: prompt_value + case: + booking: + - transfer: + dest: booking + "1": + - transfer: + dest: booking + dispatch: + - transfer: + dest: dispatch + "2": + - transfer: + dest: dispatch + default: + - transfer: + dest: no_match + booking: + - play: + url: "say:You said booking." + - hangup: {} + dispatch: + - play: + url: "say:You said dispatch." + - hangup: {} + no_match: + - play: + url: "say:Sorry, I didn't catch that. Goodbye." + - hangup: {} +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "answer": {} }, + { + "prompt": { + "play": ["say:Thanks for calling Bayview Taxi. Say booking or press 1. Say dispatch or press 2."], + "max_digits": 1, + "speech_hints": ["booking", "dispatch"] + } + }, + { + "switch": { + "variable": "prompt_value", + "case": { + "booking": [{ "transfer": { "dest": "booking" } }], + "1": [{ "transfer": { "dest": "booking" } }], + "dispatch": [{ "transfer": { "dest": "dispatch" } }], + "2": [{ "transfer": { "dest": "dispatch" } }] + }, + "default": [{ "transfer": { "dest": "no_match" } }] + } + } + ], + "booking": [ + { "play": { "url": "say:You said booking." } }, + { "hangup": {} } + ], + "dispatch": [ + { "play": { "url": "say:You said dispatch." } }, + { "hangup": {} } + ], + "no_match": [ + { "play": { "url": "say:Sorry, I didn't catch that. Goodbye." } }, + { "hangup": {} } + ] + } +} +``` + + + +#### Accept a keypad digit as well as speech via WebSocket (Relay) + +Pass both `digits` and `speech` in the collect config. A keypad reply comes back with type +`digit` and the pressed key in `params.digits`, so the code maps keys to the same words. + + + +```python +# Install: python -m pip install signalwire-sdk +# Save as collect_input_relay.py and run: python collect_input_relay.py +from signalwire.relay import RelayClient + +client = RelayClient( + project="", + token="", + host=".signalwire.com", + contexts=["default"], +) + +BY_KEY = {"1": "booking", "2": "dispatch"} + + +@client.on_call +async def handle_call(call): + await call.answer() + + action = await call.play_and_collect( + media=[{"type": "tts", "params": {"text": "Thanks for calling Bayview Taxi. Say booking or press 1. Say dispatch or press 2."}}], + collect={"digits": {"max": 1}, "speech": {"hints": ["booking", "dispatch"], "language": "en-US"}}, + ) + event = await action.wait() + result = event.params.get("result", {}) + + heard = "" + if result.get("type") == "digit": + heard = BY_KEY.get(result.get("params", {}).get("digits", ""), "") + elif result.get("type") == "speech": + heard = result.get("params", {}).get("text", "").lower() + + if "booking" in heard: + reply = "You said booking." + elif "dispatch" in heard: + reply = "You said dispatch." + else: + reply = "Sorry, I didn't catch that. Goodbye." + + async def hang_up_after_playback(_event): + if call.state != "ended": + await call.hangup() + + await call.play( + [{"type": "tts", "params": {"text": reply}}], + on_completed=hang_up_after_playback, + ) + await call.wait_for_ended() + + +client.run() +``` + + +```typescript +// Install: npm install @signalwire/sdk +// Save as collect-input-relay.mts and run: npx tsx collect-input-relay.mts +import { RelayClient } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "", + token: "", + host: ".signalwire.com", + contexts: ["default"], +}); + +const byKey: Record = { "1": "booking", "2": "dispatch" }; + +client.onCall(async (call) => { + await call.answer(); + + const action = await call.playAndCollect( + [{ type: "tts", text: "Thanks for calling Bayview Taxi. Say booking or press 1. Say dispatch or press 2." }], + { digits: { max: 1 }, speech: { hints: ["booking", "dispatch"], language: "en-US" } }, + ); + const event = await action.wait(); + const result = (event.params.result ?? {}) as { + type?: string; + params?: { text?: string; digits?: string }; + }; + + let heard = ""; + if (result.type === "digit") heard = byKey[result.params?.digits ?? ""] ?? ""; + else if (result.type === "speech") heard = (result.params?.text ?? "").toLowerCase(); + + let reply = "Sorry, I didn't catch that. Goodbye."; + if (heard.includes("booking")) reply = "You said booking."; + else if (heard.includes("dispatch")) reply = "You said dispatch."; + + await call.play([{ type: "tts", text: reply }], { + onCompleted: async () => { + if (call.state !== "ended") await call.hangup(); + }, + }); + await call.waitForEnded(); +}); + +await client.run(); +``` + + + +#### Accept a keypad digit as well as speech via Call Flow Builder + +The flow you built already accepts both, because every **Gather Input** option carries a +**Caller presses** key as well as an **Or says** word. Change the prompt text to mention the keys +and select **Deploy**. + +### Ask again when the caller says nothing + +One retry turns a dead end into a second chance. Ask the question again after silence, then give +up. + +#### Ask again when the caller says nothing via SWML + +[`goto`][swml-goto] jumps to a label within the same section, and its `when` condition and `max` +count keep the loop bounded. A `label` before the prompt and a conditional `goto` after it repeat +the question once. The [control flow guide][control-flow] compares `goto` with `execute` and +`transfer`. + + + +```typescript +// Install: npm install @signalwire/sdk +// This sample also runs as JavaScript: save as collect-input.mjs, +// then run: node collect-input.mjs +import { SWMLService } from "@signalwire/sdk"; + +const service = new SWMLService({ + name: "collect-input", + route: "/swml", + port: 3000, + basicAuth: ["signalwire", ""], +}); +const swml = service.getBuilder(); + +swml + .answer() + // The Python SDK's builder can't emit label, whose value is a bare string rather than an object; + // from Python, paste the YAML or JSON tab into a hosted script. + .label("ask") + .prompt({ + play: ["say:Thanks for calling Bayview Taxi. Say booking or dispatch."], + speech_hints: ["booking", "dispatch"], + }) + .goto({ label: "ask", when: "prompt_result == 'no_input'", max: 1 }) + .switch({ + variable: "prompt_value", + case: { + booking: [{ transfer: { dest: "booking" } }], + dispatch: [{ transfer: { dest: "dispatch" } }], + }, + default: [{ transfer: { dest: "no_match" } }], + }); + +// The builder's verb methods append to the main section. Named sections use addVerbToSection. +swml.addVerbToSection("booking", "play", { url: "say:You said booking." }); +swml.addVerbToSection("booking", "hangup", {}); + +swml.addVerbToSection("dispatch", "play", { url: "say:You said dispatch." }); +swml.addVerbToSection("dispatch", "hangup", {}); + +swml.addVerbToSection("no_match", "play", { url: "say:Sorry, I didn't catch that. Goodbye." }); +swml.addVerbToSection("no_match", "hangup", {}); + +await service.serve(); +``` + + +```yaml +version: 1.0.0 +sections: + main: + - answer: {} + - label: ask + - prompt: + play: + - "say:Thanks for calling Bayview Taxi. Say booking or dispatch." + speech_hints: + - booking + - dispatch + - goto: + label: ask + when: "prompt_result == 'no_input'" + max: 1 + - switch: + variable: prompt_value + case: + booking: + - transfer: + dest: booking + dispatch: + - transfer: + dest: dispatch + default: + - transfer: + dest: no_match + booking: + - play: + url: "say:You said booking." + - hangup: {} + dispatch: + - play: + url: "say:You said dispatch." + - hangup: {} + no_match: + - play: + url: "say:Sorry, I didn't catch that. Goodbye." + - hangup: {} +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "answer": {} }, + { "label": "ask" }, + { + "prompt": { + "play": ["say:Thanks for calling Bayview Taxi. Say booking or dispatch."], + "speech_hints": ["booking", "dispatch"] + } + }, + { + "goto": { + "label": "ask", + "when": "prompt_result == 'no_input'", + "max": 1 + } + }, + { + "switch": { + "variable": "prompt_value", + "case": { + "booking": [{ "transfer": { "dest": "booking" } }], + "dispatch": [{ "transfer": { "dest": "dispatch" } }] + }, + "default": [{ "transfer": { "dest": "no_match" } }] + } + } + ], + "booking": [ + { "play": { "url": "say:You said booking." } }, + { "hangup": {} } + ], + "dispatch": [ + { "play": { "url": "say:You said dispatch." } }, + { "hangup": {} } + ], + "no_match": [ + { "play": { "url": "say:Sorry, I didn't catch that. Goodbye." } }, + { "hangup": {} } + ] + } +} +``` + + + +#### Ask again when the caller says nothing via WebSocket (Relay) + +A loop around the prompt exits on the first reply of any kind and gives up after two silent +attempts. + + + +```python +# Install: python -m pip install signalwire-sdk +# Save as collect_input_relay.py and run: python collect_input_relay.py +from signalwire.relay import RelayClient + +client = RelayClient( + project="", + token="", + host=".signalwire.com", + contexts=["default"], +) + + +@client.on_call +async def handle_call(call): + await call.answer() + + result = {} + for _attempt in range(2): + action = await call.play_and_collect( + media=[{"type": "tts", "params": {"text": "Thanks for calling Bayview Taxi. Say booking or dispatch."}}], + collect={"speech": {"hints": ["booking", "dispatch"], "language": "en-US"}}, + ) + event = await action.wait() + result = event.params.get("result", {}) + if result.get("type") != "no_input": + break + + heard = "" + if result.get("type") == "speech": + heard = result.get("params", {}).get("text", "").lower() + + if "booking" in heard: + reply = "You said booking." + elif "dispatch" in heard: + reply = "You said dispatch." + elif result.get("type") == "no_input": + reply = "I didn't hear anything. Goodbye." + else: + reply = "I didn't understand that. Goodbye." + + async def hang_up_after_playback(_event): + if call.state != "ended": + await call.hangup() + + await call.play( + [{"type": "tts", "params": {"text": reply}}], + on_completed=hang_up_after_playback, + ) + await call.wait_for_ended() + + +client.run() +``` + + +```typescript +// Install: npm install @signalwire/sdk +// Save as collect-input-relay.mts and run: npx tsx collect-input-relay.mts +import { RelayClient } from "@signalwire/sdk"; + +const client = new RelayClient({ + project: "", + token: "", + host: ".signalwire.com", + contexts: ["default"], +}); + +type CollectResult = { type?: string; params?: { text?: string } }; + +client.onCall(async (call) => { + await call.answer(); + + let result: CollectResult = {}; + for (let attempt = 0; attempt < 2; attempt++) { + const action = await call.playAndCollect( + [{ type: "tts", text: "Thanks for calling Bayview Taxi. Say booking or dispatch." }], + { speech: { hints: ["booking", "dispatch"], language: "en-US" } }, + ); + const event = await action.wait(); + result = (event.params.result ?? {}) as CollectResult; + if (result.type !== "no_input") break; + } + + const heard = result.type === "speech" ? (result.params?.text ?? "").toLowerCase() : ""; + + let reply = "I didn't understand that. Goodbye."; + if (heard.includes("booking")) reply = "You said booking."; + else if (heard.includes("dispatch")) reply = "You said dispatch."; + else if (result.type === "no_input") reply = "I didn't hear anything. Goodbye."; + + await call.play([{ type: "tts", text: reply }], { + onCompleted: async () => { + if (call.state !== "ended") await call.hangup(); + }, + }); + await call.waitForEnded(); +}); + +await client.run(); +``` + + + +#### Ask again when the caller says nothing via Call Flow Builder + +A counter bounds the loop. Route the **Gather Input** node's **No Input** connector to a +[**Set Variables**][cfb-set-variables] node that sets `count` to `%{count} + 1`, then to a +[**Conditions**][cfb-conditions] node with the condition `%{count} > 1`. Connect the condition's +connector to a **Hangup Call** node, and **Else** to a **Play Audio or TTS** node that says "I +didn't hear anything." and leads back to **Gather Input**. The Gather Input reference builds the +same flow in [Input with infinite loop prevention][cfb-gather-loop]. + +## Next steps + + + + + Pick a voice for the questions, and speak on a call in progress over REST. + + + + Connect the branch the caller chose to a phone, and play a message when nobody answers. + + + + Point a phone number at this flow, or place the call yourself and run the flow once the + destination answers. + + + + Every node the visual editor offers, including request, recording, and AI agent nodes. + + + diff --git a/fern/products/platform/pages/calling/voice/overview.mdx b/fern/products/platform/pages/calling/voice/overview.mdx index d74f13ea69..161ea1cad1 100644 --- a/fern/products/platform/pages/calling/voice/overview.mdx +++ b/fern/products/platform/pages/calling/voice/overview.mdx @@ -21,6 +21,9 @@ Whether building a UCaaS solution, modernizing a legacy IVR, augmenting CX with Pick SWML, Relay, or the browser, then call your phone and answer a call to your number + + Ask a question, collect speech or key presses, and branch on the answer with SWML, Relay, or Call Flow Builder + Learn whether a person, voicemail, or fax machine is on a call you place or receive, and act on it @@ -178,7 +181,7 @@ Yes. Speak text from [SWML](/docs/swml/reference/calling/play), from a [Relay cl -Speech to text is available through [SWML's prompt method](/docs/swml/reference/prompt) or you can collect speech and transcribe with Node.js using the [Relay SDK](/docs/server-sdks/reference/python/relay/call/collect). +Yes. Collect what a caller says with [SWML's `prompt` method](/docs/swml/reference/calling/prompt), a Relay client, or the REST Calling API; the [Collect caller input](/docs/platform/voice/collect-input) guide covers each of them and shows how to branch on the reply. 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 deleted file mode 100644 index 3be43bf259..0000000000 --- a/fern/products/swml/pages/guides/recipes/creating-ivr-with-swml.mdx +++ /dev/null @@ -1,527 +0,0 @@ ---- -id: 80fec1fc-b153-474f-801b-8cd4aecb1566 -slug: guides/ivr -title: Simple IVR -description: Learn how to create a simple IVR with SWML. -x-custom: - tags: - - favorite - - sdk:swml - - product:voice -max-toc-depth: 3 ---- - -In this tutorial, we will demonstrate how to build a Simple IVR (Interactive Voice Response) using SWML (SignalWire Markup Language). -SWML enables you to create Relay applications using a descriptive format without the need for additional infrastructure like your own server. - -## Getting started - -Start with the [inbound calling guide](/docs/platform/voice/make-and-receive-calls#answer-a-call-via-swml) -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). - -In this guide we will create an IVR with SWML that takes a prompt from the user and directs callers to the appropriate resource. - - - - - -```yaml -version: 1.0.0 -sections: - main: - - answer: {} - - play: - urls: - - >- - say:Welcome to the Vance Refrigerator Corporation. For Quality - assurance this call maybe recorded - - record_call: - stereo: true - format: wav - - prompt: - play: >- - say:Press 1 to talk to sales, Press 2 to talk to Support, Press 3 to - talk to a Customer Representative - say_language: en-US - max_digits: 1 - speech_hints: - - one - - two - - three - - switch: - variable: prompt_value - case: - '1': - - execute: - dest: sales - '2': - - execute: - dest: support - '3': - - execute: - dest: customer - one: - - execute: - dest: sales - two: - - execute: - dest: support - three: - - execute: - dest: customer - default: - - execute: - dest: sales - sales: - - play: - url: 'say:You have reached sales' - - connect: - to: +1XXXXXXXXXX - support: - - play: - url: 'say:You have reached support' - - connect: - to: +1YYYYYYYYYY - customer: - - play: - url: 'say:You have reached a Customer representative' - - connect: - to: +1ZZZZZZZZZZ -``` - - -```json -{ - "version": "1.0.0", - "sections": { - "main": [ - { - "answer": {} - }, - { - "play": { - "urls": [ - "say:Welcome to the Vance Refrigerator Corporation. For Quality assurance this call maybe recorded" - ] - } - }, - { - "record_call": { - "stereo": true, - "format": "wav" - } - }, - { - "prompt": { - "play": "say:Press 1 to talk to sales, Press 2 to talk to Support, Press 3 to talk to a Customer Representative", - "say_language": "en-US", - "max_digits": 1, - "speech_hints": [ - "one", - "two", - "three" - ] - } - }, - { - "switch": { - "variable": "prompt_value", - "case": { - "1": [ - { - "execute": { - "dest": "sales" - } - } - ], - "2": [ - { - "execute": { - "dest": "support" - } - } - ], - "3": [ - { - "execute": { - "dest": "customer" - } - } - ], - "one": [ - { - "execute": { - "dest": "sales" - } - } - ], - "two": [ - { - "execute": { - "dest": "support" - } - } - ], - "three": [ - { - "execute": { - "dest": "customer" - } - } - ] - }, - "default": [ - { - "execute": { - "dest": "sales" - } - } - ] - } - } - ], - "sales": [ - { - "play": { - "url": "say:You have reached sales" - } - }, - { - "connect": { - "to": "+1XXXXXXXXXX" - } - } - ], - "support": [ - { - "play": { - "url": "say:You have reached support" - } - }, - { - "connect": { - "to": "+1YYYYYYYYYY" - } - } - ], - "customer": [ - { - "play": { - "url": "say:You have reached a Customer representative" - } - }, - { - "connect": { - "to": "+1ZZZZZZZZZZ" - } - } - ] - } -} -``` - - - - - -To put this script into action right away, simply copy it into a [new SWML script](https://my.signalwire.com?page=relay-bins/new), -replace placeholders with your information, and save it with a descriptive name so you can assign a phone number to run it. - -Below, we will dissect each section and method of this script to gain a clear understanding of its purpose and functionality. - -### Sections - -Each SWML script comprises a "sections" field, which serves as a mapping of named sections for execution. -The initial execution always begins with the "main" section. -It is essential to include a "main" section in the document at all times. - -Firstly, we "answer" the call. -Then, we proceed by using the [`play`](/docs/swml/reference/play) method to welcome our customer and provide them with a simple menu to choose from. -Afterward, we initiate the call recording using [`record_call`](/docs/swml/reference/record-call). - - - -```yaml -version: 1.0.0 -sections: - main: - - answer: {} - - play: - urls: - - >- - say:Welcome to the Vance Refrigerator Corporation. For Quality - assurance this call maybe recorded - - record_call: - stereo: true - format: wav -``` - - -```json -{ - "version": "1.0.0", - "sections": { - "main": [ - { - "answer": {} - }, - { - "play": { - "urls": [ - "say:Welcome to the Vance Refrigerator Corporation. For Quality assurance this call maybe recorded" - ] - } - }, - { - "record_call": { - "stereo": true, - "format": "wav" - } - } - ] - } -} -``` - - - -### Gather Input - -Still in the "main" section, we use the [`prompt`](/docs/swml/reference/prompt) method to collect digits from the end user: -`1` for Sales, `2` for Support and `3` for a Customer representative. - - - -```yaml -- prompt: - play: >- - say:Press 1 to talk to sales, Press 2 to talk to Support, Press 3 to talk - to a Customer Representative - say_language: en-US - max_digits: 1 - speech_hints: - - one - - two - - three -``` - - -```json -[ - { - "prompt": { - "play": "say:Press 1 to talk to sales, Press 2 to talk to Support, Press 3 to talk to a Customer Representative", - "say_language": "en-US", - "max_digits": 1, - "speech_hints": [ - "one", - "two", - "three" - ] - } - } -] -``` - - - -### Conditional Handling - -SWML provides you with ability to use conditional statements with the result from `prompt`. -In this final part of the "main" section, we use [`switch`](/docs/swml/reference/switch) to direct a caller to Sales in the case of a response of 1, Support for a response of 2, and a connects to a representative in the case of a response of 3. -Within each switch case, we use the [`execute`](/docs/swml/reference/execute) method to execute another section of our script. - - - -```yaml -- switch: - variable: prompt_value - case: - '1': - - execute: - dest: sales - '2': - - execute: - dest: support - '3': - - execute: - dest: customer - one: - - execute: - dest: sales - two: - - execute: - dest: support - three: - - execute: - dest: customer - default: - - execute: - dest: sales -``` - - -```json -[ - { - "switch": { - "variable": "prompt_value", - "case": { - "1": [ - { - "execute": { - "dest": "sales" - } - } - ], - "2": [ - { - "execute": { - "dest": "support" - } - } - ], - "3": [ - { - "execute": { - "dest": "customer" - } - } - ], - "one": [ - { - "execute": { - "dest": "sales" - } - } - ], - "two": [ - { - "execute": { - "dest": "support" - } - } - ], - "three": [ - { - "execute": { - "dest": "customer" - } - } - ] - }, - "default": [ - { - "execute": { - "dest": "sales" - } - } - ] - } - } -] -``` - - - -### Connecting Call - -After the user makes a selection, they are directed to dedicated sections **sales**, **support**, or **customer**, triggering an outbound call to the corresponding department or agent. -This seamless integration is achieved through the [`connect`](/docs/swml/reference/connect) method, where the "to" parameter is the department or agent's phone number. -You will want to either replace these values in your script or add environmental variables. - - - -```yaml -sales: - - play: - url: say:You have reached sales - - connect: - to: "+1XXXXXXXXXX" -support: - - play: - url: say:You have reached support - - connect: - to: "+1YYYYYYYYYY" -customer: - - play: - url: say:You have reached a Customer representative - - connect: - to: "+1ZZZZZZZZZZ" -``` - - -```json -{ - "sales": [ - { - "play": { - "url": "say:You have reached sales" - } - }, - { - "connect": { - "to": "+1XXXXXXXXXX" - } - } - ], - "support": [ - { - "play": { - "url": "say:You have reached support" - } - }, - { - "connect": { - "to": "+1YYYYYYYYYY" - } - } - ], - "customer": [ - { - "play": { - "url": "say:You have reached a Customer representative" - } - }, - { - "connect": { - "to": "+1ZZZZZZZZZZ" - } - } - ] -} -``` - - - -### Testing - -To put our creation to the test, make sure your script is saved to your Resource, or your SWML scripts with a recognizable name. -Then we'll purchase a phone number from our [SignalWire Space](https://my.signalwire.com?page=phone_numbers/new) to run it. -Follow the [Buying a Phone Number](/docs/platform/phone-numbers) if you need help. -Once you have your phone number, we can assign our newly crafted SWML Script resource to handle incoming calls for that number. - - - -![Assign resource to phone number.](/assets/images/dashboard/phone-numbers/assign-resource-voice.png) - - - - - -In the Dashboard, open **Phone Numbers** > **Purchased**, select the number, and open its edit page. Under **Inbound Call Settings**, select **+ Assign Resource**, choose the IVR SWML script, and save. - - - -Upon calling the assigned phone number, our script will execute, and you should hear the instructions running as intended. -This exciting step will allow us to experience firsthand the interactive capabilities of our SWML-based IVR system. -In just a matter of minutes, we have successfully crafted a basic IVR system using SWML script. -This powerful yet straightforward solution opens up a world of possibilities for enhancing customer interactions and streamlining communication processes. -With SWML's versatility, you can now explore more advanced features and build even more sophisticated applications to suit your specific business needs. -The journey to creating efficient and interactive voice responses has just begun, so explore our options and craft your perfect solution today. - -# Reference - -- [SWML Technical Reference](/docs/swml) diff --git a/fern/redirects.yml b/fern/redirects.yml index eef366134b..7f7565106f 100644 --- a/fern/redirects.yml +++ b/fern/redirects.yml @@ -18,6 +18,9 @@ redirects: # The SWML forwarding recipe merged into the platform forward calls guide. - source: /docs/swml/guides/forward-calls destination: /docs/platform/voice/forward-calls + # The SWML IVR recipe merged into the platform collect caller input guide. + - source: /docs/swml/guides/ivr + destination: /docs/platform/voice/collect-input # Browser SDK outbound guide consolidated into the platform calling guide, # which now carries the browser walkthrough and media options. - source: /docs/browser-sdk/guides/outbound-calls