diff --git a/fern/docs.yml b/fern/docs.yml index 7ba739f4ad..8518b59f6d 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -186,6 +186,8 @@ css: - components/voice-widget/styles.css redirects: + - source: /docs/server-sdks/guides/call-recording + destination: /docs/platform/voice/call-recording - source: /docs/agents-sdk destination: /docs/server-sdks - source: /docs/agents-sdk/:slug* diff --git a/fern/products/platform/pages/ai/guides/chat.mdx b/fern/products/platform/pages/ai/guides/chat.mdx new file mode 100644 index 0000000000..6317611167 --- /dev/null +++ b/fern/products/platform/pages/ai/guides/chat.mdx @@ -0,0 +1,286 @@ +--- +title: AI chat +sidebar-title: AI Chat +slug: /ai/chat +description: Reach a SignalWire AI agent over HTTP instead of a phone call +subtitle: AI Agents over HTTP text conversation +max-toc-depth: 3 +--- + +[chat-endpoint]: /docs/apis/rest/ai-chat/chat-methods +[error-codes]: /docs/apis/error-codes +[chat-client]: /docs/server-sdks/reference/python/agents/ai-chat-client +[chat-gateway]: /docs/server-sdks/reference/python/agents/chat-gateway +[gateway-router]: /docs/server-sdks/reference/python/agents/chat-gateway/router +[visible-messages]: /docs/server-sdks/reference/python/agents/chat-gateway/visible-messages +[client-create]: /docs/server-sdks/reference/python/agents/ai-chat-client/create-conversation +[client-chat]: /docs/server-sdks/reference/python/agents/ai-chat-client/chat +[client-log]: /docs/server-sdks/reference/python/agents/ai-chat-client/log +[client-end]: /docs/server-sdks/reference/python/agents/ai-chat-client/end +[swaig-webhook]: /docs/apis/rest/webhooks/ai-swaig-tool-webhook +[post-prompt-webhook]: /docs/apis/rest/webhooks/ai-post-prompt-callback +[tool-calling]: /docs/platform/ai/tool-calling +[ai-reference]: /docs/swml/reference/calling/ai +[api-scopes]: /docs/platform/your-signalwire-api-space +[messaging-chat]: /docs/platform/chat +[webhooks]: /docs/platform/webhooks +[messaging-swml]: /docs/swml/reference/messaging +[swml-reply]: /docs/swml/reference/messaging/reply +[inbound-message-webhook]: /docs/apis/rest/webhooks/inbound-message-webhook + +The AI agents made on the Server SDK or with SWML can be accessed by a text conversation, in addition to voice conversations. This allows +an easier fallback in case voice is inconvenient, as well as programmatic access to the AI agents. AI chat happens over HTTP, with +a chat gateway which allows you to use it securely from the browser. + + +AI chat is the [`ai`][ai-reference] method or Server SDK agents reached over text instead of voice. + +The [Chat API][messaging-chat] is an unrelated messaging product. And it is not the messaging flavor of SWML either. + + +## Basic first example + +Consider the following simple AI agent, saved as a hosted script resource. + +```json title="agent.swml.json" +{ + "version": "1.0.0", + "sections": { + "main": [ + { "answer": {} }, + { + "ai": { + "prompt": { + "text": "You are Ada, the dispatcher for Bayview Taxi. Answer questions about booking a ride. Keep replies to one or two sentences." + } + } + } + ] + } +} +``` + +Once saved, you will get a request URL like `(space name).signalwire.com/relay-bins/(uuid)`. +On top of testing this agent by voice with the Click-to-Test button, you can also hold conversations with it over HTTP. + + + +`create_conversation` opens the conversation against your agent and hands back its opening line. + +```json title="create_conversation" +{ + "jsonrpc": "2.0", + "id": "req-1", + "method": "create_conversation", + "params": { + "id": "chat-demo-1", + "config_url": "https://your-space.signalwire.com/relay-bins/8f3c1d94-2b7a-4e15-9c60-a1d8e4f70b23" + } +} +``` + +```json title="response" +{ + "jsonrpc": "2.0", + "result": { + "status": "created", + "id": "chat-demo-1", + "initial_message": "Hello! Welcome to Bayview Taxi. How can I assist you with booking a ride today?" + }, + "id": "req-1" +} +``` + +Every turn after that is the same request with `chat` as the method, and the reply comes back in +the same response. + +```json title="chat" +{ + "jsonrpc": "2.0", + "id": "req-2", + "method": "chat", + "params": { + "id": "chat-demo-1", + "message": "I need a ride to the airport." + } +} +``` + +```json title="response" +{ + "jsonrpc": "2.0", + "result": { + "response": "Sure! Which airport are you heading to, and what time do you need the ride?" + }, + "id": "req-2" +} +``` + +This is all it takes to text with your AI agent. Howerver, since this is directly over HTTP and uses your project API key, it can't be safely directly served on the browser without a proxy. + +## Connection with the AI Chat + +| | Direct | Through a gateway | +|---|---|---| +| Your code runs | on your server | in a browser, or any untrusted client | +| Holds the API token | yes | no — your gateway does | +| Talks to | `POST /api/ai/chat` | your own gateway URL | +| Protocol | JSON-RPC 2.0 | a small POST shape | +| Python helper | [`AIChatClient`][chat-client] | [`ChatGateway`][chat-gateway] | + +A browser cannot use the direct path. Anything a page can read, a visitor can read, and an API +token in a page is a published API token. + +There is a third way in that sits on top of the direct path rather than beside it: +[from a phone number](#from-a-phone-number), where someone texts your number and the agent texts +back. Your server still holds the token. + +## How a conversation works + +**Your agent** is the SWML document you serve. `config_url` is where you serve it. + +**A conversation** is a series of turns, addressed by an `id` you choose. Ids are scoped to your +project. + +**A turn** is one user message and the agent's reply, including any tool calls made along the way. +One request, one turn, one response — and it takes seconds, not milliseconds. + +## From your server + +The [AI chat endpoint reference][chat-endpoint] documents all six methods, their parameters, and +their error codes. In Python, [`AIChatClient`][chat-client] covers the same ground with typed +methods and typed exceptions. + +### AI chat methods + +| Method | What it does | Worth knowing | +|---|---|---| +| [`create_conversation`][client-create] | Creates a conversation, or resets one with `reinit` | Returns `initial_message`, so the agent speaks first | +| [`chat`][client-chat] | Sends a message, returns the reply | Passing `config_url` here creates the conversation in one call, but you never get `initial_message` | +| [`chat_log`][client-log] | Reads the conversation back | Changes nothing | +| `summarize` | Summarizes on demand | One call per minute per conversation; over that returns `-32005` | +| [`end_conversation`][client-end] | Ends it and starts post-processing | Triggers the summary and your `post_prompt_url` | +| `delete` | Removes the conversation and its messages | Nothing is post-processed and no webhook fires | + +Messages take a `role` of `user` or `system`. A `system` message steers the agent without appearing +as something the user said. + +## Accessing AI chat from a browser + +You run a **gateway**: [`ChatGateway`][chat-gateway] with the Server SDK, mounted inside a web application you already +host. It holds the API token, the project, and the `config_url`. The page holds a URL and a +publishable key that's managed by the gateway itself. + +```mermaid +flowchart LR + Browser["Browser
gateway URL + publishable key"] + Gateway["Your app
ChatGateway"] + SW["SignalWire
POST /api/ai/chat"] + + Browser -->|"publishable key"| Gateway + Gateway -->|"project + API token"| SW + SW -->|"the agent's turn"| Gateway + Gateway -->|"JSON response"| Browser +``` + +A key lifted from your page reaches one agent and can do nothing else. It cannot name a different +`config_url`, because the gateway injects that itself and never accepts it from the request. + +### The agent document + +This is either a SWML document you're hosting, or a Server SDK agent. + +### The gateway + +[`ChatGateway`][chat-gateway] mounts on any FastAPI app, and allows for safe browser-side access. + +```python title="server.py" +from signalwire.ai_chat import ChatGateway + +gateway = ChatGateway( + config_url=CONFIG_URL, # never leaves the server + key=os.environ["SIGNALWIRE_CHAT_GATEWAY_KEY"], # safe in the page + secret=os.environ["SIGNALWIRE_CHAT_GATEWAY_SECRET"], # signs handles + allowed_origins=["https://bayviewtaxi.example.com"], +) + +app.include_router(gateway.router(), prefix="/chat") +``` + +### The browser side + +One endpoint, [four methods][gateway-router]. The router adds a `POST /`, so with `prefix="/chat"` +the path is `/chat/`. The key is a bearer token that automatically gets sent. + +| Method | Body | Returns | +|---|---|---| +| `start` | `{"method": "start"}` | `greeting`, `status`, `timeout`, plus an `X-Chat-Handle` header | +| `chat` | `{"method": "chat", "handle", "message"}` | the service's JSON-RPC envelope | +| `log` | `{"method": "log", "handle"}` | `messages`, `timeout`, `last_activity` | +| `end` | `{"method": "end", "handle"}` | `{"status": "ended"}` | + +## From a phone number + +We can use the Server SDK to create a messaging variant SWML which replies to text conversations using the AI agent. + +```python title="sms.py" +@app.post("/sms") +async def sms(request: Request): + message = (await request.json())["message"] + + reply = await client.chat( + f"sms-{message['from']}", message["body"], config_url=CONFIG_URL + ) + + return {"version": "1.0.0", "sections": {"main": [{"reply": reply.text}]}} +``` + +## Webhooks you receive + +A chat conversation delivers the same webhooks a voice AI session does. + +**Tool calls** reach the `web_hook_url` on your SWAIG functions, exactly as they do on a call. +[Tool calling][tool-calling] covers the round trip, and the [AI SWAIG tool webhook][swaig-webhook] +documents the payload. Nothing about the reply shape changes for chat. + +**A summary** reaches your `post_prompt_url` after the conversation ends, when your SWML sets one. +Not instantly — expect seconds. The [AI post-prompt callback][post-prompt-webhook] documents the +payload. General webhook handling is covered under [webhooks][webhooks]. + +{/* TODO(validate): confirm and then state the values that identify a chat session — + `conversation_type` and what `call_id` carries. Those are what let one handler serve both. */} + + +`chat_log` and the post-prompt `call_log` can include your prompt and other non-dialogue entries. +Filter to `user` and `assistant` before showing either to anyone. The gateway's `log` already does. + + +## Next steps + + + + + Every method, parameter, return field, and error code on the wire. + + + + The Python client for the direct path, with every method and return type. + + + + The browser-facing gateway, with every argument, default, and cap. + + + + Connect the agent to your systems, the same way a voice agent does. + + + + Shape how the agent converses before you change any code. + + + + The SWML the agent is built from, shared with voice. + + + diff --git a/fern/products/platform/pages/calling/voice/call-recording.mdx b/fern/products/platform/pages/calling/voice/call-recording.mdx new file mode 100644 index 0000000000..2ee79fbf93 --- /dev/null +++ b/fern/products/platform/pages/calling/voice/call-recording.mdx @@ -0,0 +1,728 @@ +--- +title: Call recording +slug: /voice/call-recording +description: Learn to record calls on SignalWire, and to access and manage the recordings. +subtitle: How to record calls, and how to access and manage them +max-toc-depth: 3 +--- + +[record]: /docs/swml/reference/calling/record +[record-call]: /docs/swml/reference/calling/record-call +[stop-record-call]: /docs/swml/reference/calling/stop-record-call +[join-conference]: /docs/swml/reference/calling/join-conference +[connect]: /docs/swml/reference/calling/connect +[swml-recipe]: /docs/swml/guides/record-calls +[swml-calls]: /docs/swml/guides/make-and-receive-calls +[swml-quickstart]: /docs/swml/guides +[sdk-quickstart]: /docs/server-sdks/guides/quickstart +[phone-numbers]: /docs/platform/phone-numbers +[call-commands]: /docs/apis/rest/calls/call-commands +[paging]: /docs/apis/paging +[sdk-install]: /docs/server-sdks/guides/installation +[py-record-call]: /docs/server-sdks/reference/python/agents/function-result/record-call +[py-stop-record-call]: /docs/server-sdks/reference/python/agents/function-result/stop-record-call +[py-relay-record]: /docs/server-sdks/reference/python/relay/call/record +[py-record-action]: /docs/server-sdks/reference/python/relay/actions/record-action +[ts-record-call]: /docs/server-sdks/reference/typescript/agents/function-result/record-call +[ts-stop-record-call]: /docs/server-sdks/reference/typescript/agents/function-result/stop-record-call +[ts-relay-record]: /docs/server-sdks/reference/typescript/relay/call/record +[ts-record-action]: /docs/server-sdks/reference/typescript/relay/actions/record-action +[cxml-record]: /docs/compatibility-api/cxml/reference/voice/record +[cxml-dial]: /docs/compatibility-api/cxml/reference/voice/dial +[cxml-conference]: /docs/compatibility-api/cxml/reference/voice/conference +[rest-list]: /docs/apis/rest/recordings/list-call-recordings +[rest-get]: /docs/apis/rest/recordings/get-call-recording +[rest-delete]: /docs/apis/rest/recordings/delete-call-recording +[compat-list]: /docs/compatibility-api/rest/recordings/list-recordings +[compat-retrieve]: /docs/compatibility-api/rest/recordings/retrieve-recording +[compat-update]: /docs/compatibility-api/rest/recordings/update-recording +[compat-delete]: /docs/compatibility-api/rest/recordings/delete-recording +[recording-status-callback]: /docs/compatibility-api/rest/recordings/webhooks/recording-status-callback +[cfb-start]: /docs/call-flow-builder/reference/start-call-recording +[cfb-stop]: /docs/call-flow-builder/reference/stop-call-recording +[cfb-voicemail]: /docs/call-flow-builder/reference/voicemail-recording +[media-protection]: /docs/platform/media-protection +[compliance]: /docs/platform/compliance +[webhooks]: /docs/platform/webhooks +[browser-recording]: /docs/browser-sdk/v4/reference/webrtc-call/recording +[ai-analytics]: /docs/platform/ai/analytics +[post-prompt-webhook]: /docs/apis/rest/webhooks/ai-post-prompt-callback +[ai-method]: /docs/swml/reference/calling/ai +[ai-sidecar]: /docs/swml/reference/calling/ai-sidecar +[transcribe]: /docs/swml/reference/calling/transcribe +[live-transcribe]: /docs/swml/reference/calling/live-transcribe + +There are several ways to make calls on SignalWire, and there are several channels over which calls can be made. So there are +multiple ways to record calls based on the product, the channel, the resource you are using, and your exact use case. + +All recordings are stored on the same Space storage, and can be managed with a uniform API. + +- [SWML](#swml): + - [In the background](#recording-in-the-background): simple full conversation recording + - [Record fragments](#recording-a-single-utterance): for voicemail and prompts. The script + blocks and records until the user stops the recording with a digit, a timeout or a set period of silence + - [A conference](#recording-a-conference): to capture a conference call from the moment you join +- [Server SDKs](#server-sdks) — the SDKs emit SWML, so every SWML recording option is available under a slightly different API: + - [From an AI agent](#recording-from-an-ai-agent): `record_call` chained onto a tool handler's result, so + the agent can start recording once the caller consents + - [From a call handler](#recording-from-a-call-handler): `call.record`, which hands back an action + you can pause, resume, stop, or wait on +- [Call Flow Builder](#call-flow-builder) — drag-and-drop nodes to record calls +- [Using the Compatibility API](#already-using-the-compatibility-api) + +{/* v4 browser sdk can't record yet */} + +Once you are recording, three sections cover what to do with the result: + +- [Recording options](#recording-options) — format, which side of the call you capture, mono versus + dual-channel, and what stops, pauses, or resumes a recording +- [Browse recordings](#browse-recordings) — the API, the Dashboard, and the status callback that + tells you a file is ready +- [Storage and retention](#storage-and-retention) — what recordings cost you in space, and how long + to keep them + +If you have not learnt to place calls yet, the guides below will walk you through making your first call. + +- [Making and receiving phone calls][swml-calls] routes a number to a SWML script and answers it. +- [Your first agent][sdk-quickstart] does the same from the Server SDKs. +- [Phone numbers][phone-numbers] covers buying a number and pointing it at a resource. + +## Record a call + +### SWML + +SWML has two different modes of recording: +`record_call` records the call in the background, whereas `record` waits and records a fragment from the user (for example, to record voicemail). +A conference recording can be enabled by setting an option while joining. + +Server SDKs and Call Flow builder offer the same functionality under different APIs. + +#### Recording in the background + +[`record_call`][record-call] starts a recording and returns immediately. +[`stop_record_call`][stop-record-call] ends it, matched by `control_id`. + + + +```yaml +version: 1.0.0 +sections: + main: + - answer: {} + - record_call: + control_id: main + format: mp3 + direction: both + stereo: true + - play: + url: 'say:This call is being recorded.' + - stop_record_call: + control_id: main +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "answer": {} }, + { + "record_call": { + "control_id": "main", + "format": "mp3", + "direction": "both", + "stereo": true + } + }, + { "play": { "url": "say:This call is being recorded." } }, + { "stop_record_call": { "control_id": "main" } } + ] + } +} +``` + + + +The `control_id` attribute is optional. If not explicitly provided, an auto-generated id is stored in the `record_control_id` variable +which `stop_record_call` can target. But setting it yourself is simpler in general. + +The method also sets SWML variables `record_call_url` (the URL of the new recording) and `record_call_result` +(`success` or `failed`). You can check `record_call_result` to ensure the recording started successfully. + +You do not need `stop_record_call` for a full call recording, as the recording stops automatically on hangup. Only use it to stop early. + +#### Recording a single utterance + +[`record`][record] is a blocking method which waits until the recording ends. The SWML execution resumes once the +caller stops talking, presses a terminator digit, or hits a timeout. This enables you to record a small section for a prompt or a +voicemail application. + + + +```yaml +version: 1.0.0 +sections: + main: + - answer: {} + - play: + url: 'say:Leave a message after the beep. Press hash when you are done.' + - record: + beep: true + end_silence_timeout: 3 + - play: + url: 'say:Recording ${record_result}. Here is what we got:' + - play: + url: '${record_url}' +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "answer": {} }, + { "play": { "url": "say:Leave a message after the beep. Press hash when you are done." } }, + { "record": { "beep": true, "end_silence_timeout": 3 } }, + { "play": { "url": "say:Recording ${record_result}. Here is what we got:" } }, + { "play": { "url": "${record_url}" } } + ] + } +} +``` + + + +`record` sets `record_url` and `record_result`, which the script above plays straight back. Note the +names differ from the background method: `record_url`, not `record_call_url`. + +By default, `record` captures the `speak` side: what the caller is saying. Its default terminator +is `#`, so the user can press that key to stop the recording. + +#### Recording a conference + +Set the attribute `record` on +[`join_conference`][join-conference] to `record-from-start` to record the conference. + + +```yaml +version: 1.0.0 +sections: + main: + - answer: {} + - join_conference: + name: standup + record: record-from-start + recording_status_callback: https://example.com/recording-status +``` + + +```json +{ + "version": "1.0.0", + "sections": { + "main": [ + { "answer": {} }, + { + "join_conference": { + "name": "standup", + "record": "record-from-start", + "recording_status_callback": "https://example.com/recording-status" + } + } + ] + } +} +``` + + + +A conference status callbacks are named: +`recording_status_callback`, `recording_status_callback_method`, +`recording_status_callback_event`, and `recording_status_callback_event_type`. +See [Status callback when a recording is ready](#status-callback-when-a-recording-is-ready). + +### Server SDKs + +The Server SDKs emit the SWML above, so every option and default described on this page carries over. +Only the naming changes, and there are two entry points depending on how your application handles the +call. + +[Install the SDK][sdk-install] on your codebase as follows: + + + +```bash +pip install signalwire-sdk +``` + + +```bash +npm install @signalwire/sdk +``` + + + +| What you want | Python | TypeScript | +|---|---|---| +| Start a background recording from an AI tool | [`record_call()`][py-record-call] | [`recordCall()`][ts-record-call] | +| Stop it | [`stop_record_call()`][py-stop-record-call] | [`stopRecordCall()`][ts-stop-record-call] | +| Record from a call handler and wait for the file | [`call.record()`][py-relay-record] | [`call.record()`][ts-relay-record] | + +#### Recording from an AI agent + +An AI agent starts a recording from inside a tool handler, by chaining `record_call` onto the +`FunctionResult` the handler returns. The model decides when to call the tool, so this can be used to +only record if the caller has agreed. + + + +```python {17-23} +from signalwire import AgentBase, FunctionResult + +agent = AgentBase(name="recording-agent", route="/agent") +agent.add_language("English", "en-US", "rime.spore") + +agent.prompt_add_section( + "Recording policy", + body="Say 'This call may be recorded for quality purposes.' and ask the caller " + "whether they agree. Call start_recording only after they say yes. " + "If they decline, continue the call without recording.", +) + +@agent.tool(name="start_recording", description="Start recording the call once the caller agrees") +def start_recording(args, raw_data): + return ( + FunctionResult("Recording has started.") + .record_call( + control_id="main", + stereo=True, + format="mp3", + status_url="https://example.com/recording-status", + ) + ) + +@agent.tool(name="stop_recording", description="Stop recording the call") +def stop_recording(args, raw_data): + return FunctionResult("Recording has stopped.").stop_record_call(control_id="main") + +if __name__ == "__main__": + agent.run() +``` + + +```typescript {18-23} +import { AgentBase, FunctionResult } from '@signalwire/sdk'; + +const agent = new AgentBase({ name: 'recording-agent', route: '/agent' }); +agent.addLanguage({ name: 'English', code: 'en-US', voice: 'rime.spore' }); + +agent.promptAddSection('Recording policy', { + body: + "Say 'This call may be recorded for quality purposes.' and ask the caller " + + 'whether they agree. Call start_recording only after they say yes. ' + + 'If they decline, continue the call without recording.', +}); + +agent.defineTool({ + name: 'start_recording', + description: 'Start recording the call once the caller agrees', + parameters: { type: 'object', properties: {} }, + handler: () => + new FunctionResult('Recording has started.').recordCall({ + controlId: 'main', + stereo: true, + format: 'mp3', + statusUrl: 'https://example.com/recording-status', + }), +}); + +agent.defineTool({ + name: 'stop_recording', + description: 'Stop recording the call', + parameters: { type: 'object', properties: {} }, + handler: () => new FunctionResult('Recording has stopped.').stopRecordCall('main'), +}); + +await agent.run(); +``` + + + + + +#### Recording from a call handler + +Outside the AI agent, a Relay call handler records with `call.record()` +([Python][py-relay-record], [TypeScript][ts-relay-record]), which returns a `RecordAction` +([Python][py-record-action], [TypeScript][ts-record-action]). The action handle is the difference +that matters: `wait()` blocks until the recording reaches a terminal state and hands you the file's +`url`, `duration`, and `size`, so a voicemail flow needs no callback at all. + + + +```python {15-16} +from signalwire.relay import RelayClient + +client = RelayClient( + project="your-project-id", + token="your-api-token", + host="your-space.signalwire.com", + contexts=["default"], +) + +@client.on_call +async def handle_call(call): + await call.answer() + await call.play([{"type": "tts", "params": {"text": "Leave a message after the beep."}}]) + + action = await call.record(audio={"beep": True, "end_silence_timeout": 3, "terminators": "#"}) + event = await action.wait() + + print(f"Recording saved: {event.params.get('record', {}).get('url', '')}") + await call.hangup() + +client.run() +``` + + +```typescript {13-14} +import { RelayClient } from '@signalwire/sdk'; + +const client = new RelayClient({ + project: process.env.SIGNALWIRE_PROJECT_ID!, + token: process.env.SIGNALWIRE_API_TOKEN!, + contexts: ['default'], +}); + +client.onCall(async (call) => { + await call.answer(); + await call.play([{ type: 'tts', text: 'Leave a message after the beep.' }]); + + const action = await call.record({ beep: true, end_silence_timeout: 3, terminators: '#' }); + const event = await action.wait(); + + console.log(`Recording saved: ${event.params.record?.url ?? ''}`); + await call.hangup(); +}); + +await client.run(); +``` + + + +`RecordAction` also carries `stop()`, `pause()`, and `resume()` — see +[Pause and resume](#pause-and-resume). + +### Call Flow Builder + +Call Flow Builder provides three nodes to manage recordings: [Start Call Recording][cfb-start] +records both sides in the background and stops automatically if the call disconnects, +[Stop Call Recording][cfb-stop] stops a recording early, and +[Voicemail Recording][cfb-voicemail] records snippets for voicemail-like applications. + +The start node has the same options as SWML `record_call` — Recording Name, Stereo, Beep, +Terminators, and Format. It carries the same defaults too, so Terminators is empty unless you set +it; the values in the screenshot below are that flow's settings, not the node's defaults. + + + + +![A Call Flow Builder canvas: Handle Call connects to Answer Call, which connects to a Start Call Recording node. The node settings panel shows Recording Name "My Recording", Stereo enabled, Beep enabled, Terminators set to #, and Format set to WAV.](/assets/images/call-flow/nodes/start_recording.webp) + + + + + + +The flow runs Handle Call, then Answer Call, then Start Call Recording. The node's settings are +Recording Name (`My Recording`), Stereo (on), Beep (on), Terminators (`#`), and Format (`WAV`). + + + +Pairing the two nodes allows you to start and stop recording as required, just like with SWML, +and `%{record_call_url}` carries the recording URL into later nodes. + + + + +![A Call Flow Builder canvas: Start Call Recording named "Sales Call" leads to a Forward to Phone node with Success, No Answer, Busy, Decline, and Error branches. Every branch converges on a Stop Call Recording node, which leads to a Set Variables node assigning sales_recording_url to the value %{record_call_url}.](/assets/images/call-flow/nodes/record-call.webp) + + + + + + +Start Call Recording (named `Sales Call`) connects to Forward to Phone +(`sip:sales@example.com`). All five of that node's branches — Success, No Answer, Busy, Decline, +Error — converge on Stop Call Recording for the same named recording. That leads to a Set Variables +node assigning `sales_recording_url` the value `%{record_call_url}`. + + + +Format here offers wav and mp3 only, where SWML also takes mp4. + +## Recording options + +The recording options below apply to `record` and `record_call`, and the Server SDKs expose the same set under their +own naming — snake_case in Python, camelCase in TypeScript. The one difference is `format`: the Server SDKs accept +`wav` and `mp3` only. + +The defaults are different between the two SWML methods as they represent different use cases. + +| Option | `record` default | `record_call` default | What it changes | +|---|---|---|---| +| `format` | `wav` | `wav` | `wav`, `mp3`, or `mp4` | +| `direction` | `speak` | `both` | Which side of the conversation is captured | +| `stereo` | `false` | `false` | One mixed channel, or each party on their own | +| `beep` | `false` | `false` | Plays a tone before recording starts | +| `terminators` | `#` | none | DTMF digits that stop the recording | +| `input_sensitivity` | `44.0` | `44.0` | How readily voice activity is detected | +| `initial_timeout` | `4.0` | `0` | Seconds to wait for speech before giving up | +| `end_silence_timeout` | `5.0` | `0` | Seconds of silence that end the recording | +| `max_length` | none | none | Hard cap on recording length | +| `status_url` | none | none | Where record status events are delivered | + +### Choosing a format + +`wav` is uncompressed, so the file is large but lossless, and generally easier to process. Use it when the recording feeds +speech analytics or transcription, or when you may need to reprocess it years later. `mp3` is +compressed and roughly a tenth the size, at quality that is more than enough for human review and for +most transcription services. `mp4` carries a video track alongside the audio, so it is the format to +pick when you are recording a video call rather than a voice call. + +### Which side of the call you capture + +Set `direction` to `speak` to record what the caller says, or to `listen` for what they hear. +`record_call` also accepts `both`, for the whole conversation. `record` defaults to `speak` and +`record_call` defaults to `both`, in keeping with their most common use cases. + +### Mono and dual-channel + +By default both parties are mixed into one channel. Setting `stereo: true` puts each side on their +own channel. The caller is on the left and the agent (what the caller hears) is on the right. + +Dual-channel is useful for analytics and transcription. Unmixing the mono file is difficult to do reliably. Single mono-channel is +better if you're just keeping the call for compliance or QA reasons. + +### Stopping the recording + +A recording ends in one of four ways: + +1. when the call ends +2. when `stop_record_call` is called with the matching `control_id` +3. if the caller presses a `terminators` digit +4. when `max_length` or `end_silence_timeout` is reached + +`terminators` behaves differently across the two methods. +`record` defaults to `#`, and pressing it unblocks the rest of the script, whereas `record_call` +defaults to no terminators at all. The option exists, but setting it is a deliberate choice: it lets +the caller stop recording mid-call. Leave it unset to ensure recordings won't end abruptly from an +accidental key press. + +### Pause and resume + +You can pause and resume call recordings without stopping it entirely using either Relay commands or +a REST API invocation of the calling commands. +This can be used to not record sensitive information like card details. + +| Where you are | How to pause and resume | +|---|---| +| A Relay call handler | `pause()` and `resume()` on the `RecordAction` returned by `call.record()` ([Python][py-record-action], [TypeScript][ts-record-action]) | +| Any active call, from your own backend | The `calling.record.pause` and `calling.record.resume` [call commands][call-commands], targeting the recording's `control_id` | +| The Compatibility API | [Update a recording][compat-update] | + +SWML and the AI agent's `FunctionResult` have no pause. Calling `stop_record_call` and then +`record_call` again produces two separate recordings rather than one file with a gap. + +### Playing a beep + +`beep` is off by default and plays a tone before recording starts. It is the usual mechanical way to +signal that recording has begun, and in some jurisdictions a required one — see +[Recording and the law](#recording-and-the-law). + +## Browse recordings + +Based on how you created the recording, there are two places it can be stored: + +| Created by | Retrieve from | Operations | +|---|---|---| +| SWML, Server SDKs, Call Flow Builder | `/api/relay/rest/recordings` | [List][rest-list], [Get][rest-get], [Delete][rest-delete] | +| The Compatibility API | `/Accounts/{AccountSid}/Recordings` | See [Already using the Compatibility API](#already-using-the-compatibility-api) | + + + +A page of results comes back as a `data` array of recordings alongside a `links` object: + + + +Each record lists what you need to fetch and account for the file without a second call: `url` for +the media, `duration_in_seconds` and `byte_size` for the file itself, `stereo` and `track` for how it +was captured, and `price` with `price_unit` for what it cost. `status` tells you whether the file is +ready. A recording that failed carries an `error_code` instead of usable media. + +The rest of the object identifies where the recording came from. Every entry has a `project_id` and +timestamps, plus exactly one source reference — `relay_pstn_leg_id`, `relay_sip_leg_id`, or +`relay_webrtc_leg_id` for a call leg, and `relay_conference_id` for a conference. A leg that was part +of a conference carries both. + +The full field list is on [List call recordings][rest-list]. The `links` object beside `data` is how +you reach the pages after the first — see [Paging][paging]. + +### View recordings from your dashboard + +Recordings appear under **Storage** > **Recordings** in your SignalWire Space. Opening one shows its +status, the call it was recorded from, and a download link for each available format. + + + + +![The SignalWire Space sidebar with Storage expanded and Recordings selected. The detail pane shows a Recording with its ID, a status of Finished, the call it was recorded via, and a Files table offering Download (.wav) and Download (.mp3) links.](/assets/images/dashboard/sidebar/Call-Recording-Example-SWML.webp) + + + + + + +Recordings are listed under **Storage** > **Recordings** in your SignalWire Space. Selecting one +opens a detail view showing the recording ID, its status (for example `Finished`), the call it was +recorded from, and a Files table with a download link per available format. + + + +### Status callback when a recording is ready + +Rather than polling, you can set a callback URL that is notified when the recording is ready. That +can arrive slightly later than the recording or the call ending. Set `status_url` on `record` and +`record_call`, or `recording_status_callback` on `join_conference`. + +The payload delivered by `record_call` and `record` is a `calling.call.record` event whose +`params.state` carries one of `recording`, `paused`, `finished`, `no_input`, or `error`. + +On `finished`, the callback has a `params.record` object with the recording's `url`, `duration`, +`size`, and `format`. That is enough to store a reference without a follow-up API call. The full +payload is documented under +[status callbacks](/docs/swml/reference/calling/record-call#statuscallbacks). + +[Webhooks][webhooks] covers handling callbacks generally. + + + +## Storage and retention + +Recordings accumulate. A ten-minute mono MP3 runs about 2 to 3 MB, so a business handling a thousand +calls a day generates somewhere between 60 and 90 GB a month. Stereo roughly doubles that, and WAV +multiplies it by about ten. + +How long you keep them is usually decided for you: financial services commonly retain call recordings +for five to seven years, HIPAA-covered healthcare for six, and general business for one to two. +Training and QA libraries are the exception — keep what is useful and delete the rest. + +Build the cleanup into your application rather than leaving it to someone's calendar. List recordings +with [List call recordings][rest-list], filter by age, and remove them with +[Delete a call recording][rest-delete]. + +## Secure the media + + +Media URLs contain randomly generated UUIDs, which makes them impractical to guess, but anyone +holding the link can fetch one. Enable Media URL Protection to require your project's API +credentials instead — see [Media URL protection][media-protection]. + + +Protection is set per project and per media type, so recordings can be protected independently of +messages and faxes. + + + + +![The Settings tab of a SignalWire project showing Media URL Protection Details, with separate No/Yes toggles for Protect Recording Media URLs, Protect Message Media URLs, and Protect Fax Media URLs.](/assets/images/dashboard/media-protection-interface.webp) + + + + + + +Under **Project Details** > **Settings**, the Media URL Protection Details panel has three +independent toggles: Protect Recording Media URLs, Protect Message Media URLs, and Protect Fax +Media URLs. They are all off by default. + + + +Turning media protection on might break systems for existing customers who are using the links without authentication, so ensure all downstream systems are updated before turning media protection on. + +## Recording and the law + +Consent requirements vary by jurisdiction. Under one-party consent, which covers most US states, only +one party to the call needs to know it is being recorded, and an automated agent can be that party. +Under all-party consent — California and much of Europe, among others — everyone has to agree first, +which means telling the caller, asking, and continuing without a recording if they decline. `beep` and +a spoken disclosure are the usual ways to meet either standard, and informing callers everywhere is +the simpler policy to operate. + +Some material should not land in a recording at all: card numbers under PCI DSS, government +identifiers, passwords and PINs, and medical detail outside a healthcare context. Pause the recording +around those exchanges rather than capturing and redacting later — see +[Pause and resume](#pause-and-resume). Regulated workloads carry their own requirements — see +[Compliance][compliance]. + +This is not legal advice. Confirm what applies to you before recording production calls. + +## Alternatives to recording + +Audio recordings take up a lot of space, and time to parse and process. They also come with heavier legal compliance requirements. +Audio might not always be the best format to keep call history in. SignalWire offers transcription and AI summarization features +which are far easier to store, process and search. + +| If you need | Use | What you get | +|---|---|---| +| The summary of the call | An AI agent's post-prompt summary | Structured text at your `post_prompt_url` after the call, from the [`ai`][ai-method] method's `post_prompt` | +| The full text of the call | [`transcribe`][transcribe] | The whole call transcribed in the background, delivered when the call ends | +| Text while the call is still running | [`live_transcribe`][live-transcribe] | Transcription posted to your webhook as the call happens | +| Advice for a human agent mid-call | [`ai_sidecar`][ai-sidecar] | An AI observer on a human-to-human call, streaming events to your application without speaking | + +The post-prompt summary is the most versatile of the bunch, as it will deliver the exact information you require +in the format you specify. For example, if you're calling to get answers to certain survey questions, only the answers +to those questions can be kept. +For more information, consult the [conversation analytics][ai-analytics] guide +covers what it can report and how to read it, and the [AI post-prompt callback][post-prompt-webhook] +for a reference. + +These are not exclusive with recording. A call can be recorded and transcribed, and an AI call can +produce a summary and a recording. + +## Already using the Compatibility API + +| If you need | See | +|---|---| +| Recording in cXML markup | [``][cxml-record], [``][cxml-dial], [``][cxml-conference] | +| Pause, resume, or stop a live recording | [Update a recording][compat-update] | +| Find and manage the resulting media | [List][compat-list], [Retrieve][compat-retrieve], [Delete][compat-delete] | +| The recording status callback payload | [Recording status callback][recording-status-callback] | + +## Next steps + + + + + Every parameter for background recording in SWML, and the variables it sets. + + + + Every parameter the AI agent's `FunctionResult` accepts. TypeScript has its own [recordCall reference](/docs/server-sdks/reference/typescript/agents/function-result/record-call). + + + + List, fetch, and delete the recordings your scripts create. + + + + Foreground recording in SWML, for voicemail and single utterances. + + + + Require API credentials before anyone can fetch your recorded media. + + + diff --git a/fern/products/server-sdks/pages/guides/build-ai-agents/call-recording.mdx b/fern/products/server-sdks/pages/guides/build-ai-agents/call-recording.mdx deleted file mode 100644 index ca08cdc322..0000000000 --- a/fern/products/server-sdks/pages/guides/build-ai-agents/call-recording.mdx +++ /dev/null @@ -1,507 +0,0 @@ ---- -title: "Call Recording" -description: Record calls using record_call() and stop_record_call() methods on FunctionResult with support for stereo/mono, multiple formats, and webhook notifications. -slug: /guides/call-recording -max-toc-depth: 3 ---- - -Call recording is essential for many business applications: quality assurance, compliance, training, dispute resolution, and analytics. The SDK provides flexible recording options that let you capture exactly what you need while respecting privacy and compliance requirements. - -Recording happens server-side on SignalWire's infrastructure, so there's no additional load on your application server. Recordings are stored securely and can be retrieved via webhooks or the SignalWire API. - -### When to Record - -Common recording use cases: - -- **Quality assurance**: Review agent performance and customer interactions -- **Compliance**: Meet regulatory requirements for financial services, healthcare, etc. -- **Training**: Build libraries of good (and problematic) call examples -- **Dispute resolution**: Have an authoritative record of what was said -- **Analytics**: Feed recordings into speech analytics platforms -- **Transcription**: Generate text transcripts for search and analysis - -### Recording Overview - -**`record_call()`** - -- Starts background recording -- Continues while conversation proceeds -- Supports stereo (separate channels) or mono -- Output formats: WAV, MP3, or MP4 -- Direction: speak only, listen only, or both - -**`stop_record_call()`** - -- Stops an active recording -- Uses control_id to target specific recording -- Recording is automatically stopped on call end - -Recording methods across all languages: - -| Language | Start Recording | Stop Recording | -|----------|----------------|----------------| -| Python | `result.record_call(control_id="main", stereo=True, format="wav")` | `result.stop_record_call(control_id="main")` | -| TypeScript | `result.recordCall({ controlId: 'main', stereo: true, format: 'wav' })` | `result.stopRecordCall('main')` | -{/* - -| Go | `result.RecordCall("main", true, "wav")` | `result.StopRecordCall("main")` | -| Ruby | `result.record_call(control_id: 'main', stereo: true, format: 'wav')` | `result.stop_record_call(control_id: 'main')` | -| Java | `result.recordCall("main", true, "wav")` | `result.stopRecordCall("main")` | -| Perl | `$result->record_call(control_id => 'main', stereo => 1, format => 'wav')` | `$result->stop_record_call(control_id => 'main')` | -| C++ | `result.record_call("main", true, "wav")` | `result.stop_record_call("main")` | -| PHP | `$agent->recordCall(format: 'wav', stereo: true)` | `$result->stopRecordCall(controlId: 'main')` | - -*/} - -### Basic Recording - -```python -from signalwire import AgentBase -from signalwire.core.function_result import FunctionResult - -class RecordingAgent(AgentBase): - def __init__(self): - super().__init__(name="recording-agent") - self.add_language("English", "en-US", "rime.spore") - - self.prompt_add_section( - "Role", - "You are a customer service agent. " - "Start recording when the customer agrees." - ) - - self.define_tool( - name="start_recording", - description="Start recording the call with customer consent", - parameters={"type": "object", "properties": {}}, - handler=self.start_recording - ) - - def start_recording(self, args, raw_data): - return ( - FunctionResult("Recording has started.") - .record_call( - control_id="main_recording", - stereo=True, - format="wav" - ) - ) - -if __name__ == "__main__": - agent = RecordingAgent() - agent.run() -``` - -### Recording Parameters - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `control_id` | str | None | Identifier to stop specific recording | -| `stereo` | bool | False | True for separate L/R channels | -| `format` | str | `"wav"` | Output format: "wav", "mp3", or "mp4" (TypeScript: "wav" or "mp3" only) | -| `direction` | str | `"both"` | "speak", "listen", or "both" | -| `terminators` | str | None | DTMF digits that stop recording | -| `beep` | bool | False | Play beep before recording | -| `input_sensitivity` | float | `44.0` | Audio sensitivity threshold | -| `initial_timeout` | float | None | Seconds to wait for speech (voicemail use) | -| `end_silence_timeout` | float | None | Silence duration to auto-stop (voicemail use) | -| `max_length` | float | None | Maximum recording seconds | -| `status_url` | str | None | Webhook for recording events | - - -**Timeout Parameters**: For continuous call recording, do not set `initial_timeout` or `end_silence_timeout`. These parameters are designed for voicemail-style recordings where you want automatic termination based on speech patterns. - -- **Continuous recording**: Omit timeout parameters; use `stop_record_call()` or call termination to end -- **Voicemail recording**: Set appropriate values (e.g., `initial_timeout=10.0`, `end_silence_timeout=3.0`) - - -### Stereo vs Mono Recording - -The `stereo` parameter determines how audio channels are recorded. This choice significantly affects how you can use the recording afterward. - -#### Stereo Recording (stereo=True) - -Records caller and agent on separate channels (left and right): - -```python -def start_stereo_recording(self, args, raw_data): - return ( - FunctionResult("Recording in stereo mode") - .record_call( - control_id="stereo_rec", - stereo=True, # Caller on left, agent on right - format="wav" - ) - ) -``` - -**When to use stereo:** - -- **Speech-to-text transcription**: Most transcription services work better with separated audio, correctly attributing speech to each party -- **Speaker diarization**: Analysis tools can easily identify who said what -- **Quality review**: Isolate agent or caller audio for focused review -- **Training data**: Clean separation for building speech models -- **Noise analysis**: Identify which side has audio quality issues - -**Stereo considerations:** - -- Larger file sizes (roughly 2x mono) -- Requires stereo-capable playback for proper review -- Some basic media players may only play one channel by default - -#### Mono Recording (stereo=False) - -Records both parties mixed into a single channel: - -```python -def start_mono_recording(self, args, raw_data): - return ( - FunctionResult("Recording in mono mode") - .record_call( - control_id="mono_rec", - stereo=False, # Mixed audio (default) - format="mp3" - ) - ) -``` - -**When to use mono:** - -- **Simple archival**: Just need a record of what was said -- **Storage-constrained environments**: Smaller file sizes -- **Human playback**: Easier to listen to on any device -- **Basic compliance**: Where separate channels aren't required - -### Direction Options - -```python -## Record only what the AI/agent speaks -def record_agent_only(self, args, raw_data): - return ( - FunctionResult("Recording agent audio") - .record_call(direction="speak") - ) - -## Record only what the caller says -def record_caller_only(self, args, raw_data): - return ( - FunctionResult("Recording caller audio") - .record_call(direction="listen") - ) - -## Record both sides (default) -def record_both(self, args, raw_data): - return ( - FunctionResult("Recording full conversation") - .record_call(direction="both") - ) -``` - -### Recording with Webhook - -Receive notifications when recording completes: - -```python -def start_recording_with_callback(self, args, raw_data): - return ( - FunctionResult("Recording started") - .record_call( - control_id="webhook_rec", - status_url="https://example.com/recording-complete" - ) - ) -``` - -The webhook receives recording metadata including the URL to download the file. - -### Auto-Stop Recording - -Configure automatic stop conditions: - -```python -def start_auto_stop_recording(self, args, raw_data): - return ( - FunctionResult("Recording with auto-stop") - .record_call( - max_length=300.0, # Stop after 5 minutes - end_silence_timeout=5.0, # Stop after 5 seconds of silence - terminators="#" # Stop if user presses # - ) - ) -``` - -### Stop Recording - -Stop a recording by control_id: - -```python -from signalwire import AgentBase -from signalwire.core.function_result import FunctionResult - -class ControlledRecordingAgent(AgentBase): - def __init__(self): - super().__init__(name="controlled-recording-agent") - self.add_language("English", "en-US", "rime.spore") - - self.prompt_add_section( - "Role", - "You handle call recordings. You can start and stop recording." - ) - - self._register_functions() - - def _register_functions(self): - self.define_tool( - name="start_recording", - description="Start recording the call", - parameters={"type": "object", "properties": {}}, - handler=self.start_recording - ) - - self.define_tool( - name="stop_recording", - description="Stop recording the call", - parameters={"type": "object", "properties": {}}, - handler=self.stop_recording - ) - - def start_recording(self, args, raw_data): - return ( - FunctionResult("Recording has started") - .record_call(control_id="main") - ) - - def stop_recording(self, args, raw_data): - return ( - FunctionResult("Recording has stopped") - .stop_record_call(control_id="main") - ) - -if __name__ == "__main__": - agent = ControlledRecordingAgent() - agent.run() -``` - -### Recording with Beep - -Alert the caller that recording is starting: - -```python -def start_recording_with_beep(self, args, raw_data): - return ( - FunctionResult("This call will be recorded") - .record_call( - beep=True, # Plays a beep before recording starts - format="mp3" - ) - ) -``` - -### Complete Example - -```python -#!/usr/bin/env python3 -## compliance_agent.py - Agent with recording compliance features -from signalwire import AgentBase -from signalwire.core.function_result import FunctionResult - -class ComplianceAgent(AgentBase): - """Agent with full recording compliance features""" - - def __init__(self): - super().__init__(name="compliance-agent") - self.add_language("English", "en-US", "rime.spore") - - self.prompt_add_section( - "Role", - "You are a customer service agent. Before recording, you must " - "inform the customer and get their verbal consent." - ) - - self.prompt_add_section( - "Recording Policy", - """ - 1. Always inform customer: "This call may be recorded for quality purposes." - 2. Ask for consent: "Do you agree to the recording?" - 3. Only start recording after explicit "yes" or agreement. - 4. If customer declines, proceed without recording. - """ - ) - - self._register_functions() - - def _register_functions(self): - self.define_tool( - name="start_compliant_recording", - description="Start recording after customer consent is obtained", - parameters={"type": "object", "properties": {}}, - handler=self.start_compliant_recording - ) - - self.define_tool( - name="pause_recording", - description="Pause recording for sensitive information", - parameters={"type": "object", "properties": {}}, - handler=self.pause_recording - ) - - self.define_tool( - name="resume_recording", - description="Resume recording after sensitive section", - parameters={"type": "object", "properties": {}}, - handler=self.resume_recording - ) - - def start_compliant_recording(self, args, raw_data): - call_id = raw_data.get("call_id", "unknown") - - return ( - FunctionResult("Recording has begun. Thank you for your consent.") - .record_call( - control_id=f"compliance_{call_id}", - stereo=True, - format="wav", - beep=True, - status_url="https://example.com/recordings/status" - ) - .update_global_data({"recording_active": True}) - ) - - def pause_recording(self, args, raw_data): - call_id = raw_data.get("call_id", "unknown") - - return ( - FunctionResult( - "Recording paused. You may now provide sensitive information." - ) - .stop_record_call(control_id=f"compliance_{call_id}") - .update_global_data({"recording_active": False}) - ) - - def resume_recording(self, args, raw_data): - call_id = raw_data.get("call_id", "unknown") - - return ( - FunctionResult("Recording resumed.") - .record_call( - control_id=f"compliance_{call_id}", - stereo=True, - format="wav" - ) - .update_global_data({"recording_active": True}) - ) - -if __name__ == "__main__": - agent = ComplianceAgent() - agent.run() -``` - -### Format Comparison - -The `format` parameter determines the output file type. Each format has tradeoffs: - -| Format | Best For | File Size | Quality | Notes | -|--------|----------|-----------|---------|-------| -| `wav` | Transcription, archival | Large | Lossless | Uncompressed, no quality loss | -| `mp3` | General storage | Small | Lossy | Good compression, widely supported | -| `mp4` | Video calls | Medium | Lossy | Required for video recording | - -**Choosing a format:** - -- **wav**: Use when quality matters more than storage. Best for speech analytics, transcription services, and long-term archival where you might need to reprocess later. Files can be 10x larger than MP3. - -- **mp3**: Use for general-purpose recording where storage costs matter. Quality is sufficient for human review and most transcription services. Good balance of size and quality. - -- **mp4**: Required if you're recording video calls. Contains both audio and video tracks. - -### Storage and Retention Considerations - -Recordings consume storage and may have regulatory requirements. Plan your retention strategy: - -**Storage costs**: A 10-minute mono MP3 recording is roughly 2-3 MB. At scale, this adds up. A business handling 1,000 calls/day generates 60-90 GB/month of recordings. - -**Retention policies**: - -- **Financial services**: Often required to retain for 5-7 years -- **Healthcare (HIPAA)**: Typically 6 years -- **General business**: 1-2 years is common -- **Training/QA**: Keep only what's valuable - -**Automated cleanup**: Build processes to delete old recordings according to your policy. Don't assume someone will do it manually. - -**Access controls**: Recordings may contain sensitive information. Restrict access to those who need it for legitimate business purposes. - -### Compliance and Legal Considerations - -Recording laws vary by jurisdiction. Understanding your obligations is critical. - -#### Consent Requirements - -**One-party consent** (e.g., most US states): Only one party needs to know about the recording. The agent itself can be that party, but best practice is still to inform callers. - -**Two-party/all-party consent** (e.g., California, many European countries): All parties must consent before recording. You must: -1. Inform the caller that recording may occur -2. Obtain explicit consent before starting -3. Provide an option to decline -4. Proceed without recording if declined - -**Best practice**: Regardless of jurisdiction, always inform callers. It builds trust and protects you legally. - -#### Compliance Implementation - -```python -self.prompt_add_section( - "Recording Disclosure", - """ - At the start of every call: - 1. Say: "This call may be recorded for quality and training purposes." - 2. Ask: "Do you consent to recording?" - 3. If yes: Call start_recording function - 4. If no: Say "No problem, I'll proceed without recording" and continue - 5. NEVER start recording without explicit consent - """ -) -``` - -#### Sensitive Information - -Some information should never be recorded, or recordings should be paused: -- Credit card numbers (PCI compliance) -- Social Security numbers -- Medical information in non-healthcare contexts -- Passwords or PINs - -Use the pause/resume pattern shown in the complete example to handle these situations. - -### Recording Best Practices - -#### Compliance - -- Always inform callers before recording -- Obtain consent where legally required -- Provide option to decline recording -- Document consent in call logs -- Pause recording for sensitive information (credit cards, SSN) -- Know your jurisdiction's consent requirements - -#### Technical - -- Use control_id for multiple recordings or pause/resume -- Use stereo=True for transcription accuracy -- Use status_url to track recording completion -- Set max_length to prevent oversized files -- Handle webhook failures gracefully - -#### Storage - -- Use WAV for quality, MP3 for size, MP4 for video -- Implement retention policies aligned with regulations -- Secure storage with encryption at rest -- Restrict access to recordings -- Build automated cleanup processes - -#### Quality - -- Test recording quality in your deployment environment -- Verify both channels are capturing clearly in stereo mode -- Monitor for failed recordings via status webhooks diff --git a/fern/products/server-sdks/pages/guides/build-ai-agents/overview.mdx b/fern/products/server-sdks/pages/guides/build-ai-agents/overview.mdx index 0e14a53574..46ffef1fb6 100644 --- a/fern/products/server-sdks/pages/guides/build-ai-agents/overview.mdx +++ b/fern/products/server-sdks/pages/guides/build-ai-agents/overview.mdx @@ -7,7 +7,7 @@ max-toc-depth: 3 [contexts-and-workflows]: /docs/server-sdks/guides/contexts-workflows [state-management]: /docs/server-sdks/guides/state-management -[call-recording]: /docs/server-sdks/guides/call-recording +[call-recording]: /docs/platform/voice/call-recording [call-transfer]: /docs/server-sdks/guides/call-transfer [multi-agent-servers]: /docs/server-sdks/guides/multi-agent [search-and-knowledge]: /docs/server-sdks/guides/search-knowledge @@ -29,7 +29,7 @@ Build sophisticated voice AI agents using the SignalWire SDK. This section cover Manage data throughout call sessions using global_data, metadata, and post_prompt. - + Record calls with stereo/mono options, multiple formats, and webhook notifications.