diff --git a/agents/monitor/conversation-history.mdx b/agents/monitor/conversation-history.mdx index 52a64db..9f2e086 100644 --- a/agents/monitor/conversation-history.mdx +++ b/agents/monitor/conversation-history.mdx @@ -58,6 +58,7 @@ Each row carries the session facts: "source": "phone", "caller_number": "+15551234567", "dialed_number": "+14155550100", + "sip_call_id": "7f3a9c@sbc.example.com", "started_at": "2026-07-22T09:14:02Z", "ended_at": "2026-07-22T09:16:05Z", "duration_seconds": 123, @@ -69,7 +70,7 @@ Each row carries the session facts: } ``` -`caller_number` and `dialed_number` are set for phone sessions only (E.164) and are `null` for web sessions. `name` is the display name you passed when creating the session (`null` when omitted), and `metadata` echoes back whatever you attached when creating the session. +`caller_number` and `dialed_number` are set for phone sessions only (E.164) and are `null` for web sessions. `sip_call_id` is the SIP `Call-ID` of the call's INVITE. It is `null` on web sessions and on outbound calls that were never answered. See [Match calls with your SIP records](/agents/telephony/byo-sip#match-calls-with-your-sip-records). `name` is the display name you passed when creating the session (`null` when omitted), and `metadata` echoes back whatever you attached when creating the session. ### Status and end reason @@ -101,6 +102,7 @@ The same values appear on the [`call.ended` webhook](/agents/monitor/webhooks) a | `status` | One of `pending`, `active`, `completed`, `failed`, `unknown`; comma-separate values to match several | | `end_reason` | One or more of the `end_reason` values above, comma-separated | | `caller_number` | Exact match on the caller's E.164 number; a bare number gets `+` prepended automatically | +| `sip_call_id` | Exact match on the SIP `Call-ID` of the call's INVITE, for looking up the session behind a carrier CDR entry | | `created_after` / `created_before` | ISO 8601 timestamps | diff --git a/agents/monitor/webhooks.mdx b/agents/monitor/webhooks.mdx index 04dbc6f..81106bf 100644 --- a/agents/monitor/webhooks.mdx +++ b/agents/monitor/webhooks.mdx @@ -167,6 +167,9 @@ Every payload carries the `event` name and a `session` object with a fixed set o "direction": "outbound", "dial_status": "answered", "answered_by": "human", + "caller_number": "+15551234567", + "dialed_number": "+14155550100", + "sip_call_id": "7f3a9c@sbc.example.com", "batch_call_id": null, "conversation_started_at": "2026-07-23T12:01:12Z", "conversation_ended_at": null, @@ -185,6 +188,8 @@ Every payload carries the `event` name and a `session` object with a fixed set o `dial_status` is `answered`, `busy`, `no_answer`, or `failed`. `answered_by` is deprecated: answering-machine detection has been removed, so it is `unknown` once the call is answered and `null` before (older events may carry `human` or `voicemail`). Both facts also appear on the session, alongside `direction` (`inbound` or `outbound`) and `batch_call_id` (reserved, always `null` today); these session fields are present in every webhook payload, with the dial fields `null` on inbound sessions. The `phone_call.dial_finished` snapshot is taken when the dial resolves, so on an answered call `conversation_ended_at` and `duration_seconds` are still `null`. The final numbers arrive with `call.ended`. +`caller_number`, `dialed_number` and `sip_call_id` carry the same values as on the [sessions API](/agents/monitor/conversation-history): the E.164 numbers of the call (person-centric on outbound calls, so `caller_number` is the party you dialed) and the SIP `Call-ID` of the call's INVITE. All three are empty strings on web sessions, and `sip_call_id` is also empty on an unanswered outbound dial. How to use it against your carrier's records: [Match calls with your SIP records](/agents/telephony/byo-sip#match-calls-with-your-sip-records). + `end_user_id` and `metadata` are echoed exactly as you set them when creating the session. Use them to correlate the event with records in your own system. `branch_id` is an internal configuration-lineage identifier and is safe to ignore. What lands in `summary`, `data`, and `criteria_results` is defined by your [analysis configuration](/agents/monitor/post-call-analysis). The example shows a `completed` analysis. `analysis.status` can also be `skipped` (nothing to analyze: the caller never spoke, or analysis is disabled) or `error` (the run failed, with the cause in `analysis.error`); both arrive with `summary: null` and empty `data` / `criteria_results`. Check the status before reading results. diff --git a/agents/telephony/byo-sip.mdx b/agents/telephony/byo-sip.mdx index af51f5a..5b36249 100644 --- a/agents/telephony/byo-sip.mdx +++ b/agents/telephony/byo-sip.mdx @@ -10,8 +10,10 @@ This works with any carrier or PBX that speaks SIP trunking: Twilio Elastic SIP ## How it works -- **Inbound**: your carrier routes calls for the number over its trunk to Fish Audio's SIP endpoint. The platform matches the dialed number and hands the call to the agent bound to it; from there it is a normal [inbound call](/agents/telephony/inbound-calls). -- **Outbound** (optional): give the import a termination host and the platform can also place calls from the number. Outbound calls and warm-transfer consult legs dial out through your trunk, with the imported number as the caller ID. +A SIP trunk has two directions, and carriers name them from the carrier's point of view. **Origination** is traffic your carrier originates towards Fish Audio: an inbound call. **Termination** is traffic your carrier terminates for Fish Audio: an outbound call the platform hands to your carrier or PBX to complete. Twilio's trunk configuration uses the same two words, and so do the fields below. + +- **Inbound (origination)**: your carrier routes calls for the number over its trunk to Fish Audio's SIP endpoint. The platform matches the dialed number and hands the call to the agent bound to it. From there it is a normal [inbound call](/agents/telephony/inbound-calls). +- **Outbound (termination)**, optional: give the import a termination host and the platform can also place calls from the number. Outbound calls and warm-transfer consult legs dial out through your trunk, with the imported number as the caller ID. You can also attach custom SIP headers that every call through the termination carries, for routing hints or tenant identification on your PBX. The SIP endpoint to point your trunk at: @@ -28,7 +30,7 @@ sip:1pv316az391.sip.livekit.cloud;transport=tcp ### In the console -On the workspace **Phone numbers** page, choose **Import number**. Enter the number in E.164 format, set at least one inbound authentication factor, and optionally fill in the **Outbound calling (termination)** section. The same form is available later from the number's row menu as **Edit configuration**. +On the workspace **Phone numbers** page, choose **Import number**. Enter the number in E.164 format, set at least one inbound authentication factor, and optionally fill in the **Outbound calling (termination)** section, including any **Custom SIP headers** (one `X-Name: value` per line). The same form is available later from the number's row menu as **Edit configuration**. ### Through the API @@ -48,7 +50,8 @@ curl --request POST https://api.fish.audio/v1/agent/phone-numbers \ "termination_uri": "pbx.example.com", "termination_transport": "tcp", "termination_auth_username": "fish-outbound", - "termination_auth_password": "another-long-password" + "termination_auth_password": "another-long-password", + "termination_headers": { "X-Customer-Id": "acme" } }' ``` @@ -67,6 +70,7 @@ Returns `201` with the number object. Imported numbers land in your default work | `termination_transport` | `auto` (default), `udp`, `tcp`, or `tls`. | | `termination_auth_username` | Optional digest username for your termination; requires the password and a `termination_uri`. | | `termination_auth_password` | The matching digest password. | +| `termination_headers` | Optional: custom `X-` headers for every call through the termination, up to 20; see [Custom SIP headers](#custom-sip-headers). | At least one inbound factor (digest credentials and/or allowed addresses) is required; a `422` reports what is missing. A `409` means the number is already on the platform. A `502` means trunk provisioning failed; the number stays visible with status `error` and is safe to release and retry. @@ -141,6 +145,65 @@ What an imported number can do depends on whether you configured a termination: The number object reports this as `supports_outbound`. Place calls with the same API as purchased numbers; see [Outbound calls](/agents/telephony/outbound-calls). +### Custom SIP headers + +`termination_headers` lets you tag every INVITE the platform sends through your termination, for example to identify the tenant or pick a route on your PBX: + +```json +"termination_headers": { + "X-Customer-Id": "acme", + "X-Route-Hint": "eu-west" +} +``` + +The headers ride along on outbound calls and on warm-transfer consult legs, since both dial through the termination. Cold transfers hand the call off on the inbound leg and do not carry them. Names must be `X-` tokens or `User-to-User`, so the request's own fields (`From`, `To`, `Via`) cannot be overridden, and `X-Fish-` is reserved for headers the platform sets itself. Values are 1 to 1024 printable ASCII characters (128 for `User-to-User`). Header names are case-insensitive, so a map with two spellings of the same name is rejected. + +Values that change per call, such as a campaign or account id, go on the call instead: [`sip_headers`](/agents/telephony/outbound-calls#per-call-sip-headers) on the outbound call request is merged over these number-level headers. For matching calls to sessions you do not need a header of your own; see [Match calls with your SIP records](#match-calls-with-your-sip-records). + +## Match calls with your SIP records + +Your carrier, SBC or PBX keeps its own record of every call: a CDR, a log line, a recording file. Two identifiers let you line those records up with Fish Audio sessions, one in each direction, and neither needs any configuration. + +| Identifier | What it is | Where you read it | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `sip_call_id` | The SIP `Call-ID` of the call's INVITE, the value the SIP world uses as the identity of one call. Your SIP records carry it already. | On the session, from `GET /v1/agent/sessions/{session_id}`, as a filter on the sessions list (`?sip_call_id=`), and in the `session` object of every post-call webhook. | +| `X-Fish-Session-Id` | The Fish Audio session id, sent as a header on every INVITE and REFER the platform places through your trunk. | In your own SIP records, if your SBC or PBX logs custom headers. | + +Which one you use depends on where you start: + +- **From a Fish Audio webhook to your records**: read `sip_call_id` from the webhook's `session` object and look it up in your CDR. This works without any change on your side, because every SIP system already records `Call-ID`. +- **From your records to Fish Audio**: take the `Call-ID` from your CDR and list sessions with `GET /v1/agent/sessions?sip_call_id=…`, which returns the matching session. If your SBC logs custom headers, `X-Fish-Session-Id` from the INVITE gives you the session id directly. + +When each identifier is present: + +| Call | `sip_call_id` on the session | `X-Fish-Session-Id` in the INVITE | +| ----------------------------- | -------------------------------------- | ----------------------------------- | +| Inbound call | Yes, from your carrier's INVITE | Not applicable: the INVITE is yours | +| Outbound call, answered | Yes, from the INVITE the platform sent | Yes | +| Outbound call, not answered | `null`: no call was completed | Yes, on the INVITE that was refused | +| Warm transfer consult leg | Not exposed separately | Yes, on the consult INVITE | +| Cold transfer to a SIP target | Not applicable | Yes, as a header on the REFER | + +The `X-Fish-Session-Id` header is only sent through your own trunk. Calls from platform-purchased numbers terminate on the platform's carrier and carry no such header, but `sip_call_id` is still exposed for them. + +For your own business keys, such as a campaign or tenant id, add [custom SIP headers](#custom-sip-headers) on the number or [per call](/agents/telephony/outbound-calls#per-call-sip-headers). They complement the two identifiers above rather than replace them: a business key tells you which campaign a call belonged to, the identifiers tell you which call it was. + +A `phone_call.dial_finished` payload carries everything needed for the webhook-to-CDR direction: + +```json +{ + "event": "phone_call.dial_finished", + "dial_status": "answered", + "session": { + "id": "5f3e…", + "caller_number": "+15551234567", + "dialed_number": "+14155550100", + "sip_call_id": "7f3a9c@sbc.example.com", + "metadata": { "lead_ref": "L-2041" } + } +} +``` + ## Update the configuration Change any part of an imported number's trunk configuration in place: rotate digest credentials, adjust the allowed addresses, or add, change, and remove the termination. In the console, open **Edit configuration** from the number's row menu; over the API, `PUT` the full desired configuration: @@ -156,16 +219,18 @@ curl --request PUT https://api.fish.audio/v1/agent/phone-numbers/$PHONE_NUMBER_I "termination_uri": "pbx.example.com", "termination_transport": "tls", "termination_auth_username": "fish-outbound", - "termination_auth_password": "" + "termination_auth_password": "", + "termination_headers": { "X-Customer-Id": "acme" } }' ``` -The body is the import payload without `phone_number`, `label`, and `agent_id`, and it replaces the whole configuration. Two conveniences: +The body is the import payload without `phone_number`, `label`, and `agent_id`, and it replaces the whole configuration. Three things to know: - An empty password next to a set username keeps the stored password, so you can edit other fields without re-entering secrets. Setting a username for the first time requires a password. - An empty `termination_uri` removes the termination and makes the number inbound-only again. +- `termination_headers` replaces the stored map; omit it or send `{}` to remove all custom headers. -Updates apply in place: routing is never interrupted, and calls already in progress are unaffected. The response is the updated number object; for imported numbers it carries the non-secret configuration (`inbound_auth_username`, `inbound_allowed_addresses`, `termination_uri`, `termination_transport`, `termination_auth_username`) alongside `supports_outbound`. +Updates apply in place: routing is never interrupted, and calls already in progress are unaffected. The response is the updated number object; for imported numbers it carries the non-secret configuration (`inbound_auth_username`, `inbound_allowed_addresses`, `termination_uri`, `termination_transport`, `termination_auth_username`, `termination_headers`) alongside `supports_outbound`. ## Billing diff --git a/agents/telephony/inbound-calls.mdx b/agents/telephony/inbound-calls.mdx index 1d63859..3a72567 100644 --- a/agents/telephony/inbound-calls.mdx +++ b/agents/telephony/inbound-calls.mdx @@ -65,12 +65,13 @@ Point a phone number at an agent and it picks up every inbound call. Phone calls ## Phone sessions in history -Every answered call becomes a session with `source: "phone"` and two attribution fields: +Every answered call becomes a session with `source: "phone"` and these attribution fields: | Field | Description | | --------------- | --------------------------------------------------------------------------- | | `caller_number` | The caller's number, E.164. `null` for non-phone sessions. | | `dialed_number` | The workspace number that was called, E.164. `null` for non-phone sessions. | +| `sip_call_id` | The SIP `Call-ID` of the carrier's INVITE, for [matching your SIP records](/agents/telephony/byo-sip#match-calls-with-your-sip-records). | List calls with the standard sessions endpoint. The `caller_number` filter is an exact match; bare numbers are automatically prefixed with `+`, so `15551234567` matches `+15551234567`: diff --git a/agents/telephony/outbound-calls.mdx b/agents/telephony/outbound-calls.mdx index 4a45e78..f33ed15 100644 --- a/agents/telephony/outbound-calls.mdx +++ b/agents/telephony/outbound-calls.mdx @@ -70,9 +70,25 @@ This endpoint requires an API key; there is no anonymous variant. See the [API r | `overrides` | Optional: replace whole configuration fields for this call, subject to the agent's [override allowlist](/agents/deploy/authenticated-sessions#overrides). | | `metadata` | Optional: your own JSON object, returned verbatim on session reads and in webhook payloads. Never interpreted. | | `llm_extra_body` | Optional: JSON object (at most 16 KB) forwarded to a [custom LLM](/agents/build/custom-llm) endpoint on every request as `fishaudio_extra_body`. Ignored on platform-model agents. | +| `sip_headers` | Optional, [imported SIP numbers](/agents/telephony/byo-sip) only: custom headers for this call's INVITE. See [Per-call SIP headers](#per-call-sip-headers). | The session's [time and timezone context](/agents/build/time-timezone) resolves from the destination number when the agent has no fixed timezone configured, so "tomorrow morning" means the callee's morning. +### Per-call SIP headers + +When the from-number is an imported SIP number, `sip_headers` adds headers to the INVITE the platform sends through your termination, so your SBC or PBX can route the call or tag its CDR with a value that changes per call: a campaign id, an account id, a consent reference. + +```json +"sip_headers": { + "X-Campaign-Id": "spring-24", + "X-Account-Id": "acct_8812" +} +``` + +They follow the same rules as the number's [`termination_headers`](/agents/telephony/byo-sip#custom-sip-headers) and are merged over them, so a per-call header replaces a number-level header of the same name. The same headers ride on a warm-transfer consult leg placed during the call. Every INVITE through your termination also carries `X-Fish-Session-Id` with the session id, so the SIP side can be joined to the platform side without any header of your own. + +Platform-purchased numbers refuse `sip_headers` with `422 sip_headers_unsupported`: the shared trunk terminates on the carrier and nothing downstream would read them. `sip_headers` is not `metadata`: it leaves the platform for your SIP network, while `metadata` is only echoed back to you. + ### Retry safely with an Idempotency-Key Outbound dials spend money and ring real phones, so put an `Idempotency-Key` header on every create. For 24 hours, repeating the same key with the same body returns the call already placed instead of dialing again. The same key with a **different** body is refused with `422 idempotency_key_reuse`, and a retry that races an in-flight first attempt gets `409 idempotency_key_conflict`; back off and retry the same request. If the create fails with an ambiguous network error, retry with the same key: you get the placed session back if the first attempt went through. @@ -98,6 +114,8 @@ Two session fields carry the outcome: | `dial_status` | `answered`, `busy`, `no_answer`, or `failed`. `null` while the call is still ringing, and on inbound calls. | | `answered_by` | Deprecated. Answering-machine detection has been removed, so new calls report `unknown` once answered (`null` before). Older sessions may still carry `human` or `voicemail`. | +Once answered, `sip_call_id` holds the SIP `Call-ID` of the leg the platform placed. It is `null` while ringing and on unanswered dials. See [Match calls with your SIP records](/agents/telephony/byo-sip#match-calls-with-your-sip-records). + On outbound sessions the attribution fields are person-centric: `caller_number` is the human you dialed and `dialed_number` is your workspace number. The same values reach the agent as `{{system.caller_number}}` and `{{system.dialed_number}}`, so a CRM lookup tool can use `https://crm.example.com/contacts?phone={{system.caller_number}}` without a per-call variable; see [System variables](/agents/build/dynamic-variables#system-variables). Everything else about the session (transcript, recording, [post-call analysis](/agents/monitor/post-call-analysis), `call.ended` and `call.analyzed` webhooks, hang-up via `POST /v1/agent/sessions/{session_id}/end`) works exactly as for [inbound calls](/agents/telephony/inbound-calls). ## Allowed destinations @@ -128,6 +146,7 @@ Unlike most [Agents API errors](/api-reference/agent-errors), phone-call errors | `409` | `agent_not_published` | [Publish](/agents/deploy/versions-publishing) the agent first. | | `409` | `idempotency_key_conflict` | A request with this key is still in flight; back off and retry the same request. | | `422` | `number_provider_unsupported`, `number_termination_missing`, `number_inactive` | The from-number can't place calls; see [which numbers support outbound](/agents/telephony/byo-sip#outbound-calls-and-transfers). | +| `422` | `sip_headers_unsupported` | `sip_headers` was sent with a platform-purchased number. See [per-call SIP headers](#per-call-sip-headers). | | `422` | `destination_invalid`, `destination_not_allowed`, `premium_destination_blocked`, `self_call_blocked` | The destination is refused; see [allowed destinations](#allowed-destinations). | | `422` | `idempotency_key_reuse` | The key was already used with a different body; mint a fresh key per distinct call. | diff --git a/api-reference/openapi.json b/api-reference/openapi.json index 1935405..f149dd2 100644 --- a/api-reference/openapi.json +++ b/api-reference/openapi.json @@ -503,7 +503,7 @@ "/v1/agent/sessions": { "get": { "summary": "List Agent Sessions", - "description": "List your team's sessions, newest first. Filter by agent, status, end\nreason, caller number, or creation time. Paginate with `cursor` (recommended; follow\n`next_cursor` while `has_more` is true) or with `page` for offset pagination\nwith a `total` count — the two are mutually exclusive.", + "description": "List your team's sessions, newest first. Filter by agent, status, end\nreason, caller number, SIP Call-ID, or creation time. Paginate with `cursor` (recommended; follow\n`next_cursor` while `has_more` is true) or with `page` for offset pagination\nwith a `total` count — the two are mutually exclusive.", "security": [ { "BearerAuth": [] @@ -605,6 +605,25 @@ }, "deprecated": false }, + { + "in": "query", + "name": "sip_call_id", + "description": "Exact-match SIP Call-ID of the call's INVITE, for looking up the session behind a carrier CDR entry (phone sessions only).", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sip Call Id" + }, + "deprecated": false + }, { "in": "query", "name": "created_after", @@ -1479,6 +1498,18 @@ "default": null, "title": "Dialed Number" }, + "sip_call_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sip Call Id" + }, "timezone": { "anyOf": [ { @@ -3748,7 +3779,7 @@ }, "patch": { "summary": "Update Draft Config", - "description": "Patch the draft configuration section by section; omitted sections keep\ntheir value. Changes only affect live sessions after the next publish.\n`prompt.system_prompt` is limited to 32000 tokens (422 beyond); keeping it\nunder 2000 tokens is recommended for latency and cost.\n`voice.voice_id` accepts any public voice model id.\n`voice.speaking_language` accepts any of the 52 supported ISO 639-1 codes\n(the same set the console offers, see the Voice & language docs); anything else is 422. `voice.expressive` opts into richer expressive\n\ndelivery (emotion steering, laughter and sounds, pauses); off keeps the\nstandard delivery. `voice.keyterms` is a speech-recognition vocabulary of\nup to 50 plain terms (brand names, product terms, personal names), each at\nmost 100 characters with no commas or semicolons; `[]` clears it and 20-50\nfocused terms work best. `tool_ids` and\n`knowledge_source_ids` replace their attachment lists wholesale and every\nid must resolve, else 422. `llm.custom` points the agent at your own\nOpenAI-compatible endpoint; mutually exclusive with `llm.model`, cleared\nwith an explicit null.", + "description": "Patch the draft configuration section by section; omitted sections keep\ntheir value. Changes only affect live sessions after the next publish.\n`prompt.system_prompt` is limited to 32000 tokens (422 beyond); keeping it\nunder 2000 tokens is recommended for latency and cost.\n`voice.voice_id` accepts any public voice model id.\n`voice.speaking_language` accepts any of the 52 supported ISO 639-1 codes\n(the same set the console offers, see the Voice & language docs); anything else is 422. `voice.expressive` (default `true`) enables richer\nexpressive delivery (emotion steering, laughter and sounds, pauses); off\nkeeps the standard delivery. `voice.keyterms` is a speech-recognition vocabulary of\nup to 50 plain terms (brand names, product terms, personal names), each at\nmost 100 characters with no commas or semicolons; `[]` clears it and 20-50\nfocused terms work best. `tool_ids` and\n`knowledge_source_ids` replace their attachment lists wholesale and every\nid must resolve, else 422. `llm.custom` points the agent at your own\nOpenAI-compatible endpoint; mutually exclusive with `llm.model`, cleared\nwith an explicit null.", "security": [ { "BearerAuth": [] @@ -8686,6 +8717,14 @@ "description": "Imported `sip` numbers: the termination digest username. Passwords are never echoed.", "title": "Termination Auth Username" }, + "termination_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Imported `sip` numbers: custom SIP headers added to every outbound INVITE through the termination.", + "title": "Termination Headers", + "type": "object" + }, "created_at": { "format": "date-time", "title": "Created At", @@ -9209,6 +9248,14 @@ "description": "Imported `sip` numbers: the termination digest username. Passwords are never echoed.", "title": "Termination Auth Username" }, + "termination_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Imported `sip` numbers: custom SIP headers added to every outbound INVITE through the termination.", + "title": "Termination Headers", + "type": "object" + }, "created_at": { "format": "date-time", "title": "Created At", @@ -9555,6 +9602,14 @@ "description": "Imported `sip` numbers: the termination digest username. Passwords are never echoed.", "title": "Termination Auth Username" }, + "termination_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Imported `sip` numbers: custom SIP headers added to every outbound INVITE through the termination.", + "title": "Termination Headers", + "type": "object" + }, "created_at": { "format": "date-time", "title": "Created At", @@ -9917,7 +9972,7 @@ "/v1/agent/phone-calls": { "post": { "summary": "Create Phone Call", - "description": "Place an outbound call from one of your Twilio phone numbers to a US,\nCanada or Japan destination. A domestic trunk 0 after +81 (e.g.\n+81080...) is accepted and normalized to E.164 (+8180...). Returns\nimmediately with the session queued for\ndialing; subscribe to the `phone_call.dial_finished` webhook or poll\n`GET /v1/agent/sessions/{session_id}` for the dial outcome. Ringing is\nnever billed — metering starts when the callee answers.\n\nErrors carry a machine-readable `reason` (e.g. `destination_not_allowed`,\n`insufficient_credit`, `daily_limit_exceeded`,\n`concurrency_limit_exceeded`).", + "description": "Place an outbound call from one of your phone numbers. Platform numbers\ndial US, Canada or Japan destinations, and a domestic trunk 0 after +81\n(e.g. +81080...) is accepted and normalized to E.164 (+8180...).\nImported SIP numbers dial through their own termination and may attach\ncustom INVITE headers with `sip_headers`. Returns immediately with the\nsession queued for dialing. Subscribe to the `phone_call.dial_finished`\nwebhook or poll `GET /v1/agent/sessions/{session_id}` for the dial\noutcome and the leg's `sip_call_id`. Ringing is never billed: metering\nstarts when the callee answers.\n\nErrors carry a machine-readable `reason` (e.g. `destination_not_allowed`,\n`insufficient_credit`, `concurrency_limit_exceeded`).", "security": [ { "BearerAuth": [] @@ -14466,6 +14521,18 @@ "default": null, "title": "Dialed Number" }, + "sip_call_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Sip Call Id" + }, "timezone": { "anyOf": [ { @@ -14569,6 +14636,7 @@ } ], "default": null, + "description": "IANA timezone (like `Asia/Tokyo`) for the agent's sense of local date and time in this session. Overrides the agent's configured timezone, which defaults to UTC. Invalid names are rejected with 422.", "title": "Timezone" }, "client_timezone": { @@ -14582,6 +14650,7 @@ } ], "default": null, + "description": "The end user's device timezone as an IANA name (like `Asia/Tokyo`), used as a fallback hint: it applies only when neither `timezone` nor the agent's configured timezone is set, and invalid values are ignored rather than rejected. The Web SDK fills it automatically from the browser for public agents. When your backend creates the session, forward the value from your client.", "title": "Client Timezone" }, "world_context": { @@ -15722,11 +15791,35 @@ "default": "", "title": "Warm Briefing Instructions", "type": "string" + }, + "on_failure": { + "$ref": "#/components/schemas/AgentTransferOnFailurePatch" } }, "title": "AgentTransferDestinationPatch", "type": "object" }, + "AgentTransferOnFailurePatch": { + "additionalProperties": false, + "properties": { + "action": { + "default": "return_to_agent", + "enum": [ + "return_to_agent", + "end_call" + ], + "title": "Action", + "type": "string" + }, + "message": { + "default": "", + "title": "Message", + "type": "string" + } + }, + "title": "AgentTransferOnFailurePatch", + "type": "object" + }, "PublicAgentAnalysisCriterion": { "additionalProperties": false, "properties": { @@ -17108,7 +17201,7 @@ "AgentLLMConfigRedacted": { "properties": { "model": { - "default": "google/gemini-3.5-flash-lite", + "default": "google/gemini-3.6-flash", "enum": [ "google/gemini-3.5-flash-lite", "google/gemini-3.6-flash", @@ -17319,15 +17412,38 @@ "default": "", "title": "Warm Briefing Instructions", "type": "string" + }, + "on_failure": { + "$ref": "#/components/schemas/AgentTransferOnFailure" } }, "title": "AgentTransferDestination", "type": "object" }, + "AgentTransferOnFailure": { + "properties": { + "action": { + "default": "return_to_agent", + "enum": [ + "return_to_agent", + "end_call" + ], + "title": "Action", + "type": "string" + }, + "message": { + "default": "", + "title": "Message", + "type": "string" + } + }, + "title": "AgentTransferOnFailure", + "type": "object" + }, "AgentVoiceConfig": { "properties": { "voice_id": { - "default": "4501d82f5de3467ebf4d7ef095a2deee", + "default": "b347db033a6549378b48d00acb0d06cd", "title": "Voice Id", "type": "string" }, @@ -18383,6 +18499,14 @@ "description": "Imported `sip` numbers: the termination digest username. Passwords are never echoed.", "title": "Termination Auth Username" }, + "termination_headers": { + "additionalProperties": { + "type": "string" + }, + "description": "Imported `sip` numbers: custom SIP headers added to every outbound INVITE through the termination.", + "title": "Termination Headers", + "type": "object" + }, "created_at": { "format": "date-time", "title": "Created At", @@ -18450,7 +18574,7 @@ }, "PublicSipNumberImportPayload": { "additionalProperties": false, - "description": "The `sip` variant of POST /v1/agent/phone-numbers: import a number that\nstays at your carrier. Point your trunk's origination at our SIP host,\ngive inbound at least one authentication factor (digest and/or source\nCIDRs), and optionally a termination host + credentials so the number can\nplace calls. Nothing is rented: carrier charges stay on your account, and\nimported numbers carry no telephony charges at all: no monthly fee, no\nphone surcharge, no transfer fees; you pay agent minutes only.", + "description": "The `sip` variant of POST /v1/agent/phone-numbers: import a number that\nstays at your carrier. Point your trunk's origination at our SIP host,\ngive inbound at least one authentication factor (digest and/or source\nCIDRs), and optionally a termination host + credentials (plus custom\n`X-` headers for every outbound INVITE) so the number can place calls.\nNothing is rented: carrier charges stay on your account, and\nimported numbers carry no telephony charges at all: no monthly fee, no\nphone surcharge, no transfer fees; you pay agent minutes only.", "properties": { "inbound_auth_username": { "default": "", @@ -18500,6 +18624,13 @@ "title": "Termination Auth Password", "type": "string" }, + "termination_headers": { + "additionalProperties": { + "type": "string" + }, + "title": "Termination Headers", + "type": "object" + }, "phone_number": { "title": "Phone Number", "type": "string" @@ -18674,6 +18805,22 @@ ], "default": null, "title": "Llm Extra Body" + }, + "sip_headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Custom SIP headers for this call's INVITE, imported SIP numbers only. X- names or User-to-User, printable ASCII values, merged over the number's termination_headers.", + "title": "Sip Headers" } }, "required": [