# Call routing

Call routing is the chain from a dialled number to the code that answers it: number to workspace, workspace to line, line to handler. Connect keeps that chain on `channel_routes`, the same table every channel's endpoints live on, and every step of it can refuse — a switched-off line, closed hours, a spent budget, or a worker that has not checked in.

- **Status:** Available
- **Audience:** both
- **Channels:** phone
- **In the app:** #/calls
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/technology/call-routing/

## Four questions between a ring and a handler

1. **Whose number is this?** The dialled number resolves to exactly one workspace. Nothing else can be decided until it has.
2. **Which line is it?** A number is an endpoint with its own purpose, hours, permissions and limits — not merely an address.
3. **Which direction is this?** A carrier posts to the same answer URL for a customer ringing in and for one of your own endpoints ringing out.
4. **Is anything able to take it?** Opening hours, the line's on/off state, the budget and, on the realtime engine, whether a worker is alive and has room.

Getting the order wrong is not a cosmetic error. Reading a line's voice settings *before* entering the workspace that owns them is how a switched-on line reads as disabled: the handler here once answered a real caller with the goodbye message, created no call record at all, and left the carrier logging a completed, charged six-second call. Every handler now runs inside the owning workspace, not only the signature check.

## A line is a row, not a table of its own

There is no separate phone-line table. `channel_routes` already answers *whose endpoint is this and what is it for* for every channel, so the telephone facts live on it: the provider, the assignment, inbound and outbound permission, hours, routing, the recording policy, capabilities, limits and status.

A person's browser line is a row in the same table with `channel='softphone'`. That is not a tidiness argument — it is what makes a browser call route through the same permission checks as a call from the business's main number, instead of through a parallel path nobody audits.

> **Note** Capability questions are asked of the provider rather than branched on its name: `telephony.can(provider, capability)`. Branching on a provider's name is how a system ends up unable to remove that provider.

## Direction is part of the routing decision

This is the least intuitive part of telephony webhooks and it has bitten this system directly. The carrier posts to one answer URL for both directions and distinguishes them with a `Direction` field. Ignore that field and an agent dialling out from the browser is greeted as though they were the customer, while the person they dialled never rings.

A browser call is worth following end to end, because almost nothing about it works the way the phrase *the dialer dials the number* suggests. The dialer sends a SIP invitation to the endpoint's registrar; the carrier does **not** dial the number inside it. It posts to the application's answer URL with an outbound direction and then does whatever the returned instructions say.

1. Register the browser endpoint over SIP-to-WebSocket.
   - Result: The line shows registered — and an endpoint not attached to the voice application registers perfectly and can still never place a call.
2. Dial. The carrier posts to the answer URL with an outbound direction.
   - Result: The handler matches the caller against the workspace's softphone routes rather than treating it as an unknown inbound caller.
3. The handler asks whether this call may be placed at all.
   - Result: It returns a dial instruction with the business's own caller identity, or a spoken refusal. The bridged leg becomes an outbound call record owned by a human rather than by the engine.

## Does Connect use call routing, and what decides it?

**Used, on both engines, for both audiences.** On the turn-based carrier path the number is attached to the application and the handler answers with the carrier's own instruction dialect. On the realtime path the number is linked at the carrier to an inbound trunk whose origination target is the project's SIP host, a dispatch rule starts the voice worker on the room, and the model holds the call directly.

Routing is also where a call gets refused, and a refusal here is spoken rather than silent. A closed or switched-off line used to be a bare error that ended with the caller hearing the call connect and drop, with no record of it anywhere. The refusal now carries the business's own closed-line message, is played on a track of its own, and leaves a call record with the outcome that explains it.

| Refusal | What the caller gets | Recorded as |
|---|---|---|
| The line is switched off | The business's closed-line message, spoken | `line_off` |
| Outside opening hours | The same message, deliberately without reciting hours | `after_hours` |
| The budget cannot cover the call | A spoken closed-line message, not silence | A refusal on the record |
| No worker has checked in for 90 seconds | The call is refused before any record exists, so a drain retries | `not_ready` |
| Every live worker is over the busy-load threshold | Refused at admission, matching what the worker tells the platform | `not_ready` |
| The configured model id is malformed | The call is refused rather than rung and dropped | `bad_model` |

## Two routing bugs worth recognising

**One URL doing two jobs.** The status callback URL and the hang-up URL were the same address, and the handler ended the call on *any* status. A routine `ringing` callback therefore hung up on a live caller. If one endpoint serves two purposes, it has to branch on the event, not on having been called.

**Verification outside the workspace.** A credential check that runs before workspace resolution reads workspace-scoped credentials as empty and rejects every genuine call before pickup. The symptom is a carrier log full of calls and an application with almost none. Reading the carrier's own call log beside the call records is the fastest audit available: a completed call on their side with no row on yours is a bug every time.

## Questions

### Can one workspace have several numbers with different behaviour?

Yes. Each number is its own row with its own purpose, hours, permissions and limits, and voice settings resolve per line as well as per workspace. A service line and a sales line can sound and behave differently while sharing everything behind them.

### What happens to a call that arrives while the line is off?

The caller hears the business's own closed-line message and the call is recorded with an outcome saying why. It is not dropped in silence, and it is not answered as though the line were open.

### Why did a browser call ring nobody?

Almost always because the endpoint is registered but not attached to the voice application. Registration and routing are separate facts: the line shows connected because the registrar accepted it, and the call has nowhere to go because no answer URL is involved.

## Related

- [How a call reaches your workspace](https://connectbyjbrh.com/docs/phone/carrier-routing/)
- [Configuring a phone line](https://connectbyjbrh.com/docs/phone/line-configuration/)
- [Business hours and the closed line](https://connectbyjbrh.com/docs/phone/business-hours/)
- [DID — a direct inward dialling number](https://connectbyjbrh.com/docs/technology/did/)
- [Calling from the browser](https://connectbyjbrh.com/docs/phone/softphone/)
- [Proving a phone number belongs to a workspace](https://connectbyjbrh.com/docs/security/number-ownership/)
- [Provider webhook signatures](https://connectbyjbrh.com/docs/technology/webhook-signature/)

## What this page is based on

- `docs-source/sources/PHONE.md` §1 — three layers and channel_routes
- `docs-source/sources/PHONE.md` §2 — six production defects
- `docs-source/sources/PHONE.md` §9 — failure and refusal
- `docs-source/sources/PHONE.md` §11 — the softphone
