Errors and reason codes

Every reason code of the z402 protocol and every HTTP error of the facilitator, with what it means and what to do about it.

On this page

Errors reach you from several places. This page lists each one, says what it means, and says what to do.

A 402 with a reason codeAn HTTP errorA start-up checkA service statusA start-up lineWhat did you get?Reason codesHTTP errors of the facilitatorProvider check errorsErrors of the starter servicesStart-up and configurationerrors
A 402 with a reason codeAn HTTP errorA start-up checkA service statusA start-up lineWhat did you get?Reason codesHTTP errors of the facilitatorProvider check errorsErrors of the starter servicesStart-up and configurationerrors
WhereWhat you seeSection
POST /verify and POST /settleHTTP 200 with invalidReason or errorReasonReason codes
The agent, after a refused verifyHTTP 402 whose PAYMENT-REQUIRED header carries the reason code as errorReason codes
The other facilitator routesHTTP 400, 401, 403, 404, 409 or 500 with { "error": "<code>" }HTTP errors of the facilitator
A resource server at start-upA ProviderCheckError with a field and retryableProvider check errors
A starter serviceA status and { "error": "<code>" }Errors of the starter services
A process at startOne line on stderr that names the variableStart-up and configuration errors

Reason codes

Once the API key is accepted, the facilitator answers POST /verify and POST /settle with HTTP 200 whatever happens. A missing or unknown key is a 401. A refusal is a reason code in the body: invalidReason for a verify, errorReason for a settle. Both use the same closed list of 19 codes, 8 from the x402 standard and 11 of this scheme. The facilitator emits nothing else, and a client treats a code it does not know as fatal.

Only the code reaches the agent, so the code carries the action. A refused verify arrives as a 402 whose PAYMENT-REQUIRED header has the code as error, and, for this scheme, the session snapshot in accepts[0].extra.session. That is the corrective 402: it is the only way a refused client learns its limits. A refused settle arrives as the errorReason of the settlement result.

An invalidMessage or errorMessage accompanies each code. A message names a field and a rule. It never repeats text the caller sent, though it may state a number, such as a total or a limit.

Standard codes

CodeWhenWhat the client does
invalid_payloadThe body is not a JSON object, is not JSON, or is over 64 KB. The payload or the requirements fail the x402 v2 schema. accepted is not an object. The voucher is malformed: a hex field in uppercase or of the wrong length, a cumulativeZat that is not canonical, a bad depositTxid. The message names the first field and rule.Fatal. A bug or a version mismatch.
invalid_payment_requirementsThe requirements are not this rail's for this key: payTo is not the key's provider, amount is not a canonical zat from 1 to windowZat, the asset is not ZEC, or extra does not carry exactly the rail's terms. A scheme or network that this facilitator does not serve is unsupported_scheme.Fatal. The resource server drifted from the facilitator.
invalid_networkaccepted.network differs from paymentRequirements.network.Fatal.
invalid_schemeaccepted.scheme differs from paymentRequirements.scheme.Fatal.
unsupported_schemeThe requirements name a version, scheme and network that this facilitator does not serve.Fatal.
invalid_x402_versionpaymentPayload.x402Version is not 2.Fatal.
unexpected_verify_errorA verify failed inside the facilitator, or the facilitator is waiting for a restart after a failed ledger write.Back off 1 s, then try again once.
unexpected_settle_errorThe same for a settle, or the ledger refused the batch.Back off 1 s, then try again once.

Scheme codes

CodeWhenWhat the client does
z402_awaiting_confirmationThe new total is over the limit, and a live pending deposit exists. cumulativeZat <total> is over the limit <limit> until a deposit confirms.Never deposit. Wait until the pending deposit has one confirmation, then try again.
z402_deposit_requiredThe new total is over the limit, and nothing is pending. cumulativeZat <total> is over the limit <limit> with nothing pending.Make a deposit, then try again. A client that is not allowed another deposit stops.
z402_cumulative_mismatchThe total did not advance by at least this call's price. cumulativeZat must be at least <last total plus the price>.Resync the total from session.cumulativeZat, then try again.
z402_invalid_signatureThe Ed25519 signature does not verify over the scheme, network, payTo, session key and total. the voucher signature does not verify.Fatal. Report it.
z402_deposit_not_foundThe chain showed no such transaction for the whole wait, 30 s by default. the chain has no such tx.Keep the txid. Check the wallet's own status, back off, then try again.
z402_chain_unavailablelightwalletd gave no definitive answer, the lookup queue was full, or a deposit past its expiry has not been read again yet. the chain is unreadable, not looked up, or the deposit has not been read since its expiry.Keep the txid. Back off 5 s, then try again.
z402_deposit_memo_mismatchThe transaction was found, and this session cannot use it: no output decrypts to the provider's address, no output carries this session's memo, or the txid is credited to another session. One code and one message, the deposit cannot be credited, for all three, so an answer never says whether a transaction pays the provider.Drop the txid. Fatal: report it. Usually a wrong payTo, or a bug.
z402_deposit_transparentThe transaction has a transparent input or output. Deposits must be shielded.Drop the txid. Fatal: report it.
z402_deposit_deadThe deposit expired unmined, was dropped, or was orphaned. the deposit is dead.Drop the txid, make a new deposit, then try again.
z402_deposit_not_to_merchant, z402_deposit_other_sessionIn the list, but not emitted since protocol v1.2: those cases answer z402_deposit_memo_mismatch.Treated like z402_deposit_memo_mismatch.

A deposit is acknowledged only by a successful settle. A snapshot that comes with a refusal is projected and informational, and the client never reads a deposit listed in it as an acknowledgement.

Reading the codes as an operator

Most codes are the agent's business. A few point at your facilitator or at a resource server you run:

  • invalid_payment_requirements, for every call of one provider. That provider's resource server has stale terms, a price outside 1 to windowZat, or a payTo that is not the one its key is registered to. Check the key with GET /providers/me, and restart the server after any change to the terms.
  • unsupported_scheme. A resource server points at a facilitator on another network.
  • z402_chain_unavailable, in numbers. The facilitator cannot read the chain. Check the lightwalletd endpoint and Z402_LWD_URL.
  • z402_deposit_not_found. The deposit has not reached the endpoint the facilitator reads. On a load-balanced public endpoint, backends see a new transaction up to about 16 s apart.
  • unexpected_verify_error and unexpected_settle_error. The facilitator's log has the cause, on a line that starts with prepaid-voucher verify failed or prepaid-voucher settle failed (verify failed unexpectedly or settle failed unexpectedly for a failure outside the scheme). If the message says the facilitator must restart, a ledger write failed. The process logs the ledger cannot be written, exiting so that a restart replays it and exits 1.
  • z402_deposit_memo_mismatch for a client that did deposit. The client paid an address that is not the provider's, or its memo does not carry the session key it vouchers with.

HTTP errors of the facilitator

Outside /verify and /settle, an error is an HTTP status with a JSON body. The body is { "error": "<code>", "message": "<text>" }, and some have no message. Facilitator API has the routes.

StatuserrorWhereMeaning and what to do
400invalid_requestPOST /providers, POST /discovery/resources, GET /discovery/resources, GET /discovery/searchA field is missing or invalid, or the body is not JSON or is over 64 KB. The message names the field and the rule. A payTo of more than 1,024 characters is refused (payTo: expected at most 1024 characters). A refused declaration also carries the item's index. Fix the field and send it again.
401unauthorizedPOST /verify, POST /settle, GET /providers/me, POST /discovery/resourcesThe API key is missing, unknown, or was rotated out. The answer has WWW-Authenticate: Bearer. Use the current key. A resource server that gets this from /verify shows Facilitator ... failed (401).
401unauthorizedGET /supported, GET /discovery/resources, GET /discovery/searchThe request carried a key that names no provider. These routes need no key, so leave the header out or send a valid one.
401unauthorized, with messagePOST /providersThe facilitator has a registration token and the request did not carry it: registration needs the registration token as a bearer token. Send Authorization: Bearer <registration token>.
403not_ownerPOST /providersThe viewing key does not own payTo, or payTo has a transparent receiver. Take both from the same keys file, and use the Orchard-only address.
403provider_limitPOST /providersZ402_MAX_PROVIDERS is reached and the address is new: the limit of <n> providers is reached. The operator raises it.
403registration_disabledPOST /providersThe facilitator has no providers directory (this facilitator has no providers directory, which only an embedded or test facilitator has), or it runs on mainnet with no registration token and no Z402_REGISTRATION_OPEN=1 (registration is closed on this facilitator). The operator sets the token or the opt-in.
404not_foundAny other path or methodThe route does not exist. A wrong method on a known path is a 404 too.
409operatorPOST /providerspayTo is the operator's own address, which is never registered. A provider needs keys of its own.
500internal_errorAny routeSomething failed that the caller did not cause. The body has no detail. The cause is in the facilitator's log. A registration that gets a 500 changed nothing, and can be sent again.

Declaration refusals

A refused POST /discovery/resources is a 400 with the item's index. One bad item refuses the whole call and nothing is applied. The message is a path and a rule:

MessageWhat it asks for
body: must be a JSON objectA JSON object with an items array.
items: must be an array of 1 to 32 resourcesOne to 32 items per call.
items[i].resource: must be an absolute http(s) URL of at most 2048 characters, without userinfoA plain absolute URL.
items[i].type: must be "http" and items[i].x402Version: must be 2The fixed values.
items[i].accepts: must be an array of 1 to 4 entriesOne to four payment options.
items[i].accepts[j].scheme: must be prepaid-voucherThe scheme of this rail.
items[i].accepts[j].network: must be <network>The facilitator's network.
items[i].accepts[j].asset: must be ZECThe asset.
items[i].accepts[j].payTo: must be the payTo of this providerThe address the key is registered to.
items[i].accepts[j].amount: must be a canonical zat amount from 1 to <windowZat>A price the facilitator would accept.
items[i].accepts[j].maxTimeoutSeconds: must be a positive numberA positive number.
items[i].accepts[j].extra: must carry the terms of the railThe four terms of GET /supported.
items[i].accepts[j].extra.termsVersion: must be the termsVersion of the railThe current termsVersion. Restart the service, which reads it at start.
items[i].description: must be a string of at most 512 charactersAt most 512, and 128 for mimeType.
items[i].serviceName: must be 1 to 32 printable ASCII charactersThe same rule for each tag, items[i].tags[k].
items[i].tags: must be an array of at most 5 tags or must hold at most 5 distinct tagsAt most five tags that differ without regard to case.
items[i].iconUrl: must be an http(s) URL of at most 2048 characters, without userinfo, on a named hostNo IP literal and no loopback host.
items[i].extensions: must be a JSON object or must be at most 8192 bytes as JSONA plain object of at most 8 KB.
items[i].extensions.bazaar.info.input.method: must be one of GET, POST, PUT, PATCH, DELETE, HEADA known HTTP method.
items[i]: this provider already lists 128 resources or the catalog already lists 4096 resourcesThe caps. A new item is refused.
the catalog is fullThe items of the catalog already take 16 MiB. The whole call is refused.

Query refusals

MessageCause
limit: must be a non-negative decimal integerlimit is not a decimal number. A valid limit is clamped to 1 to 100.
offset: must be a non-negative decimal integeroffset is not a decimal number.
<name>: must be given once, as a stringA parameter was repeated.
<name>: is too largeA number of more than 15 digits.
query: is required or query: must be 1 to 256 charactersGET /discovery/search needs a query of that length.
cursor: must be a non-negative decimal integercursor is not a decimal number. Use the cursor the last answer returned.

Errors that other layers add

The x402 SDK and the resource server put a few strings of their own in a 402. None of them is a facilitator code, and a client treats each as fatal:

error of the 402Meaning
Payment requiredThe request had no PAYMENT-SIGNATURE, or it could not be decoded. This is the first 402 of every paid call.
No matching payment requirementsThe accepted entry of the payment was altered, and matches none of the resource's requirements.
Facilitator ... failed (<status>)The resource server could not use the facilitator. After a 401, its API key is wrong, missing or was rotated: set Z402_FACILITATOR_TOKEN.

A facilitator that does not answer in time reaches the agent as an HTTP 502 with neither PAYMENT-REQUIRED nor PAYMENT-RESPONSE. That is an ambiguous outcome, not a refusal: the client cannot tell whether the call was charged, so it counts the call as paid.

Provider check errors

A resource server calls checkProvider from @z402-net/server before it starts, to prove that its API key and its payTo belong together. A failure is a ProviderCheckError. Branch on field and retryable, never on the message. The message never echoes the key.

OutcomefieldretryableWhat to do
A connection error, a timeout, a body that breaks off, a 5xx, a 408 or a 429facilitatorUrltrueThe facilitator is starting or briefly down. Wait and ask again. The starter services retry for Z402_FACILITATOR_WAIT_MS, 120 s by default.
A 404, any other status, or a 200 with an unreadable bodyfacilitatorUrlfalseThe URL is not a facilitator of this rail. Check Z402_FACILITATOR_URL.
A 401 or a 403facilitatorTokenfalseThe key is missing, unknown or rotated out. Check Z402_FACILITATOR_TOKEN.
A 200, but for another payTo or networkpayTofalseThe key belongs to another address, or the facilitator serves another network. Check Z402_PAY_TO and Z402_NETWORK.
Before any request: a URL that is not http(s) or that carries a user name or passwordfacilitatorUrlfalseFix the URL. Use the token for credentials.
Before any request: a key that is not one b64tokenfacilitatorTokenfalseFix the key. It is letters, digits and -._~+/.
check-key.ts
import { checkProvider, ProviderCheckError } from "@z402-net/server";

try {
  await checkProvider({
    facilitatorUrl: "https://facilitator.example.com",
    token: process.env.Z402_FACILITATOR_TOKEN,
    payTo: process.env.Z402_PAY_TO ?? "",
    network: "zcash:regtest",
  });
} catch (e) {
  if (e instanceof ProviderCheckError && e.retryable) {
    console.error(`facilitator not ready (${e.field}), try again`);
  } else {
    throw e;
  }
}

Errors of the starter services

Each starter service refuses everything it can before the agent sends a voucher, so a refused call is never charged. A failure after the voucher is counted as paid by the client, which is why those are rare. Each service page has its own table.

StatuserrorServiceMeaning
404not_foundEvery serviceA spelling of a paid path that no route can match, such as an encoded slash (/v1/generate%2F). A trailing slash, a doubled slash (//v1/generate), another letter case or a percent-encoded letter is served as the paid route. The drop box also answers not_found for a path it has no route for.
405method_not_allowedEvery serviceA method the route does not serve. The Allow header says which one it does.
409payment_in_flightEvery serviceThe same voucher is already paying for a call.
503not_readyEvery serviceThe service is not ready: the model provider does not list the model, or the chain tip is not known. Readiness is cached for 5 s, and llm and embeddings also reuse their provider's answer for 10 s.
400invalid_requestllm, embeddingsThe body is not valid. The message says why.
400invalid_urlmarkdownNo url, not an absolute http(s) URL, over 4,096 characters, credentials in it, or a port that is not allowed.
400empty_body, bad_request, bad_bodydropboxAn upload with no body, an id that is not valid percent-encoding, or a body that could not be read.
403blocked_addressmarkdownThe host, or a redirect, leads to an address that is not public.
404unknown_streamllmThe stream id is not known.
401unauthorizeddropboxA delete without a bearer token. The answer has WWW-Authenticate: Bearer.
403forbiddendropboxA delete with the wrong delete token.
404unknown_dropdropboxA hold, a download or a delete for a drop that is unknown or expired.
409stream_busyllmAnother call on this stream is in flight.
409hold_cap_reacheddropboxA hold would take the drop past DROPBOX_MAX_HOLD_BLOCKS beyond the tip.
410stream_finishedllmEvery chunk was delivered.
413body_too_largellmThe request body is too large.
413too_many_tokensembeddingsThe inputs hold more characters than EMBEDDINGS_MAX_CHARS or EMBEDDINGS_MAX_INPUT_CHARS, or the body is too large.
413too_largemarkdown, dropboxA page body over MARKDOWN_MAX_BYTES, a page too heavy to convert, or an upload over DROPBOX_MAX_BYTES.
415unsupported_content_typemarkdownThe page is not text/html, application/xhtml+xml or text/plain.
415not_encrypted, unsupported_encodingdropboxThe upload is not shaped like an age file, or has a Content-Encoding.
429rate_limitedmarkdown, dropboxOver the per-client rate. Retry-After says when to try again.
502fetch_failed, convert_failedmarkdownThe page could not be fetched or converted.
502provider_failedllm, embeddingsThe model provider failed.
503busymarkdownThe fetch limit is reached, or the cache is full of pages held for paid retries.
503too_many_streams, stream_closedllmThe stream limit is reached, or the service is shutting down.
504fetch_timeoutmarkdownNo complete answer from the page within MARKDOWN_TIMEOUT_MS.
504chunk_timeoutllmThe chunk took longer than LLM_CHUNK_TIMEOUT_MS.
507storage_fulldropboxThe drops on disk would pass DROPBOX_MAX_TOTAL_BYTES.

The demo API answers an unpaid request of any method with a 402, and a paid request that is not a GET with a 405 and Allow: GET, after verification and before any settlement.

Start-up and configuration errors

A process that is misconfigured exits 1 with one line on stderr that names the variable at fault and never quotes a secret.

ComponentLine starts withWhere the causes are
The facilitatorfacilitator:Run the facilitator lists each message, cause and fix.
The demo APIapi:Configuration.
A starter servicellm:, markdown:, embeddings:, dropbox:Configuration.
scripts/register-provider.mjserror:, exit 1 refused or failed, exit 2 a usage errorRun the facilitator.
The markdown-register jobregister-provider:Deploy with Docker.

A start-up check of the facilitator reads startup check failed (<check>): <detail>, where the check is self-test, owns, ownedByKey, hasTransparent, network or consensusBranchId. A ledger problem starts with ledgerPath:, a registry problem with providersDir:, and a listen problem with port: or host:.

References

  • packages/protocol/src/reasons.ts: the closed list of 19 reason codes.
  • packages/facilitator/src/scheme.ts: when each code is emitted, and its message.
  • packages/facilitator/src/core/evaluate.ts: how a deposit is judged.
  • packages/facilitator/src/app.ts: the HTTP error bodies, the 401 challenge and the 404 and 500 fallbacks.
  • packages/facilitator/src/providers/register.ts: the registration refusals.
  • packages/facilitator/src/discovery/validate.ts: the declaration and query refusals.
  • packages/server/src/provider.ts: checkProvider and ProviderCheckError.
  • packages/facilitator/src/startup.ts: the start-up check messages.
  • packages/facilitator/src/config.ts: the configuration messages.
  • docker/register-provider.mjs: the messages of the registration job.
  • services/README.md: the starter services' refusals before payment.
  • docs/spec/protocol.md: section 8, the reason codes and the client actions.
  • docs/superpowers/specs/2026-10-28-z402-multi-provider-design.md: sections 4.1 to 4.4 and 4.6.