Skip to content
looot docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.

Example

job:people.email.find, maxAttempts: 3, maxCostUsd: 0.05

  1. Provider Amissempty answer$0.0089
  2. Provider A, 2nd endpointprovider_already_triedA already had an attempt$0
  3. Provider Cover_cost_capwould pass maxCostUsd$0
  4. Provider Bhitfound, 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.

Was this page helpful?