docs / http api

OC Attest HTTP API

The hosted API at https://ochk.io/api/* is a reference deployment of OC Attest. Self-hosted verifiers provide the same endpoints. Every route is documented here.

Rate limits are per-IP; 60 req/min for read endpoints, 20 req/min for auth endpoints. Response shape is stable; new optional fields may be added, existing fields will not change meaning.

GET /api/check

The load-bearing sybil-gate primitive. One call, one answer.

Request

GET /api/check?addr=bc1q...&min_sats=100000&min_days=30
GET /api/check?id=<attestation_id>&min_sats=100000
GET /api/check?identity=github:alice&min_sats=50000
ParamRequiredNotes
addrat least one of…Bitcoin address
id…these threeAttestation ID (64-char hex)
identityprotocol:identifier, e.g. github:alice
min_satsoptionalInteger; default 0
min_daysoptionalInteger; default 0

Response — 200

{
    "ok": true,
    "sats": 125000,
    "days": 47,
    "score": 30.12,
    "attestation_id": "a3f5b8c2...",
    "address": "bc1q...",
    "identities": [{ "protocol": "github", "identifier": "alice" }],
    "network": "mainnet"
}

Response — below threshold (200, ok: false)

{
  "ok": false,
  "sats": 50000,
  "days": 12,
  "score": 15.15,
  "attestation_id": "a3f5b8c2...",
  "address": "bc1q...",
  "identities": [...],
  "network": "mainnet",
  "reasons": ["below_min_sats", "below_min_days"]
}

Response — not found (404)

{ "ok": false, "reasons": ["not_found"] }

Cached 60 s by default.

POST /api/verify

Raw attestation verification — no relay lookup, no thresholds. Give it (addr, msg, sig), get back full codes + metrics.

Request

{
    "addr": "bc1q...",
    "msg": "orangecheck\nidentities: ...\n...",
    "sig": "AUBH...",
    "scheme": "bip322"
}

Response — 200

Same shape as /api/check without the policy fields. No cache.

/api/challenge

Signed-challenge auth — prove control of an address in real-time.

GET /api/challenge?addr=bc1q…&audience=https://example.com&purpose=login

Issue a short-lived message for the user to sign.

Response:

{
    "message": "orangecheck-auth\n...",
    "nonce": "a3f5b8c2...",
    "expiresAt": "2026-04-24T07:00:00.000Z"
}

POST /api/challenge

Verify a signed response.

{
    "message": "orangecheck-auth\n...",
    "signature": "AUBH...",
    "expectedNonce": "a3f5b8c2...",
    "expectedAudience": "https://example.com",
    "expectedPurpose": "login"
}

Response: { ok: true, address: "bc1q..." } on success, { ok: false, reason: "invalid_challenge" } otherwise.

The public primitive is deliberately generic — no session cookies, no account side effects. The caller is responsible for nonce replay prevention. If you want the session-aware variant, see Sign in with Bitcoin.

GET /api/discover

List attestations for a subject — returns the full envelope array from Nostr.

GET /api/discover?addr=bc1q...
GET /api/discover?id=<attestation_id>
GET /api/discover?identity=github:alice

Response:

{
    "count": 2,
    "total": 2,
    "attestations": [
        /* AttestationEnvelope[] */
    ]
}

Use this when you need to see historical attestations for an address, not just "does the latest meet thresholds." Goes out to Nostr relays on every call — no cache.

Status codes

CodeSeverityMeaning
sig_ok_bip322successBIP-322 signature verified
sig_ok_legacysuccessLegacy signmessage verified (P2PKH only)
sig_invaliderrorSignature does not match
sig_unsupported_scripterrorLegacy sig for SegWit address
invalid_schemeerrorscheme field unknown
decode_errorerrorCanonical message malformed
invalid_attestation_iderrorClaimed ID doesn't match sha256(msg)
bond_confirmedsuccessChain lookup returned usable metrics
bond_zerowarnAddress holds no confirmed UTXOs
bond_pendinginfoOnly unconfirmed UTXOs found
bond_insufficienterrorbond: extension exceeds confirmed balance
below_min_satserrorPolicy threshold — /api/check only
below_min_dayserrorPolicy threshold — /api/check only
aud_mismatchwarnSigned audience ≠ expected (challenge flow)
expirederrorChallenge TTL exceeded
network_testmodewarnSignature is on testnet/signet
bad_requesterrorStructural problem with request body

HTTP status vs. status code

The HTTP status tells you the request was processable; the codes / reasons fields tell you what the verification found. Don't conflate them.

HTTPMeaning
200Request was valid and the verifier ran
400Request shape was wrong (missing addr, malformed JSON)
401/api/auth/* session rejected
403Cross-site POST to a browser-only endpoint
404/api/check subject not found
429Rate limit exceeded
500Verifier crashed — file a bug

See also