Call Routing
Overview
The Call Routing service decides what happens to an inbound call. Each call arrives on a line (a SIP address the service listens on), and the line's configuration says how to dispose of it — loop it back, bridge it to an operator or PBX, reject it, join it to a call already on hold, or ask an external system what to do. The service runs stateless: several instances can run at once and share call load automatically.
Everything is driven by line configuration. Changing a line's disposition is a config edit plus ./sbn-media config reload — no restart, and the change applies to the next call.
Dispositions (modes)
A line's mode selects one disposition:
| Mode | What it does |
|---|---|
echo | Loops the caller's own audio back to them. Test/load parity. |
park | Answers and holds the leg open silently — nothing is streamed back — until it is joined to a held call or hangs up. The silent-hold sibling of echo; an optional message can be played on the held leg. |
bridge | Dials a second destination and joins the two legs into one two-way call. |
reject | Refuses the call with a SIP status code (default 603 Decline) before answering. |
dtmf | Answers, asks the caller to key an account (optionally a two-field prefix#account#), then bridges them to that account's held call — or, if the line names a resolver, hands the keyed digits to it for disposition (see Held calls and Resolve). |
heldcall | Bridges straight to a held call whose account is already in the dialled address — no keypad step. |
script | Hands the decision to a small Lua script that runs per call and returns a disposition. |
resolve | Asks an external resolver over the message bus what to do, per call, and applies the reply (see Resolve). |
bridge, echo, park, and reject decide from the line config alone. dtmf, heldcall, script, and resolve decide per call from live state (a keyed account, a script, or a remote answer).
Echo
The service answers and streams the caller's own audio back to them. It bridges nothing and holds no other leg. Used to prove a line is up and to measure the media path under load.
Park
The service answers the call and holds it open, but — unlike echo — streams nothing back, so the caller hears silence (or a short spoken message, if one is configured) instead of their own audio. The leg stays answered until something joins it to a held call or it hangs up. This is the disposition behind the operator callback: when an operator dials in and the system is dialling their device back, the operator's leg is parked — answered and quiet — until the device reconnects and the two are joined into two-way voice, rather than being echoed their own audio while they wait.
Bridge
The service answers, dials a second destination on the outbound trunk, and joins the two legs into one two-way conversation. The destination can be a full SIP address, a GSM number, or a bare extension, and it may be templated from the inbound call. In this version both legs must negotiate the same codec; a mismatch is refused rather than bridged into silence. This is the everyday "connect the caller to an operator or PBX" disposition, and it is the mode most media taps attach to.
Reject
The service refuses the call with a SIP status code before answering (so the caller is never charged for a connected call). The code defaults to 603 Decline; any SIP status can be configured. Used to turn a line off, block out-of-hours calls, or act as the fail-closed fallback for the dynamic modes below.
Script
The disposition is produced by a small Lua script that runs for the call: an on_invite entry point inspects the call and returns one of the static dispositions (bridge here, reject with this code, and so on). It lets simple per-call logic — time of day, caller pattern — live in a script instead of requiring a code change. If the script errors or returns nothing usable, the line falls back to its fallback disposition (reject by default), so a broken script never hangs a call.
Held calls — operator callback to a parked two-way alarm
When a two-way alarm comes in (for example a P100 lift alarm), the receiving line can hold the voice leg open and register it, keyed by its alarm account, in a short-lived held-call registry (the entry expires after a few minutes if nobody joins it). An operator can then be connected to that exact held leg, giving immediate two-way voice with the site — without the alarm having to re-dial.
Two lines reach a held call:
dtmf— a single shared operator line. The operator dials it, hears a prompt, keys the
alarm account (terminated with #, or auto-completing at the account width), and is bridged to that account's held leg. Configurable prompts cover the account-unknown, call-already-ended, and connect-failed cases. The account is combined with the line's prefix to look up the held call, so the operator line's prefix must match the receiving line's.
When the line names a resolver, the service hands the keyed digits to it instead of looking up the held call directly, and the resolver returns the disposition — bridge (for example to a registered operator device), defer to a callback, or reject. An optional second field (enabled with capturePrefix, prompted by prefixPrompt) collects a prefix#account# pair for resolvers that key on both. The digits can be keyed in-band or as out-of-band RFC 2833 telephone-events.
heldcall— the account is carried in the dialled address itself
(sip:<prefix-account>@line), so there is no keypad step. Used when the caller (or an upstream system) already knows exactly which held leg to join.
Joining a held call is subject to the same tenant rules as any bridge: an operator can only be joined to a held leg belonging to their own dealer, unless that dealer is on the line's cross-dealer allow-list.
Resolve — per-call disposition from an external system
mode: resolve lets a line delegate the decision to an external resolver instead of baking it into config. On each inbound call the service issues a synchronous request on the bus and applies the disposition the resolver replies with: bridge to this destination, join this held call, hold the leg (park), or reject — and which media taps to run on the resulting call.
The request subject follows the standard NATS convention (<message-type>.<domain>.<subdomain>.<environment>.<tenant-id>.<site-id>.<action>), with the resolver as a trailing discriminator after the action so requests reach the specific resolver the line expects:
for example request.media.routing.eucloud.saf_arc-12345.saf_sl6-24123456.resolve.safeline. It is a request/reply exchange, so no transaction id is needed. Each resolver subscribes with the environment, tenant, and device wildcarded and only its own name fixed at the end:
so multiple resolvers coexist without competing for the same subject — discrimination is by resolver, not by tenant (two resolvers may serve overlapping tenants). The line's resolver: setting supplies <name>, and the tenant/device the service already knows for the call fill the <tenant>/<device> fields.
This is how a system that already owns the routing brain (customer accounts, on-call schedules, per-device settings) can drive the routing service without that logic living in line config. Two properties matter operationally:
- Fail-closed. If the resolver doesn't answer within the budget (a few seconds), or replies
with something unusable, the call falls back to the line's fallback disposition (reject by default). A slow or down resolver never hangs a call.
- Trust boundary. The resolver chooses the destination; the routing service still enforces
its own tenant and loop guards on that destination. A resolver cannot talk the service into bridging across dealers that the line isn't allowed to bridge into.
Media taps — recording, transcription, translation
Any bridged or held call can carry media taps — services that observe or transform the audio. Taps are declared per line (media:), or, on a resolve line, chosen per call by the resolver's reply.
- Record — stores a call recording (one per dealer). Requires
consent: trueand plays a
spoken announcement before capture.
- Transcribe — speech-to-text transcript. Also consent-gated and announced.
- Translate — live, two-way translation between the two legs of a bridged or held call.
- Simplex — echo/feedback control for hands-free room audio (e.g. a lift cabin): it opens
the site microphone only when the operator isn't speaking, and the operator can steer it from the keypad (speak / listen / automatic).
How translation is controlled
Translation runs in one of two ways, and the difference matters:
- Always-on — translation is active for the whole call. Used on a plain bridge where every
call on the line should be translated.
- Operator-controlled (press-0) — the operator toggles translation (or transcription) on and
off mid-call from the keypad. This is the model for the operator-callback / held-call flow, where the operator decides in the moment whether the site audio needs translating, and a spoken prompt confirms each time it starts and stops. Operator-controlled translation is part of the simplex control on that call, so the same keypad that steers speak/listen also starts and stops translation.
The source language for translation comes from the call's configuration (per device), so the operator side always hears their own language.
Security and safety
Independent of mode, the service enforces:
- No cross-tenant bridging unless the target dealer is explicitly on the line's cross-dealer
allow-list.
- No loops back to the originating line.
- Codec compatibility — a call whose audio format can't be matched to the far leg is refused
rather than bridged into silence.
Configuration examples
Requirements
- Runs when: at least one line has a
routingblock. - Dependencies: NATS (message bus), SIP Server, SIP Media; the outbound trunk for bridges;
a reachable resolver for resolve lines; the media services for any taps in use.
- Instances: multiple deployments share call load automatically.