Fallback
How fallback tries the next provider of a job inside one hold, what makes it move on or stop, and what route.skipped and route.summary tell you.
Fallback lets a run move to the next provider of the same job when the one you asked for misses
or fails, all inside one hold. It is off by default, on a job run too: add fallback yourself.
job:people.email.find, maxAttempts: 3, maxCostUsd: 0.05
- Provider A
missempty answer$0.0089 - Provider A, 2nd endpoint
provider_already_triedA already had an attempt$0 - Provider C
over_cost_capwould pass maxCostUsd$0 - Provider B
hitfound, walk stops$0.0198
route.summary: Provider A: miss ($0.0089). Provider B: found ($0.0198). Charged $0.0287.
Turn it on
# Not in the installed CLI (1.1.0) yet. Use MCP or REST for fallback today.{
"endpointId": "job:people.email.find",
"input": { "first_name": "Jane", "last_name": "Doe", "domain": "example.com" },
"idempotencyKey": "<new unique key>",
"fallback": { "maxAttempts": 3, "maxCostUsd": 0.1, "prefer": "cheapest" }
}--fallback and --fallback-<key> are in the CLI’s source but not shipped in the installed
looot 1.1.0. Until the next CLI release, use MCP or REST when you want fallback.
fallback is true for every default, or an object:
{ enabled, maxAttempts, maxCostUsd, prefer, exclude, stopAtFirstMiss }
| Field | Default | Meaning |
|---|---|---|
enabled |
true |
Turn fallback off while still sending the object. |
maxAttempts |
3 |
How many providers the walk may try, 1 to 10. |
maxCostUsd |
the sum of the first maxAttempts prices |
The most the whole route may hold. |
prefer |
balanced |
cheapest, reliable, fastest or balanced. Orders the job’s providers, same values as search and discover use. |
exclude |
none | Up to 50 endpoint or provider ids to skip. |
stopAtFirstMiss |
false |
Stop the walk on the first miss; the next provider is not tried. |
An unknown key or a bad value is validation_error naming the field, for example
fallback.maxAttemps.
The endpoint you named (or the job’s picked provider, for a job:<id> run) runs first. The job’s
other providers follow in prefer order.
One hold, one attempt per provider
Fallback reserves one hold for the whole route, at most maxAttempts candidates’ prices, capped
at maxCostUsd. Only attempts that actually ran are charged. A provider that would push the route
past maxCostUsd is skipped as over_cost_cap and the walk goes on to a cheaper one.
Each provider gets at most one attempt per route: once a provider has had an attempt (a miss, an
error, or a provider-side skip), its other endpoints in the same route are listed in
route.skipped as provider_already_tried and are not dispatched. A $0 attempt on a free row
does not use the provider up (see Free first). An endpoint that can write
or change something is never tried as a fallback step.
What moves on, and what stops
| What happened | Result |
|---|---|
| A hit | Stop. The walk is done. |
| A weak hit (the provider flagged it) | Stop, except a pattern guess, which moves on to the next provider (see Free first). |
| A miss (empty answer, declared not-found, a verifier’s unknown) | Next provider, unless stopAtFirstMiss is set. |
| A 5xx, timeout, or an answer that could not be read | Counted as an error. Next provider, at most 2 errors per route. |
| A 429 | Counted as an error. Next provider. |
| looot’s own account problem with the provider (402, or a 401/403 on looot’s key) | Skipped, not charged. Next provider. |
| Your input rejected (400/422) | Stop. The provider’s text names the field. |
| Your own key rejected (401/403) | Stop. |
| An async job still parked | Stop. The run is pending. |
Reading the result
Every run carries outcome and outcomeReason. A fallback run also carries route:
{
"servedBy": "hunter-email-finder",
"outcome": "hit",
"chargedUsd": 0.0345,
"capped": false,
"attempts": [
{ "n": 1, "endpointId": "zerobounce-guessformat", "provider": "zerobounce", "outcome": "weak", "status": "completed", "chargedUsd": 0.01, "ms": 412, "reason": "guessed an address from the domain's email format; verify it before use", "verdict": "guessed" },
{ "n": 2, "endpointId": "hunter-email-finder", "provider": "hunter", "outcome": "hit", "status": "completed", "chargedUsd": 0.0245, "ms": 380 }
],
"skipped": [
{ "endpointId": "tomba-email-finder", "code": "provider_already_tried", "reason": "tomba already had an attempt on this route" }
],
"summary": "zerobounce: guessed ($0.01). hunter: found ($0.0245). Charged $0.0345."
}
route.outcome is hit, weak, miss, failed, capped, pending or rejected.
route.attempts[] carries n, endpointId, provider, outcome, status, chargedUsd, ms,
reason, and verdict on a people.email.verify route (valid, invalid, catch_all,
risky, unknown or guessed). GET /v1/runs/{id}/attempts and the runs_evidence tool list
the same attempts with their receipts.
route.skipped[].code can be excluded, unavailable, unpriced, not_idempotent,
blocked_by_policy, input_invalid, needs_identity, no_credential, over_cost_cap,
insufficient_balance, not_on_this_path, max_attempts, fallback_disabled, error_bound,
provider_already_tried or stopped.
When nothing can run
When every candidate is skipped, the route is refused before any hold, error.code is
route_capped (at least one candidate was over maxCostUsd) or route_no_fit (no other fit), and
error.retryHint says “Raise fallback.maxCostUsd, or change the input; route.skipped says why
each provider was skipped.” When the requested endpoint’s own price is already above
maxCostUsd, the run is refused up front with a message naming both numbers: “fallback.maxCostUsd
($X) is below this endpoint’s price ($Y) and fallback cannot run here; raise maxCostUsd or pick a
cheaper endpoint.”
Money
See Cost cap and Settlement ceiling for how the hold is sized and how far a charge can go above the estimate.