Release candidate — 1.0.0-rc.7
StitchAPI
GuidesResilience

Verdict

Declare what counts as success — accept a non-2xx as a normal result, or fail a 200 whose body says it failed.

Every call ends with one decision: is this response a result, or an error? The stitch calls that decision the verdict, and verdict is where you configure it.

By default every status >= 400 is a failure: the stitch throws a StitchError, so a 404 you expected — a resource that's gone, where you fall back to a broader call — has to be handled in a catch. That turns ordinary control flow into exception handling.

Where it runs

The verdict is rendered at stage 4 of the pipeline, by the surface's interpret hook. verdict is the declarative input that hook reads:

call → throttle → request → retry → interpret → paginate → pick → validate → cache → result

                                  the verdict, from `verdict`

That is why it is one envelope rather than a loose status option: it names a stage, and its members are the inputs to that stage's decision. stitch diagram and the MCP teaching list both render it, so the decision is visible rather than implied.

accept — a non-2xx that is a normal result

An accepted status no longer throws: its response body becomes the result and flows through the same pipeline a 2xx does — interprettransformpickoutput validation — so the happy path stays on the happy path.

import {  } from 'stitchapi';

const  = <{ : string } | { : true }>({
    : 'https://api.example.com',
    : '/items/{id}',
    : { : [404] }, // a 404 is "not found", not a thrown error
    : () =>
        // Reshape the 404 payload into a sentinel your code can branch on.
         && typeof  === 'object' && 'id' in 
            ? 
            : { : true },
});

const  = await ({ : { : 'abc' } });
if ('missing' in ) {
    // fall back to a broader call — no try/catch needed
}

A 404 here resolves with the response body instead of rejecting. Everything else (401, 500, …) still throws a StitchError exactly as before — accept is additive, never a blanket "ignore failures".

A number, a list, or a predicate

accept is a StatusMatch: a single status, a number list, or a predicate (status) => boolean. A bare number is shorthand for its one-element list (accept: 404accept: [404]). Use a predicate to accept a range:

import {  } from 'stitchapi';

const  = ({
    : 'https://api.example.com',
    : '/probe',
    // accept every 4xx as a result; a 5xx still throws
    : { : () =>  >= 400 &&  < 500 },
});

flag — a 200 whose body says it failed

Some APIs — older ones especially — answer 200 OK and put the real outcome in the payload: { ok: false, code: 'RATE_LIMITED' }. flag is a dot-path (the same shape as pick) to a field that is explicitly falsy on failure:

import {  } from 'stitchapi';

const  = ({
    : 'https://api.example.com',
    : '/legacy',
    : { : 'meta.success' }, // a 200 with meta.success: false is a failure
});

accept and flag move the verdict in exactly one direction each — accept can only turn a failure into a success, flag only a success into a failure. Neither invents a verdict from absence.

It has to say failure

flag is three-state, and only one state is a verdict:

at the pathverdict
present, truthysuccess — the flag confirms it
present, falsy (false 0 '')failure — the flag says so. The feature.
nullno signal
absent (undefined)no signal — falls through to the status verdict

A missing path cannot manufacture a failure, and a 200 carrying no flag is still a 200. That matters because a server sends what it sends: the same endpoint returns { meta: { success: true }, data } on Tuesday and a bare { data } on Wednesday — a different version, a cache tier, a partial rollout. This library exists to survive that, so it must not be the thing that breaks on it.

null sits with absence rather than with false on the same reasoning: APIs spell "not applicable" and "unknown" as null constantly, and JS truthiness would read that as a declaration of failure it never made.

A path that resolves to nothing still emits an info drift finding, so a typo — or an API that quietly dropped its envelope — shows up in .inspect() and the drift report without anyone's call failing.

If the envelope is genuinely guaranteed, declare the field in output and let validation enforce it. That is what the schema is for, it produces a real error with a real path, and it means flag needs no strict mode.

Interaction with retry

retry.on wins while attempts remain. A status listed in both retry.on and verdict.accept is retried until attempts are exhausted, then accepted (returned) on the final attempt — so you can retry a flaky 503 a few times and still treat a persistent one as a result rather than an error:

import {  } from 'stitchapi';

const  = ({
    : 'https://api.example.com',
    : '/maybe',
    : { : 3, : [503] }, // retry a 503 up to 3 times…
    : { : [503] }, // …then accept it on the final attempt
});

verdict is orthogonal to throttle.delegate, which surfaces a RateLimitError on rate-limit statuses earlier and is unaffected by this option.

When you still want the error body

If a non-2xx is a genuine failure but you need the API's error payload — its { error: "…" } body — don't reach for verdict. Let it throw and read StitchError.body (and .url), which carry the parsed payload and final request URL on the awaited / .safe() path.

Beyond configuration

verdict is the declarative half. When the rule is genuinely code — "a 200 whose status field is PENDING should be re-attempted" — the stage has a functional seam too: a surface's interpret hook, which can also ask for another attempt. Compose verdictOf in front of your own rules so a 500 stays a 500:

import { type ,  } from 'stitchapi';

const :  = {
    : 'poller',
    : (, ) =>
        (, ) ??
        ((. as { ?: string }). === 'PENDING'
            ? { : false, : true, : 'PENDING', : 1_000 }
            : { : true, : . }),
};

A body-driven retry shares the retry.attempts budget and reuses the run's Idempotency-Key, so replaying a write is as safe as a status-driven retry.

On this page