checkout402_
Reference

API reference#

Base URL https://api.checkout402.com. The live OpenAPI schema is at /openapi.json, with a browsable version at /docs.

Authentication#

Authorization: Bearer c402_live_...

Sellers only — buyers never authenticate. A browser session cookie works anywhere a key does, which is how /create and /seller operate.

Status Meaning
401 Missing, malformed, or revoked key
403 Key lacks the required scope

Checkouts#

POST /v1/checkouts#

Create a checkout. Returns 201.

Field Type
title string required
price_usd string required — decimal string, e.g. "0.10"
pay_to string required — your payout wallet
description string
fulfillment inline | relay | webhook default inline
payload any the goods, for inline
origin_url string your endpoint, for relay
reusable bool true = payment link; default single-shot
test_mode bool settles without a chain
expires_in_minutes int default 7 days
input_schema, output_schema object offer card
example_input, example_output object offer card
freshness, coverage, source, constraints string offer card
curl -X POST https://api.checkout402.com/v1/checkouts \
  -H "Authorization: Bearer $C402_KEY" -H "content-type: application/json" \
  -d '{"title":"Financials","price_usd":"0.10","pay_to":"0x...","payload":{"revenue":1}}'

Idempotency-Key (optional header). Send one if you might retry. A repeat with the same key returns the original checkout and Idempotency-Replayed: true instead of creating a second one — so a timeout that you retry does not leave you selling the same thing twice.

Keys are scoped to your account, so pick whatever you like without worrying about collisions with other sellers. A create that fails releases the key, so you can fix the request and retry with the same one.

curl -X POST https://api.checkout402.com/v1/checkouts \
  -H "Authorization: Bearer $C402_KEY" -H "Idempotency-Key: order-8891" \
  -H "content-type: application/json" -d '{ … }'

GET /v1/checkouts#

Your checkouts, newest first. { "checkouts": [...] }.

PATCH /v1/checkouts/{id}#

Update title, description, and any offer-card field.

Price and payout are deliberately immutable

price_usd and pay_to cannot be patched — they change what a buyer may already have been quoted. Create a new checkout instead. An unknown field is a 422, never a silent no-op.


The checkout URL#

GET /c/{id}#

Content-negotiated. Accept: text/html gives the pay page; anything else gives 402 plus accepts[] and the offer card.

Send X-PAYMENT on this same URL to settle and receive the goods in one round trip.

POST /c/{id}/prepare#

Build the exact EIP-712 typed data for a buyer to sign.

{ "payer": "0xYourAddress", "network": "eip155:8453" }

Returns typed_data, a payload_stub, and the resolved network. The server signs nothing and holds no key — this exists so a browser doesn't reimplement consensus-critical crypto in JavaScript.

POST /c/{id}/pay#

Same as sending X-PAYMENT to GET /c/{id}, for clients that prefer an explicit POST.


Packages#

Set max_deliveries when creating a checkout and you have sold a package: one payment, one on-chain settlement, N calls. Minimum package price is $1.00.

Because settlement happens once, the per-call cost collapses — a $1 package of 1,000 calls pays the protocol's fixed component once instead of a thousand times. That is how sub-cent API pricing works here.

The purchase response carries the key, once:

{
  "fulfillment": { "payload":  },
  "package_key": "pk_x7Kd…",
  "deliveries_remaining": 999
}

Spend it with any parameters you like:

curl "https://api.checkout402.com/c/chk_abc123?q=globex" \
  -H "Authorization: Bearer pk_x7Kd…"
{ "fulfillment": { "payload":  }, "calls_remaining": 998 }

Notes that matter:

  • The key is shown once. Only its hash is stored, so it cannot be re-issued. Lose it and you buy the package again.
  • It is scoped to one checkout. A key for another offer gets 401, the same answer as an unknown key.
  • Parameters are free per call. This is why a package needs a key at all: re-presenting X-PAYMENT can only ever repeat one query, because the params are bound into the signature.
  • Exhaustion is 402, with calls_used and calls_included in the help block — buy again to continue.
  • A failed origin call is refunded to your allowance; you are not charged for a call that returned nothing.

Offers advertise their size, so a package and a single call are distinguishable before purchase:

{ "id": "chk_abc123", "price": {"amount": "10.00"}, "calls_included": 1000 }

Receipts#

GET /r/{payment_id}#

JSON or HTML. Contains payer, seller, network, the exact split, tx_hash, and delivery outcome.

subject is a stable pseudonym for the buyer, scoped to you. The same wallet buying from you again produces the same subject, so you can recognise a repeat customer without asking anyone to register — but the same wallet buying from a different seller produces a different one, so nobody can correlate your buyers against theirs. It is derived, not reversible to a wallet address.

Buyers are anonymous by default and nothing here changes that: subject is an identifier we compute, not an account anyone created.

{
  "id": "pay_dVo7SDQsatf4HIFC",
  "checkout_id": "chk_abc123",
  "network": "eip155:8453",
  "amount_usd": "0.100000",
  "provider_amount": 100000,
  "fee_amount": 5000,
  "total": 105000,
  "payer": "0x7099...",
  "subject": "sub_9f2c41ab7e05d3b8c6a1f402",
  "pay_to": "0xSeller...",
  "tx_hash": "d84fa280...",
  "test_mode": false,
  "settled_at": "2026-07-28T08:15:54Z"
}

Keys#

POST /v1/keys Mint one. {name, mode, scope}. Shown once. mode defaults to test
GET /v1/keys List (prefixes only, never the secret)
DELETE /v1/keys/{id} Revoke immediately

Scopes escalate read_only < payments < full.


Origin keys#

The seller's OWN upstream credentials, so a relay checkout can wrap an endpoint that answers 401. Distinct from /v1/keys, which authenticate you to us.

POST /v1/origin-keys {auth_type, value, name?, header_name?} → stored encrypted
GET /v1/origin-keys list, with checkouts_using per key
PATCH /v1/origin-keys/{id} {value} — rotates everywhere at once
DELETE /v1/origin-keys/{id} revoke (soft; warns if checkouts still use it)

auth_type is bearer | basic | api_key | custom; the last two need header_name.

Reference one from a checkout with origin_auth_secret_id, or pass origin_auth_value to store a one-off.

The value is never returned

Not on create, not on list, not on rotate. There is no read path — it is decrypted only inside the relay, on the way out to your origin. A credential belonging to another account returns 404.


Webhooks#

POST /v1/webhooks Register. Returns the signing secret once
GET /v1/webhooks List, with failure counts

See Webhooks for signature verification.


Auth and profile#

POST /auth/magic-link/request {email} → emails a sign-in link
GET /auth/verify?token= Consumes the link, sets the session cookie
POST /auth/wallet/challenge {address, network} → a message to sign
POST /auth/wallet/verify {message, signature} → account + first key
GET /auth/me Your full profile
PATCH /auth/me Update name, bio, slug, website_url, avatar_url, profile_type, social_links
POST /auth/logout Clears the session

Empty string clears a profile field; omitting it leaves it unchanged. A taken slug returns 409.


Facilitator#

x402 facilitator endpoints, one set per family:

/x402/base/{supported,verify,settle}
/x402/solana/{supported,verify,settle}
/x402/stellar/{supported,verify,settle}

Errors#

Errors are RFC 9457 problem documents, served as application/problem+json. Branch on type — it is a stable URI, and unlike a prose message it will not change wording under you.

{
  "type": "https://checkout402.com/problems/payment-required",
  "title": "Payment required",
  "status": 402,
  "detail": { "message": "invalid payment: signature does not match payer", "help": {  } },
  "help": {
    "fix": "re-read the 402 and sign the CURRENT requirement",
    "prepare": "POST /c/chk_.../prepare {\"payer\": \"0x...\"}",
    "retry_is_safe": "re-presenting the SAME X-PAYMENT redelivers; it never charges twice"
  }
}

The help block is the useful part: it names the fix, not just what went wrong. It is available both at the top level and inside detail, which is preserved unchanged for existing clients.

type Status Meaning
/problems/unauthenticated 401 No valid key or session
/problems/payment-required 402 The payment was presented and rejected
/problems/forbidden 403 Authenticated, but not yours
/problems/not-found 404 No such checkout, receipt or key
/problems/conflict 409 An Idempotency-Key request is still in flight
/problems/invalid-request 422 Validation failed; see errors[] for the fields
/problems/upstream-failed 502 The seller's origin failed after payment
/problems/unavailable 503 No settleable network right now

The 402 is not an error#

GET /c/{id} answering 402 is the protocol working, not a failure, so it is not a problem document. Its body is the x402 payment-required shape with accepts[], and the same requirements are in the PAYMENT-REQUIRED header. Pay and retry — see Buy as an agent.

Validation errors name the field#

{
  "type": "https://checkout402.com/problems/invalid-request",
  "status": 422,
  "detail": { "message": "request failed validation" },
  "errors": [{ "field": "price_usd", "reason": "Field required" }]
}