---
title: Fallback
description: 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.
sidebar:
  icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="looot-icon"><circle class="li-i" cx="4.75" cy="12" r="2.25"/><circle class="li-i" cx="12" cy="12" r="2.25"/><path class="li-i" d="M7 12h2.75M14.25 12h2.75"/><circle class="li-af" cx="19.25" cy="12" r="2.5"/></svg>'
---

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.

<FallbackTimeline />

## Turn it on

<CodeGroup>

```bash CLI
# Not in the installed CLI (1.1.0) yet. Use MCP or REST for fallback today.
```

```json MCP
{
  "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" }
}
```

</CodeGroup>

`--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`](/errors/rest-errors#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](/concepts/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](/concepts/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`](/concepts/outcomes) and `outcomeReason`. A fallback run also carries `route`:

```json
{
  "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`](/mcp-tools/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`](/errors/rest-errors#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`](/errors/job-refusals#route_capped) (at least one candidate was over `maxCostUsd`) or [`route_no_fit`](/errors/job-refusals#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](/money/cost-cap) and [Settlement ceiling](/money/settlement-ceiling) for how the
hold is sized and how far a charge can go above the estimate.

<Related />
