docs / status & error codes

Status & error codes

Every verification path — hosted API, SDKs, CLI — returns results as a flat list of status codes. Success codes (sig_ok_bip322, bond_confirmed) sit alongside failure codes (sig_invalid, expired) in the same shape, so your gate can log one canonical stream instead of branching on ok: true | false.

This page is the single source of truth. If you see a code that isn't here, it's a bug — open an issue.

The response shape

All three surfaces return the same envelope:

{
  "ok": true,
  "codes": ["sig_ok_bip322", "bond_confirmed"],
  "address": "bc1q...",
  "metrics": { "sats_bonded": 125000, "days_unspent": 47, "score": 30.12 }
}

ok: true iff every signature code is a success AND thresholds are met. codes[] is the ordered list of everything the verifier observed — always present, even on success.

Signature codes

CodeSeverityMeaningWhat to tell the user
sig_ok_bip322✅ successBIP-322 signature valid against the declared address.
sig_ok_legacy✅ successLegacy signmessage signature valid (P2PKH addresses only).
sig_invalid❌ errorSignature does not match message + address."Check you copied the full canonical message. Check your wallet is on the right account — this is the #1 cause. For SegWit/Taproot addresses you need BIP-322; Electrum does not support BIP-322."
sig_unsupported_script❌ errorWallet produced a legacy signature for a SegWit/Taproot address."Use a BIP-322-capable wallet (Sparrow, Bitcoin Core, UniSat, Xverse) or switch to a P2PKH address (1…)."
invalid_scheme❌ errorscheme was neither bip322 nor legacy."Use a supported signing scheme."
decode_error❌ errorCanonical message could not be parsed — malformed header, wrong line endings, bad base64."Copy the full canonical message from Step 2 without modification. LF line endings only, one trailing newline."
invalid_attestation_id❌ errorattestation_id does not equal sha256(message)."This attestation has been tampered with. Refuse it."

Bond / chain-state codes

CodeSeverityMeaningWhat to tell the user
bond_confirmed✅ successConfirmed UTXOs found at the address; sats_bonded and days_unspent are derivable.
bond_zero⚠ warnAddress has no confirmed UTXOs. Signature is still valid — the bond just doesn't exist."Send sats to this address and wait for one confirmation before retrying."
bond_pendingℹ infoAddress has only unconfirmed transactions."Wait for the first confirmation (≈10 min)."
bond_insufficient❌ errorDeclared bond: extension exceeds the confirmed balance."Lower your declared bond, or fund the address up to the declared amount."

Policy / threshold codes (returned by /api/check)

These are emitted in addition to the signature/bond codes above, when an /api/check call has threshold parameters (min_sats, min_days).

CodeSeverityMeaning
below_min_sats❌ errorsats_bonded < min_sats.
below_min_days❌ errordays_unspent < min_days.

When /api/check returns ok: false, these appear under a separate reasons key so your error handler can switch on the cause:

{ "ok": false, "sats": 50000, "days": 12, "reasons": ["below_min_sats", "below_min_days"] }

Context codes

CodeSeverityMeaningWhat to tell the user
aud_mismatch⚠ warnaudience in the signed message doesn't match what the verifier expected.Usually means the signature was made for a different host — verify-intentional or refuse, your choice.
expired❌ errorexpires_at has passed (relevant for signed-challenge auth, not attestations — attestations have no expiry)."Your challenge has expired. Request a fresh one."
network_testmode⚠ warnSignature is on testnet or signet."Test networks can't be verified in production. Use mainnet."
bad_request❌ errorStructural problem with the request body — missing fields, wrong types."Check your request shape against the docs."

Programmatic access

The full table above is shipped as a typed record in the JS SDK:

import { STATUS_META, type StatusCode } from '@orangecheck/sdk';

const meta = STATUS_META['sig_invalid'];
// → { label: 'Signature verification failed', detail: '...', severity: 'error' }

Use STATUS_META[code].label for short headlines and .detail for the verbose user-facing message. The severity field ('success' | 'info' | 'warn' | 'error') lets your UI apply the right visual treatment without branching on string keys.

The Python SDK exposes an equivalent STATUS_META dict.

HTTP status vs. status code

The HTTP status says whether the request was processable; the codes[] field says what the verification actually found. Do not conflate them.

HTTPMeaning
200Request was valid and the verifier ran. ok and codes tell you what happened.
400Request shape was wrong — missing addr, malformed JSON, etc. codes may be absent.
401/api/auth/* rejected the session (e.g. not_authenticated).
403Cross-site POST to a browser-only endpoint.
404/api/check subject lookup returned no attestation.
429Rate limit hit. Retry after Retry-After.
500Verifier crashed. Open an issue.

Further