-
Notifications
You must be signed in to change notification settings - Fork 25
Custom SIP headers, X-Fish-Session-Id and sip_call_id for BYO SIP numbers #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,12 +6,14 @@ | |
|
|
||
| If your numbers already live at a carrier, you can connect them to your agents without porting anything. Point the carrier's SIP trunk at Fish Audio and import the number: it stays with your carrier, who keeps billing you for the telephone-network legs, and on Fish Audio the calls bill as ordinary agent sessions. Imported numbers carry no monthly rental and no telephony charges of any kind: no phone surcharge, no transfer fees; the call bills like a web session. | ||
|
|
||
| This works with any carrier or PBX that speaks SIP trunking: Twilio Elastic SIP Trunking, Asterisk or FreePBX, and most SIP providers. It is also the only way to use non-US/CA numbers, which the purchasable inventory does not cover. | ||
|
|
||
| ## 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 @@ | |
|
|
||
| ### 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 @@ | |
| "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 @@ | |
| | `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. | ||
|
|
||
|
|
@@ -80,30 +84,30 @@ | |
| The SIP endpoint is shared, so an import must prove that calls really come from your trunk: | ||
|
|
||
| - **Digest credentials**: the platform challenges your trunk and verifies the username and password. Use this whenever your carrier or PBX answers digest challenges (Asterisk, FreePBX, most SIP providers). | ||
| - **Allowed source addresses**: calls are only accepted from the listed IPs or CIDR ranges. Use this for carriers that do not authenticate their origination traffic; Twilio Elastic SIP Trunking is one, so for Twilio this is the required factor. | ||
|
Check warning on line 87 in agents/telephony/byo-sip.mdx
|
||
|
|
||
| Set both when your carrier supports it. | ||
|
|
||
| ## Carrier walkthroughs | ||
|
|
||
| ### Twilio Elastic SIP Trunking | ||
|
|
||
| <Steps> | ||
| <Step title="Create a trunk"> | ||
| In the Twilio console, under **Elastic SIP Trunking**, create a trunk (or | ||
| reuse an existing one). | ||
| </Step> | ||
| <Step title="Point origination at Fish Audio"> | ||
| Add an origination URI: `sip:1pv316az391.sip.livekit.cloud;transport=tcp`. | ||
| </Step> | ||
| <Step title="Attach your number"> | ||
| On the trunk's **Numbers** tab, add the phone number. Twilio routes its | ||
| calls through the trunk from then on. | ||
| </Step> | ||
| <Step title="Import on Fish Audio"> | ||
| Twilio's origination does not answer digest challenges, so authenticate by | ||
| source address: allow Twilio's published signaling IP ranges for the regions | ||
| you use (see [Twilio's IP address | ||
| list](https://www.twilio.com/docs/sip-trunking/ip-addresses)). In the | ||
| console, the **Twilio Elastic SIP Trunking preset** button fills the ranges | ||
| and sets the transport for you. | ||
|
|
@@ -113,19 +117,19 @@ | |
| (`yourprefix.pstn.twilio.com`) and attach a **Credential List**. Pass the | ||
| host as `termination_uri` and the credentials as `termination_auth_username` | ||
| and `termination_auth_password`. Credentials are required here: Fish Audio's | ||
| outbound traffic does not come from fixed IPs, so Twilio IP access control | ||
|
Check warning on line 120 in agents/telephony/byo-sip.mdx
|
||
| lists cannot authorize it. | ||
| </Step> | ||
| <Step title="Allow transfers (optional)"> | ||
| For [cold transfers](/agents/telephony/transfers), enable **Call Transfer | ||
| (SIP REFER)** in the trunk's settings so Twilio honors the handoff. | ||
| </Step> | ||
| </Steps> | ||
|
|
||
| ### Asterisk, FreePBX, and other SIP platforms | ||
|
|
||
| - Route the number's inbound calls to `sip:1pv316az391.sip.livekit.cloud;transport=tcp`. | ||
| - Configure digest credentials on the trunk and pass the same pair as `inbound_auth_username` and `inbound_auth_password`; add your PBX's public IPs to `inbound_allowed_addresses` for defense in depth. | ||
|
Check warning on line 132 in agents/telephony/byo-sip.mdx
|
||
| - For outbound, expose a termination host reachable from the internet and pass it as `termination_uri`, with digest credentials if your PBX requires registration or authentication. | ||
|
|
||
| ## Outbound calls and transfers | ||
|
|
@@ -141,6 +145,65 @@ | |
|
|
||
| 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 @@ | |
| "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: | ||
|
Check warning on line 227 in agents/telephony/byo-sip.mdx
|
||
|
|
||
| - 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. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
python - <<'PY'
import json
with open("api-reference/openapi.json", encoding="utf-8") as source:
spec = json.load(source)
for path, operations in spec["paths"].items():
if path.endswith("/sip-config") and "put" in operations:
schema = operations["put"]["requestBody"]["content"]["application/json"]["schema"]
print("PUT path:", path)
print("Request schema:", schema)
ref = schema.get("$ref")
if ref:
name = ref.rsplit("/", 1)[-1]
model = spec["components"]["schemas"][name]
print("Referenced schema:", name)
print("additionalProperties:", model.get("additionalProperties"))
print("termination_headers present:", "termination_headers" in model.get("properties", {}))
PYRepository: fishaudio/docs Length of output: 152 🏁 Script executed: #!/bin/bash
set -o pipefail
printf '%s\n' '--- matching paths and fields ---'
rg -n -C 8 '"[^"]*sip-config[^"]*"|"termination_headers"' api-reference/openapi.json
printf '%s\n' '--- changed documentation excerpt ---'
sed -n '215,240p' agents/telephony/byo-sip.mdx
printf '%s\n' '--- relevant diff summary ---'
git diff --unified=6 4ef5fb1706a3d7ec9835cbc7635c55442b372824 b782ce0e6db3d39f20b4a5f841dd1d128a4d0f5d -- agents/telephony/byo-sip.mdx api-reference/openapi.jsonRepository: fishaudio/docs Length of output: 39275 🏁 Script executed: #!/bin/bash
python3 - <<'PY'
import json
with open("api-reference/openapi.json", encoding="utf-8") as f:
spec = json.load(f)
print("--- phone-number paths and methods ---")
for path, ops in spec.get("paths", {}).items():
if "phone-number" in path:
print(path, sorted(k for k in ops if k in {"get", "post", "put", "patch", "delete"}))
for method in ("put", "patch"):
op = ops.get(method)
if not op:
continue
body = op.get("requestBody", {}).get("content", {}).get("application/json", {}).get("schema")
print(" ", method.upper(), "schema:", body)
if isinstance(body, dict) and "$ref" in body:
name = body["$ref"].rsplit("/", 1)[-1]
model = spec.get("components", {}).get("schemas", {}).get(name)
print(" ", "resolved:", name)
print(" ", "properties:", sorted((model or {}).get("properties", {})))
print(" ", "additionalProperties:", (model or {}).get("additionalProperties"))
print("--- schemas containing termination_headers ---")
for name, model in spec.get("components", {}).get("schemas", {}).items():
if "termination_headers" in (model.get("properties") or {}):
print(name, "properties=", sorted(model["properties"]), "additionalProperties=", model.get("additionalProperties"))
PYRepository: fishaudio/docs Length of output: 1469 Align the SIP configuration update contract with the documented request. The OpenAPI document has no Add 🤖 Prompt for AI Agents |
||
|
|
||
| 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 | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: fishaudio/docs
Length of output: 8186
Qualify carrier-CDR lookup for unanswered outbound calls.
The nearby call-type table states that an unanswered outbound call has
sip_call_id: null, but the lookup instruction still says that the filter returns the matching session without this condition. An INVITECall-IDfrom an unanswered outbound call therefore cannot find the session throughsip_call_id. Qualify all three descriptions to apply only when the session storessip_call_id. KeepX-Fish-Session-Idas the alternative when the SBC or PBX logs it.Suggested documentation fix
📝 Committable suggestion
🤖 Prompt for AI Agents