# The budget that stops a call

`budget_refusal` is asked twice: before Connect places an outbound call, and before it answers an inbound one. A refused outbound is simply not placed. A refused inbound is answered and told — the caller hears the line's own closed-line message in the business's name and the call ends, rather than connecting to silence.

- **Status:** Available
- **Audience:** both
- **Channels:** phone
- **In the app:** #/phone, #/usage
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/phone/voice-budget/

## The two questions

A voice call is the most expensive thing Connect does per minute, and it is also the one thing that cannot be half-done. So the budget is asked before anything is committed, at both of the moments where a call can begin.

**Before an outbound** — Asked before the dial. Nothing rings, no session opens, no row is created for a call that was never going to complete. A due follow-up refused here stays due and is attempted again while it is inside its window.
**Before answering an inbound** — Asked while the phone is still ringing, before the conversation is set up. The call is answered — it has to be, or the caller learns nothing — but it is answered with a message rather than with a conversation.

The same gate stands in front of every kind of call: one the engine places, one a follow-up triggers, one the Assistant asks for, one a person dials from the browser line. There is a single outbound path, so there is a single place this is decided.

## What a refused caller hears

Not silence, and not a provider's error tone. The refusal brief carries `closed_line_text` — the line's own closing message, in the business's own name. It is deterministic text, it deliberately contains **no opening hours**, and it is warmed alongside the greetings under the `closed` kind so it is already audio when it is needed.

The worker plays it on a track of its own, posts `refused_line`, and hangs up. The row is `ended` with an outcome that names the reason. A caller gets a short, complete interaction with a business rather than a call that connects and drops.

> **Note** The behaviour predates the budget check and was built for a switched-off or closed line, which used to be a bare error the worker answered by deleting the room: the caller heard the call connect and drop, and no call record existed at all. Every refusal now goes through the same spoken path, which is why a budget refusal sounds like a closed line rather than like a fault.

## The breaker is not the budget

`ai_quota` is a separate mechanism with a different shape. It opens a breaker when the provider answers 429 or the key is revoked — a statement that the provider is refusing right now, rather than a statement about what the workspace has chosen to spend.

|  | Budget | Quota breaker |
|---|---|---|
| Decided by | The workspace's own spending limits | The provider refusing or the key failing |
| Scheduled work | Refused | Skipped |
| A caller already on the line | Refused with the spoken message | Still attempted |
| Clears when | Budget is available again | The provider stops refusing |

The asymmetry in the third row is deliberate. A background task can wait an hour and lose nothing. A human holding a phone cannot, so when the provider is merely rate-limiting, the attempt is made — it may well succeed, and failing without trying would be a certainty rather than a risk.

## Living with a refusal

1. Check which refusal you have. The call record names the reason, and a budget refusal and a provider refusal look nothing alike in the record even though they look similar from a desk.
   - Result: Only one of them is fixed by changing a limit.
2. For a budget refusal, look at what is consuming it before raising it.
   - Result: Most of a voice bill is carried context re-billed every turn, so a large brief can cost more than an extra hour of calls.
3. For a breaker, check the key before the quota.
   - Result: A revoked key and an exhausted quota open the same breaker and have completely different fixes.
4. Watch the follow-up queue while either is open.
   - Result: Due call-backs accumulate rather than failing, and they are abandoned once a day late — so a refusal that lasts overnight quietly costs you the commitments as well as the calls.

## Questions

### Does a refused inbound call still cost money?

It is answered, so there is a call, and the closed-line audio is usually already synthesised and cached. What it does not do is open a model session, which is where the cost of a real conversation is. The refusal is cheap precisely because it happens before that.

### Will a caller know they were refused for budget?

No, and deliberately so. They hear the line's closing message in the business's name. Announcing an internal spending limit to a customer would be worse than the refusal itself; the reason is recorded where the business can see it.

### Why does scheduled work stop while live calls carry on during a breaker?

Because the cost of waiting is different. Skipping a background task delays something; abandoning a person mid-ring ends something. When the provider is rate-limiting rather than dead, the live attempt is worth making.

## Related

- [What a call costs](https://connectbyjbrh.com/docs/phone/voice-cost/)
- [Switching the line off](https://connectbyjbrh.com/docs/phone/line-off/)
- [Business hours and the closed line](https://connectbyjbrh.com/docs/phone/business-hours/)
- [Executing phone follow-ups](https://connectbyjbrh.com/docs/phone/phone-followup-drain/)
- [Answering inbound calls](https://connectbyjbrh.com/docs/phone/inbound-calls/)
- [Greeting warm-up](https://connectbyjbrh.com/docs/phone/greeting-prewarm/)

## What this page is based on

- Connect phone source pack — `docs-source/sources/PHONE.md` §4, cost and what stops a call
- Connect phone source pack — `docs-source/sources/PHONE.md` §9, the refusal brief and the closed line
- Connect capability registry — `docs-source/facts.py` (`voice_cost_metering`, `phone_followup_drain`)
