# Inbound phone call, end to end

A call reaches the carrier, the carrier calls Connect, and the handler enters the owning workspace before it reads a single setting. The line is resolved, a `calls` row is opened, the voice answers under that line's persona and rules, and the ending is attributed, costed and turned into a summary, a lead and a follow-up. Each stage can fail on its own, and each has its own recovery.

- **Status:** Available
- **Audience:** both
- **Channels:** phone
- **In the app:** #/calls, #/phone-advanced
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/workflows/inbound-phone-call/

## The chain, for one ringing number

This is the same chain every flow in Connect runs; what changes is what each stage means when the trigger is a person dialling a number and waiting on the line. The clock is the difference. An email that takes four seconds longer costs nothing; four seconds of silence on a live call is a caller deciding nobody is there.

1. Trigger — the PSTN delivers a call to a number the workspace owns.
2. External event — the carrier posts to the answer URL, signed, with `Direction=inbound`.
3. Authentication and workspace resolution — the signature is checked and the handler enters the owning workspace *first*.
4. Ingest — the carrier's own webhook shape is normalised in `voice_carriers.py`, the only file that speaks a provider's dialect.
5. Canonical record — a `Call` row is created with ring time, direction and line; `call_events` stores the event once, keyed on the signature nonce.
6. Reasoning — the turn-based engine writes a reply per turn; the realtime engine listens and speaks at once.
7. Knowledge, memory and rules — `voice_engine.brief()` renders persona, line purpose, contact facts, standing instructions, knowledge and hard rules, once per call, and `voice_region.plan` writes the language rule into it from the evidence about the caller.
8. Autonomy and approval — `budget_refusal` is asked before the call is answered; guardrails are re-checked on every sentence spoken.
9. Action through a provider — the carrier reads out the reply, or a Gemini Live model speaks directly into the LiveKit room.
10. Result — the call ends; `hangup_by` is attributed to caller, model, owner, engine, carrier or agent.
11. Relationship, timeline and memory — summary, lead capture, memory write and follow-up, all through `voice.end_call`.
12. Audit, usage and Needs You — audio-token cost is booked, the call is on the timeline, and anything needing a person is queued.

> **Note** Stage three is not a formality. Reading a setting before entering the workspace made a switched-on line read as `enabled=False`; the caller got the goodbye message, the carrier logged a completed six-second call, and no `Call` row existed at all. Every handler now runs inside the owning workspace, not just the signature check.

## Stage by stage, with the failures beside them

| Stage | What you see | What changes | What can fail |
|---|---|---|---|
| Carrier receives the call | Nothing yet — the caller hears ringing | Nothing in Connect | The number is not linked to the voice application, so no webhook is ever sent |
| Webhook, signature, workspace | Nothing | `call_events` gains one row, nonce-guarded | A signature checked outside workspace scope reads the credentials as empty and answers 403 before pickup |
| Line resolved | Ringing continues | The `channel_routes` row supplies purpose, hours, permissions and capabilities | Hours closed or line switched off — a refusal brief, not a fault |
| Answer | The greeting, ideally at once | `Call` row opened; `greeting.prewarmed` records whether a warmed line was used | A greeting that cannot be synthesised ends the call with `engine_error {at: greeting}` |
| Conversation | Question, answer, interruption | `call_turns`, `turn_timing` and `barge_in` events | Model refuses the session; watchdog nudge cancels a generation in flight; caller not heard |
| Ending | The line clears | `hangup_by`, outcome, disposition kept separate | A status callback that ends the call on *any* status — a `ringing` callback once hung up on a live caller |
| Digest | A summary appears shortly after | Summary, lead, memory, follow-up; `facts.digest` moves off `pending` | A call with under two caller lines and five caller words is never summarised, on purpose |
| Cost | The call appears in usage | Audio and text tokens metered separately; duration charged when no tokens are reported | A session that restarted bills only the survivor unless every session is merged |

## Answering is not one behaviour

Which engine the workspace has chosen changes what happens between the greeting and the goodbye, and nothing else in the chain.

**Turn-based (carrier path)** — A webhook carries one heard sentence, Connect writes a reply, the carrier reads it out. Simple, provider-driven, and paced by the carrier's own speech detection.
**Realtime** — The number is linked at the carrier to an inbound trunk, a dispatch rule spawns the `connect-voice` worker on the room, and the model listens and speaks at once. The worker holds no business logic: it is handed a brief and posts back every sentence, every event and the ending.

The language the call is held in is decided in the same place, before anybody speaks. The greeting is pre-synthesised, so it opens the call; from the first reply onwards `voice_region.plan` governs. A pinned language holds the call. A `preferred_language` remembered from an earlier call is spoken from that first reply, and only a whole sentence in another language overrides it. Where nothing is remembered but the contact's city or state maps to a region, the call opens in the line's language, does not assume the regional one, and moves to it the moment the caller uses it — two or three words, mixed with English or not. With nothing on file, it takes a whole sentence. Nothing is offered and no phrase is quoted, because on a production call a quoted sample was read out to the caller word for word.

The realtime floor is the model's first token plus its end-of-turn detection — 3.3 s median reply on the best measured call, not the one to two seconds people expect from a demo. Connect's default model reaches first audio in about 0.6 s on the host, against 1.2–2.2 s for the 2.5 native-audio model, which is why the default is what it is.

## What counts as the call having worked

Not "the caller hung up politely". A verified inbound call has all of these, and the audit is worth running against the carrier's own log rather than against your impression of the day:

- A `Call` row exists for every call the carrier logged as answered. A `Completed` on their side with no row on yours is a defect every time.
- `hangup_by` names somebody other than the fallback. Before attribution existed, every ending that was not the caller's read as the agent's.
- The outcome is a conversation word, not `no_answer` or `not_reached` — those two mean the call rang and nobody picked up, or never rang at all.
- The cost is booked from measured audio tokens, or from duration when the session reported none. Never zero.
- Anything promised on the call exists as a follow-up with a due time, not as a sentence in a transcript.

## Questions

### Why does the caller sometimes hear the greeting and then silence?

Historically two causes, both fixed and both worth recognising. A host-less action URL meant the carrier had nowhere to send the next turn. And the `<Gather>` was the last element in the document, so when nothing usable was heard the carrier ran off the end of the XML and hung up. A `<Redirect>` now follows the gather, back to the turn handler, which answers empty speech rather than dropping the line.

### Is the call recorded?

No. Recording is a foundation capability: it is asked of the provider through `telephony.can` rather than assumed, and it is not enabled on the live carrier. What you get instead is the transcript, the summary and the turn timings, which is what most reviews of a call actually need.

### What happens if the daily budget is already spent when someone calls?

`budget_refusal` is asked before the call is answered, and a refused inbound call gets the spoken closed-line message in the business's own name — not silence, and not a dropped line.

## Related

- [Outbound phone call, end to end](https://connectbyjbrh.com/docs/workflows/outbound-phone-call/)
- [From a call to a customer relationship](https://connectbyjbrh.com/docs/workflows/call-to-relationship/)
- [The call connected and nobody spoke](https://connectbyjbrh.com/docs/troubleshooting/call-answered-in-silence/)
- [Phone and voice in Connect](https://connectbyjbrh.com/docs/phone/)
- [Idempotency for retried telephony webhooks](https://connectbyjbrh.com/research/idempotent-telephony-webhooks/)

## What this page is based on

- Reconciled 2026-09-10 against `f5ea518` — `backend/app/voice_region.plan`, read from the code rather than its docstring
- AGENTS.md §9a — the phone system, defects 2, 3, 4 and 5
- `backend/app/voice.py`, `voice_carriers.py`, `voice_engine.py`
- Connect capability registry (docs-source/facts.py)
