Error codes
Stable identifier strings returned by the verifier and the settler.
Both the verifier (/verify) and the settler (/settle) return string error identifiers that merchants and SDKs can match on programmatically. The codes are stable; changing one is a breaking change.
(V) indicates the code may be returned by /verify. (S) indicates /settle. (V, S) indicates both.
Verification + settlement codes
invalid_envelope(V) —paymentPayloaddoes not match the V2 envelope schema.unsupported_scheme(V, S) — the facilitator does not handle this scheme.requirements_mismatch(V, S) — theacceptedblock does not match the merchant'spaymentRequirements.network_mismatch(V) —accepted.networkdiffers from the merchant's requirement.amount_mismatch(V) —accepted.amountdiffers from the merchant's requirement.asset_mismatch(V) —accepted.assetdiffers from the merchant's requirement.payTo_mismatch(V) —accepted.payTodiffers from the merchant's requirement.invalid_inner_payload(V) —payload.userOporpayload.settlementCallSpecis malformed.calldata_decode_failed(V) —userOp.callDatadoes not decode toexecute(...)or to anexecuteBatch(...)containing asettle(...)invocation.spec_mismatch(V) — the decodedsettlearguments differ frompayload.settlementCallSpec.replay(V, S) — therequestHashhas already been settled.
Settlement-only codes
in_flight(S) — another settlement for thisrequestHashis in progress. Caller should retry after the indicatedRetry-Afterinterval.bundler_error(S) — the bundler rejected the UserOp. Causes include rate-limit rejection, simulation revert, and RPC error.user_op_failed(S) — the UserOp executed on chain but reverted. Themessagefield carries the surfaced revert reason.receipt_timeout(S) — the bundler did not return a receipt within the configured deadline. The UserOp may still settle later; idempotency onrequestHashensures a retry will not double-spend.
Internal
internal_error(V, S) — facilitator-side fault. The client SHOULD retry once before treating this as a hard failure.
Reserved
Codes outside this list are reserved. New scheme implementations and new failure modes SHOULD allocate a new code rather than overload an existing one.
Where the codes appear
For /verify: {isValid: false, invalidReason: "...", details?: ...}. The details field, when present, carries machine-readable specifics (a Zod issue list for envelope errors, a sub-mismatch identifier for spec_mismatch).
For /settle: {success: false, x402Version: 2, errorCode: "...", message: "..."}. The message field is human-readable; do not match programmatically on it.
Programmatic matching example
const settle = await fetch(facilitatorUrl + "/settle", { ... }).then(r => r.json());
if (settle.success) {
// happy path
} else {
switch (settle.errorCode) {
case "in_flight":
return retryAfter(...);
case "user_op_failed":
return surfaceRevert(settle.message);
case "bundler_error":
case "receipt_timeout":
return retryWithBackoff(...);
case "replay":
// already settled — no further action; record the original txHash if possible
break;
default:
// unknown codes are reserved and may be added in future versions
log.warn("unknown settle errorCode", settle.errorCode, settle.message);
}
}