orangecheck · HTTP API
The hosted me.ochk.io substrate exposes the contracts the SDK calls into. The SDK is the canonical integration path; this page is the raw HTTP surface for clients that aren't TypeScript or that want to bypass the SDK.
Every endpoint is at https://me.ochk.io/api/... unless noted. Auth is by
cross-subdomain oc_session cookie (Domain=.ochk.io, HttpOnly, SameSite=None,
Secure) issued by the auth host at https://ochk.io, OR by an
Authorization: Bearer <jwt> header (cross-domain integrators that captured the
session token from the popup-signin postMessage). Mutating endpoints are
rate-limited per IP per bucket; limits are returned in X-RateLimit-* headers.
Sign-in endpoints (auth host · https://ochk.io)
The session cookie is issued by the family auth host, not by me.ochk.io
directly. The sign-in popup at me.ochk.io/popup/signin calls these endpoints
cross-origin with credentials — they are CORS-allowed for any *.ochk.io
browser origin. ochk.app, on its own domain, calls them without credentials and
keeps the token they return in its own cookie.
| Method | Path | Purpose |
|---|---|---|
POST | /api/auth/email-otp/start | issue a stateless OTP token + email a 6-digit code (rate-limited 5/hr) |
POST | /api/auth/email-otp/verify | verify {email, code, token}, upsert account, set oc_session cookie |
GET | /api/challenge | issue a fresh BIP-322 challenge to sign |
POST | /api/auth/signin | verify a signed BIP-322 challenge, upsert account, set oc_session |
POST | /api/auth/logout | clear the oc_session cookie |
GET | /api/auth/me | crypto-only identity lookup; 401 otherwise |
See
oc-me-web/src/pages/popup/signin.tsxfor the canonical client implementation against these endpoints.
Public endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /api/health | liveness probe |
GET | /api/health/deep | dependency probe |
GET | /api/envelope/[id] | canonical envelope JSON for a billable event |
GET | /api/payment/[id] | canonical envelope for a payment |
GET | /api/platform-fee-policy | published PLATFORM_FEE_POLICY |
GET | /api/abuse-limits | published per-class abuse rate limits |
GET | /api/integrator-config?project_key=… | sample IntegratorPriceConfig lookup |
GET | /api/stats/earned | aggregate sat-earnings (last 7d, by class) |
POST | /api/integrator-config | validate an IntegratorPriceConfig (echoes on success) |
POST | /api/contact | contact form submission (Resend-backed) |
Authenticated endpoints (oc_session required)
Integrator-facing endpoints (
/api/integrator/event,/api/integrator/event/batch,/api/payment/authorize) additionally accept anAuthorization: Bearer <token>header — a cross-domain integrator's backend is on a different eTLD+1 and never receives theDomain=.ochk.iooc_sessioncookie, so it forwards the popup-issued JWT instead. The remaining endpoints in this section require the cookie.
| Method | Path | Purpose |
|---|---|---|
GET | /api/auth/me | crypto-only identity lookup; 401 otherwise |
GET | /api/me/balance | sat balance + last-7d aggregate + by-class breakdown |
GET | /api/me/notifications | notifications feed |
POST | /api/me/notifications | mark read/unread ({ id, read } or { all: true, read: true }) |
GET | /api/me/export | GDPR-style account dump · downloadable JSON |
Authenticated · sessions (Per session)
| Method | Path | Purpose |
|---|---|---|
POST | /api/session/create | open a session — records no charge |
POST | /api/session/refresh | refresh inside the policy window — free |
POST | /api/session/invalidate | tear down — free, telemetry-only |
Authenticated · payment authorization (Per action)
| Method | Path | Purpose |
|---|---|---|
POST | /api/payment/authorize | authorize a payment, rate-limited 30/min · moves no sats yet |
Authenticated · integrator event ingestion
project_key (in the body) identifies the billing project. The earner is either
user_address — the user's did_oc, inside the body you sign, which is how a
backend fires — or, from a browser, the signed-in user's cookie or
Authorization: Bearer token (see the note above). Sign the request with the
project signing secret (X-OC-Signature) — in both test and live mode. A
user session proves a user, never the paying project, so an unsigned POST is
admitted only when the session belongs to the project's own owner (the CLI
test-fire path). Anything else gets 401 project_auth_required.
| Method | Path | Purpose |
|---|---|---|
POST | /api/integrator/event | fire a billable event under a project_key (live mode requires domain verification) |
POST | /api/integrator/event/batch | fire 1–100 billable events in one call · per-event idempotency keys |
Outside the project's pay periods both answer 200 with
earned: false (not_paying per event in a batch) and record nothing. Paying
users is optional.
Authenticated · integrator project lifecycle
| Method | Path | Purpose |
|---|---|---|
GET | /api/me/projects | list every project owned by your identity |
POST | /api/me/projects | create a project, rate-limited 5/min. Pays nobody unless pay: { action: 'add', … } asks it to |
GET | /api/me/projects/[id] | per-project state · owner-gated |
POST | /api/me/projects/[id] | update IntegratorPriceConfig (prices only; a name or domain in the body is ignored) · owner-gated, validates |
PATCH | /api/me/projects/[id] | rename: { display_name }, 1–80 characters · owner-gated |
POST | /api/me/projects/[id]/domain | change the domain: { domain }. Never verified: replaced at once. Verified: opens a move (see below) |
DELETE | /api/me/projects/[id]/domain | cancel an open move · owner-gated |
DELETE | /api/me/projects/[id] | delete project · owner-gated |
POST | /api/me/projects/[id]/pay-periods | when the project pays its users: add, end, set_end, cancel (details) · owner-gated |
POST | /api/me/projects/[id]/verify | run a domain-ownership check (DNS TXT, falls back to meta tag); during a move, checks the new domain · rate-limited 5/min |
DELETE | /api/me/projects/[id]/verify | reissue the verification token; a verified domain stays verified until its next check · owner-gated |
POST | /api/me/projects/[id]/test-fire | fire a real envelope under this project_key (same pipeline as production) · owner-gated · for the smoke-test sandbox |
GET | /api/me/projects/[id]/events | live event stream for this project · owner-gated |
GET | /api/me/projects/[id]/webhooks | list registered webhooks · owner-gated |
POST | /api/me/projects/[id]/webhooks | register a webhook URL · owner-gated |
POST | /api/developer/webhook-test | test-fire a synthetic envelope at a registered webhook |
Moving a verified domain
A verified domain is what consent screens name and what every receipt records,
so it never changes in one step. POST /api/me/projects/[id]/domain on a
verified project opens a move: the project gets a pending_domain with its own
token, and keeps running on its current domain. Publish that token at the new
domain, exactly as for a first verification, then call /verify. When the check
passes, the new domain becomes the project's domain and the old one is kept in
previous_domains, so receipts recorded under it still belong to the project. A
failed check changes nothing.
Opening a move needs a fresh identity check, as revealing the signing secret
does: without one the call answers 401 identity_reproof_required. A frozen
project answers 409 project_frozen.
Response shapes
All authenticated 401s are { ok: false } with
Cache-Control: no-store, private and Vary: Cookie. All 422 validation
failures return
{ error: string, details?: [{ subtype?: string, message: string }] }. All 429
rate-limit responses include a Retry-After header in seconds.
Rate limits per bucket
| Bucket | Limit | Window |
|---|---|---|
integrator.event | 120 | 60s |
payment.authorize | 30 | 60s |
wallet.send | 30 | 60s |
webhooks.register | 10 | 60s |
projects.test-fire | 20 | 60s |
developer.projects.create | 5 | 60s |
verify-domain:<addr> | 5 | 60s |
Limits are per IP, per bucket (except verify-domain which is per-account so
DNS retries don't get throttled by a noisy neighbor). Headers
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Window are emitted
on every rate-limited response. v1 buckets reset on cold start (per-instance
in-memory); production fronts every endpoint with an edge-level limiter.