Reason codes
Every reason a call was not routed and every reason a target was skipped — the code, its label, the sentence the console shows, whose problem it is and how to fix it.
After this page you can read any refusal the router made, say whose problem it is, and turn a code into a filter, a report row or an alert. Whenever the router declines to do something it records a reason code and a sentence. The code is short, stable and never renamed, so you can filter, group and page on it; the sentence is what a person reads.
There are two families. An unrouted reason ends the call: a gate failed before any target was dialled, or every target was tried and none took it. A call has at most one, and it is stored on the call itself. A skip reason passes over one entry in the routing plan while the call carries on down it; one call can collect many, and they live in the call's waterfall.
| Unrouted reason | Skip reason | |
|---|---|---|
| How many per call | At most one | One per target considered |
| Where it is stored | On the call — reasonCode and reasonSentence | In the call's waterfall, against that target |
| What it did to the call | Ended it | Nothing — the call moved to the next target |
| Can a report group by it | Yes: groupBy=reason | No |
| Can the call log filter on it | Yes: reason= | No — a skip code answers 400 |
| Which event carries it | call.unrouted | None; read the waterfall |
Unrouted reasons
These are listed in the order the router tests them, which is also the order of decreasing blast radius: your account, then the campaign, then the caller, then the plan. The first one that applies is the one the call gets, so a call on a campaign that is both closed and capped reads campaign_closed.
| Code | Label | The sentence | Whose |
|---|---|---|---|
workspace_inactive | Subscription inactive | The workspace's subscription is not active, so its numbers are not routing calls. Restart the plan under Wallet & billing. | Workspace |
workspace_unfunded | Wallet empty | The workspace wallet is empty, so the call was not routed. Add funds under Wallet & billing and routing resumes on the next call. | Workspace |
campaign_not_live | Campaign not live | The campaign this number belongs to is not live. Set the campaign live to start routing its calls. | Campaign |
caller_blocked | Caller blocked | The caller's number is on the blocked callers list. | Workspace |
anonymous_blocked | Anonymous caller | The caller hid their number, and this campaign does not accept anonymous callers. | Campaign |
campaign_closed | Outside campaign hours | The call arrived outside the campaign's hours of operation. | Campaign |
campaign_capped | Campaign cap reached | The campaign has reached its call cap. | Campaign |
repeat_blocked | Repeat caller | This caller has called the campaign before, and the campaign blocks repeat callers. | Campaign |
no_targets | No targets in the plan | The campaign's routing plan has no active targets. Add at least one target to the routing plan. | Campaign |
no_eligible_target | No eligible target | Every target in the routing plan was skipped, so nobody could take this call. The waterfall lists the reason for each one. | Campaign |
all_targets_failed | No target answered | Every eligible target was dialled and none of them answered. | Target |
outcome=unrouted on the call log is exactly the set of calls that ended on one of them.What to do about each one
| Code | First thing to check |
|---|---|
workspace_inactive | The plan under Wallet & funding → Plan & billing. Nothing routes until the subscription is live again; see Billing and usage. |
workspace_unfunded | The wallet balance. Routing resumes on the next call after funds arrive — there is nothing to switch back on. |
campaign_not_live | The campaign's status. A draft or paused campaign keeps its numbers and refuses their calls. |
caller_blocked | The blocked callers list. If it was a mistake, remove the number; the next call from it routes normally. |
anonymous_blocked | The campaign's anonymous-caller policy. Some traffic sources withhold caller ID by default, so check the publisher before you blame the caller. |
campaign_closed | The campaign's hours of operation and the time zone they are read in. A campaign whose hours are set in the wrong zone turns away a whole shift. |
campaign_capped | The campaign's caps. The stored sentence names the window and the figure, so you can tell an hourly cap from a monthly one without opening the campaign. |
repeat_blocked | The campaign's repeat-caller policy. This is a campaign-level rule, and it is not the same thing as a target's duplicate window. |
no_targets | The routing plan. An empty plan, or one whose every entry is inactive, refuses before it dials anything. |
no_eligible_target | The waterfall. Every target has its own skip reason beside it, and that is where the real cause is — a cap, a filter or a state rule. |
all_targets_failed | The dial attempts on the waterfall and their SIP results. Targets answered the phone network and did not answer the call. |
Skip reasons
Why one target on the routing plan was passed over. The call carried on to the next entry, so a call full of skips can still connect — and usually does. A skip is only a problem when every entry has one, which is no_eligible_target.
| Code | Label | The sentence | Whose |
|---|---|---|---|
paused | Target paused | The target is paused. | Target |
buyer_paused | Buyer paused | The buyer that owns this target is paused. | Buyer |
no_destination | No destination | The target has no phone number or SIP address to send calls to. | Target |
suppressed | Temporarily switched off | The target was switched off for a short time after repeated failures. It is retried automatically. | Target |
closed | Outside target hours | The call arrived outside the target's hours of operation. | Target |
state | State not accepted | The caller's state is not one this target accepts. | Target |
concurrency | At concurrency limit | The target is already on its maximum number of simultaneous calls. | Target |
capped | Target cap reached | The target has reached its call cap. | Target |
buyer_capped | Buyer cap reached | The buyer has reached its call cap. | Buyer |
spend_capped | Revenue cap reached | The target has reached its daily revenue cap. | Target |
duplicate | Duplicate caller | This caller already reached the target inside its duplicate window. | Target |
tag_filter | Filtered by tags | The call's tags did not pass the tag filters for this target. | Target |
no_bid | No bid | The buyer's bidding endpoint declined to bid on this call. | Buyer |
timeout | Bid timed out | The buyer's bidding endpoint did not answer in time. | Buyer |
error | Bid request failed | The buyer's bidding endpoint returned an error. | Buyer |
What to do about each one
paused,buyer_pausedandno_destinationare configuration. Somebody switched the target or its buyer off, or saved a target with nothing to dial. Nothing recovers on its own.suppressedis the platform protecting you: a destination that failed repeatedly is stood down briefly and tried again without anybody doing anything. Repeated suppression is a conversation to have with the buyer.closed,state,capped,buyer_capped,spend_cappedandconcurrencyare the terms you agreed with the buyer, working. If they skip more traffic than you expected, the terms are wrong, not the router.duplicateis the target's duplicate window, not the campaign's repeat-caller policy. The two are different settings with different owners — see Repeat and blocked callers.tag_filternames the tag that failed in the stored sentence, so you can see which rule fired without reading the whole filter set. See Filters and tags.no_bid,timeoutanderrorare the buyer's endpoint. The ping ledger has the request that was sent and the answer that came back, which is what their engineer will ask for.
The sentence is written at the time
Each table above shows the context-free sentence: the one that is true on its own, with no call in front of it. What the router actually stores is richer, because it knows the names and the figures at the moment it refuses. Those stored sentences never change afterwards — a cap you raise tomorrow does not rewrite what yesterday's call says about itself.
| Code | Context-free | As stored on a real call |
|---|---|---|
campaign_capped | The campaign has reached its call cap. | The campaign Acme Health — Inbound has reached its daily cap of 500 calls. |
capped | The target has reached its call cap. | The target BlueSky Legal — Intake has reached its hourly cap of 25 calls. |
spend_capped | The target has reached its daily revenue cap. | The target BlueSky Legal — Intake has reached its daily revenue cap of $2,500.00. |
concurrency | The target is already on its maximum number of simultaneous calls. | The target Acme Health — Dallas floor is already on its maximum of 6 simultaneous calls. |
state | The caller's state is not one this target accepts. | The target BlueSky Legal — Intake does not accept calls from NV. |
duplicate | This caller already reached the target inside its duplicate window. | This caller already reached the target Acme Health — Dallas floor in the last 30 days. |
repeat_blocked | This caller has called the campaign before, and the campaign blocks repeat callers. | This caller already called the campaign on 2026-08-14, and the campaign blocks repeat callers. |
all_targets_failed | Every eligible target was dialled and none of them answered. | 3 targets were dialled and none of them answered. |
timeout | The buyer's bidding endpoint did not answer in time. | The bidding endpoint of the buyer BlueSky Legal did not answer within 1,500 ms. |
error | The buyer's bidding endpoint returned an error. | The bidding endpoint of the buyer BlueSky Legal returned HTTP 503: upstream unavailable. |
paused, no_destination, campaign_closed — stores its context-free sentence unchanged.A code the platform does not recognise still produces a sentence rather than an error, because this runs inside a live call: an unknown code reads "The call was not routed (<code>)." Treat an unknown code the same way — as "not routed" — and show the sentence, which always arrives beside it.
Whose problem it is
Every code carries an owner, which is the console's way of sending a reader to the right screen. It is one of five words, and it is stable — an integration can route an alert on it without parsing English.
owner | Means | Where to go |
|---|---|---|
workspace | Your account: the subscription, the wallet, the blocklist. | Wallet, Billing and usage, blocked callers. |
campaign | The campaign's own settings: status, hours, caps, policies, routing plan. | Campaigns overview. |
target | One destination: its status, hours, caps, states, duplicate window, or the phone at the other end. | Targets. |
buyer | The company behind the target: paused, capped, or a bidding endpoint that would not answer. | Buyers. |
platform | Only ever returned for a code the platform does not recognise. Tell us. | — |
What a publisher is told
A publisher sees the calls they sent you, and they see why a call was not connected — but not in your words. Your sentences name your wallet, your plan and your buyers' caps, and none of that is a partner's business. So a publisher's call page is built from a different table, never filtered from yours: nothing the router stored is copied into a publisher's answer.
What a publisher needs to know is whether the fault was theirs — the caller, the hour, the cap they were given — or not. That is exactly what these sentences say, and no more.
| Code | What the publisher reads |
|---|---|
workspace_inactive | The campaign was not accepting calls when this call arrived. |
workspace_unfunded | The campaign was not accepting calls when this call arrived. |
campaign_not_live | The campaign was not accepting calls when this call arrived. |
caller_blocked | This caller's number is blocked on the campaign. |
anonymous_blocked | The caller hid their number, and this campaign does not accept anonymous callers. |
campaign_closed | The call arrived outside the campaign's hours of operation. |
campaign_capped | The campaign had reached its call cap. |
repeat_blocked | This caller has called the campaign before, and the campaign does not accept repeat callers. |
no_targets | No buyer was available to take this call. |
no_eligible_target | No buyer was available to take this call. |
all_targets_failed | No buyer answered this call. |
- The first three codes collapse into one sentence on purpose. "The wallet is empty" is a thing a seller would much rather tell a partner itself, and a lapsed subscription is nobody else's business.
- The codes that are genuinely about the traffic — a blocked caller, a withheld number, the hour, a repeat caller — are passed through in full, because a publisher can act on them.
- Skip reasons are never shown to a publisher at all. They are about your buyers, and a publisher does not see your buyers.
- Your own call page still shows the full sentence. The two are different views of one call, not two records; see The call waterfall.
Reading a code out of the API
A call carries reasonCode and reasonSentence, and both are null on a call that routed. reasonSentence is the sentence the router stored at the time, falling back to the context-free one if none was stored.
{
"id": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"ref": "CA-482137",
"outcome": "unrouted",
"status": "no_answer",
"reasonCode": "campaign_capped",
"reasonSentence": "The campaign Acme Health — Inbound has reached its daily cap of 500 calls."
}The call.unrouted event carries the code as reason, the stored sentence beside it, and action — what the caller then heard.
{
"id": "req_5f0c2a91b7d34e10",
"event": "call.unrouted",
"createdAt": "2026-09-20T14:03:12.418Z",
"data": {
"callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"ref": "CA-482137",
"status": "rejected",
"campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"campaignName": "Acme Health — Inbound",
"reason": "workspace_unfunded",
"sentence": "The workspace wallet is empty, so the call was not routed. Add funds under Wallet & billing and routing resumes on the next call.",
"action": "hangup"
}
}Worked example: which refusal is costing the most
Yesterday was quiet on Acme Health — Inbound and nobody knows why. Ask for the calls behind one suspicion, then ask the whole day to rank itself. This needs a key with calls:read and reports:read; see API keys.
# Every call yesterday that a full campaign cap turned away
curl -G "https://api.buy3.io/api/v1/calls" \
-H "Authorization: Bearer $BUY3_API_KEY" \
--data-urlencode "reason=campaign_capped" \
--data-urlencode "from=2026-09-19T05:00:00Z" \
--data-urlencode "to=2026-09-20T05:00:00Z"
# And the same window grouped by reason, worst first
curl -G "https://api.buy3.io/api/v1/reports/summary" \
-H "Authorization: Bearer $BUY3_API_KEY" \
--data-urlencode "groupBy=reason" \
--data-urlencode "from=2026-09-19T05:00:00Z" \
--data-urlencode "to=2026-09-20T05:00:00Z"{
"groupBy": ["reason"],
"rows": [
{
"dimensions": [{
"dimension": "reason",
"key": "campaign_closed",
"label": "Outside campaign hours",
"sentence": "The call arrived outside the campaign's hours of operation.",
"owner": "campaign"
}],
"calls": 61,
"connected": 0,
"converted": 0,
"unrouted": 61,
"connectRate": 0,
"conversionRate": null,
"revenueCents": 0
}
]
}Sixty-one calls arrived outside the campaign's hours, and the row hands you the label, the sentence and the owner without a second lookup. owner is campaign, so the fix is the campaign's hours — most often a schedule saved in the wrong time zone. Note that a report grouped by reason counts only unrouted calls: routed calls have no reason and are left out of the rows, though they are still in totals.
Using the codes
- Filter the call log by reason to see every call a closed campaign or a full cap cost you, then export it — see Exports.
- Group a report by
reasonto find the one worth fixing first. Rank bycalls, not by how loud the complaint was. - Alert on them: subscribe a webhook to
call.unroutedand page somebody whenworkspace_unfundedorno_eligible_targetturns up. The first is money; the second is a plan that has quietly stopped having anyone in it. - Branch on the code, print the sentence. The code is the contract and the sentence is the copy. Writing your own English for a code means maintaining a second copy of this page.
- Expect new codes. They are only ever added, never renamed or removed. An integration that treats an unknown code as "not routed" and shows its sentence never needs changing.
