# How a call reaches your workspace

A call arrives as a signed request naming the number that was dialled. Connect matches that number to a `channel_routes` row, learns which workspace owns it, and enters that workspace before it reads a single setting. Everything after that point — credentials, greeting, hours, behaviour — is read inside the owning workspace, because reading it outside answered live calls with a goodbye message.

- **Status:** Available
- **Audience:** both
- **Channels:** phone
- **In the app:** #/calls
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/phone/carrier-routing/

## The route, in order

1. The carrier posts to Connect's answer URL, or opens a SIP leg into the project's trunk.
2. The signature on the request is checked before anything in it is believed. See [Carrier callback verification](/docs/phone/carrier-verification/).
3. `Direction` is read, because the same URL serves a customer calling in and one of the workspace's own endpoints calling out.
4. The dialled number is matched to a `channel_routes` row, which names exactly one workspace.
5. The handler enters that workspace. Only now are settings, credentials and behaviour read.
6. The line's permissions and hours are applied, and the call is answered, refused in words, or handed to the realtime worker.

Steps four and five are one unit and cannot be reordered. Workspace resolution is not a lookup that happens somewhere near the beginning — it is the boundary that makes every subsequent read return this business's data instead of nothing.

## The defect this rule exists to prevent

`carrier_incoming` once read the voice settings **before** entering the workspace. Outside the workspace those settings resolve to an empty row rather than to an error, so a line that was switched on read as `enabled=False`. Connect answered a real caller with the goodbye message and created no `Call` row at all. The carrier, meanwhile, logged a completed, charged six-second call — so the only place the failure was visible was the provider's own billing.

The fix was structural rather than local: every handler now runs inside the owning workspace, not merely the signature check. A related failure had the same shape from the other end — `_carrier_form` verified the workspace's own number outside any workspace scope, so the workspace-scoped credentials read as empty and every real call was answered `403` before the caller heard anything.

> **Careful** A row with no workspace stamp is invisible, not missing. That distinction is the whole class of bug: the query succeeds, returns nothing, and the code treats "nothing" as a legitimate answer. On a phone line the legitimate-looking answer is "this line is off".

## Direction: one URL, two meanings

A carrier posts to the same answer URL for a customer dialling your business number and for one of your SIP endpoints placing a call out. For a long time `Direction` was never read, so an agent dialling from the browser was greeted as though they were the customer — and the person they had dialled never rang at all. The parameter now decides which handler owns the request before anything else is done with it.

| `Direction` | Who is calling | What Connect does |
|---|---|---|
| inbound | A member of the public dialled your number | Resolve the workspace, apply hours and permission, answer or refuse in words |
| outbound | One of the workspace's own softphone endpoints is placing a call | Match `From` against the workspace's `channel='softphone'` routes, ask `may_place_call`, then bridge or refuse aloud |

## Numbers that resolve to nothing

If the dialled number matches no `channel_routes` row, there is no workspace to enter, so there is nothing to read and nothing to write. This is worth recognising quickly, because it looks from the caller's side like a fault in the conversation and it is not: the number is pointed at Connect at the carrier, but no workspace has claimed it here.

**Number claimed, line on** — Normal handling. A `Call` row exists from the moment the call is accepted.
**Number claimed, line off or closed** — A spoken refusal in the business's own name, an `ended` row, and outcome `line_off` or `after_hours`.
**Number not claimed** — No workspace, no settings, no row. Fix it by claiming the number, not by changing the voice.
**Claimed by another workspace** — The call is handled by that workspace under its rules. A number belongs to exactly one.

The fastest audit for any of this is to read the carrier's own call log next to the `calls` list for the same window. A call the carrier records as completed with no row in Connect is a routing or verification problem every time, and it points at this page rather than at the transcript.

## Questions

### Can one number serve two workspaces?

No. A number resolves to exactly one `channel_routes` row and therefore to one workspace. That is the same rule that makes tenant isolation checkable: a call cannot be ambiguous about whose data it may read.

### Why does Connect check the signature before resolving the workspace?

Because the request has to be proved genuine before any value in it — including the dialled number — is used to select a workspace. Resolving first would let an unverified request choose whose settings are loaded.

### A caller says the line connected and then dropped. Where does that come from?

Historically from a bare refusal that the worker answered by deleting the room: the caller heard the call connect and drop, and no `Call` row existed. A refusal now carries a spoken closed-line message and the row is written as `ended`. A connect-and-drop today is worth checking against worker capacity instead.

## Related

- [Carrier callback verification](https://connectbyjbrh.com/docs/phone/carrier-verification/)
- [Answering inbound calls](https://connectbyjbrh.com/docs/phone/inbound-calls/)
- [Your business number in Connect](https://connectbyjbrh.com/docs/phone/business-number/)
- [Calling from the browser](https://connectbyjbrh.com/docs/phone/softphone/)
- [A row with no workspace stamp is invisible, not missing](https://connectbyjbrh.com/research/invisible-not-missing/)
- [Security and isolation](https://connectbyjbrh.com/docs/security/)

## What this page is based on

- AGENTS.md §9a — routing and the workspace boundary (via docs-source/sources/PHONE.md)
- PROJECT-STATE.md — the six telephony defects (via docs-source/sources/PHONE.md §2)
- `backend/app/voice_carriers.py` — `carrier_incoming` and `_carrier_form`
- `backend/app/workspace_kernel.py` — workspace scope
