LCR API contract (v1)¶
The JSON contract between siphon and the external Least-Cost-Routing API. siphon
POSTs an LcrRequest to lcr.api_url and expects an LcrResponse. LCR is
B2BUA-only — see the cookbook.
Typed models operators build against ship in the siphon-sip SDK:
A runnable reference server (FastAPI) is
examples/lcr_api_server.py.
Request (siphon → API)¶
POST {api_url} with Content-Type: application/json and the configured
auth_header (if any) as Authorization.
{
"version": "1",
"call_id": "abc123@10.0.0.1",
"from": "sip:+13105550100@sbc.example.com",
"to": "sip:+12025550123@sbc.example.com",
"dialed_number": "+12025550123",
"source": {
"ip": "192.0.2.50",
"trunk_group": "cust-trunks",
"transport": "udp"
},
"attributes": { "customer_id": "cust-42" }
}
| Field | Type | Notes |
|---|---|---|
version |
string | Contract version, currently "1". |
call_id |
string | A-leg Call-ID (for API-side correlation). |
from / to |
string | A-leg From / To URIs. |
dialed_number |
string | The number to rate — normalized by the script (canonical +E.164 recommended). |
source.ip |
string | A-leg source IP. |
source.trunk_group |
string? | Ingress trunk, if the script set it. Part of the cache key. Omitted when unset. |
source.transport |
string | udp | tcp | tls | ws | wss. |
attributes |
object | Free-form script hints. Omitted when empty. |
Response (API → siphon)¶
{
"routes": [
{ "carrier_id": "carrier-a", "gateway_group": "carrier-a",
"rate": 0.0042, "currency": "USD", "billing_increment": 60, "timeout_secs": 12 },
{ "carrier_id": "carrier-b", "next_hop": "sip:203.0.113.21:5060", "rate": 0.0051 }
],
"cache_ttl_secs": 300,
"reject": null
}
Ordered routes (cheapest / most-preferred first). Each route needs at least
one of gateway_group / next_hop / ruri.
| Route field | Type | Notes |
|---|---|---|
carrier_id |
string | Opaque id, carried into CDR/charging. Never routed on. |
gateway_group |
string? | A gateway: pool — siphon dials a healthy member, skips the route if the pool is down. Preferred (health-probed). |
next_hop |
string? | Explicit next-hop URI (when no group, or to pin the wire destination). |
ruri |
string? | Full Request-URI override (carrier IMPU shape / number format). |
tech_prefix |
string? | Dial/tech-prefix prepended to the R-URI userpart for this carrier (e.g. "1010288"). |
number_policy |
string? | Named number_policies: preset applied to this carrier's B-leg From/To/PAI (per-carrier CLI/identity shape). R-URI stays controlled by tech_prefix/ruri. |
rate |
number? | Per-minute rate (CDR/charging). |
currency |
string? | ISO 4217. |
billing_increment |
int? | Seconds (60 = per-minute, 1 = per-second). |
min_duration |
int? | Minimum billable seconds. |
timeout_secs |
int? | Per-attempt ring timeout (else the call-level default). |
headers |
object? | Headers to inject on this carrier's B-leg INVITE (applied after the header policy). |
cdr_fields |
object? | Fields siphon auto-stamps onto the CDR when this carrier wins (no per-field script). |
reroute_causes |
int[]? | SIP codes from this carrier that fail over to the next (overrides per-gateway + global). |
Top-level:
| Field | Type | Notes |
|---|---|---|
routes |
array | Ordered carriers. Empty + reject: null = no route (script answers 4xx/5xx). |
cache_ttl_secs |
int? | How long siphon may cache this decision. 0/absent = do not cache. |
reject |
object? | { "code": int, "reason": string } — siphon rejects the call with this instead of routing (API-side block). |
Behavior notes¶
- Caching — keyed by
{trunk_group}:{dialed_number}inlcr.cache, forcache_ttl_secs(orlcr.cache_ttl_secs). Redis-backed caches are shared fleet-wide. - Fallback — on transport error / timeout / 5xx, siphon uses
lcr.fallback_gateway_groupif set (a synthesized single-route decision), else the script seesNone. - Reroute — a carrier failure fails over to the next carrier only when its
code is a reroute cause (per-route
reroute_causes> per-gatewaygateway.groups[].reroute_causes> globallcr.reroute_causes, default[408, 500, 502, 503, 504]). A definitive response (486, 603) is forwarded to the caller. - Forward-compatibility — unknown response fields are ignored; new optional
fields can be added without a version bump. Bump
versiononly on a breaking change.