API

Everything on this site is also JSON. No key, no sign-up, no rate limit worth mentioning — it is cached at the edge.

Read API

All read responses carry Cache-Control: public, s-maxage=30, stale-while-revalidate=300.

EndpointReturns
GET /v1/leaderboardsevery board with its title, unit and population
GET /v1/leaderboards/{stat}?limit=50&offset=0one page of a board; limit is clamped to 200
GET /v1/players/{handle}a player's records and ranks, each with the board's direction and population; 404 for an unknown handle
GET /v1/players?q=whiskhandle search; prefix matches first, then substring. Two characters minimum
GET /v1/players/{handle}/events?limit=50&before=…the raw event log, newest first, cursor-paged. Page until next is absent, not until a page comes back short
GET /v1/events?type=…&handle=…the same raw log across every player, each row naming its handle; both filters optional
GET /v1/events/stream?type=…&handle=…live raw events as server-sent events, filters applied server-side (never cached, never replays — re-read /v1/events on reconnect)
GET /v1/compare?handles=a,b,cup to eight players pivoted board-first, read against one view of the projections
GET /v1/statsthe collection census — how many events, by type, by day/week/month/year, and what has been derived from them. About catlog, not about any player
GET /v1/feed?limit=30the activity feed as JSON rows, newest first
GET /v1/feed/streamthe same feed rows live, as server-sent events (never cached)
GET /v1/feed/ssethe live activity stream as datastar HTML fragments — this site's own transport; API clients want /v1/feed/stream
GET /.well-known/catlog-jwks.jsonthe public keys catlog signs licenses with
GET /.well-known/catlog-denylist.jsonthe signed revocation list
GET /healthz{"ok": true}
curl -s https://catlog.science.fail/v1/leaderboards/biggest_lithobrake_survived | jq '.rows[0]'

The raw views publish what catlog recorded, with three exceptions that never appear anywhere: the mod's install id is dropped, the client's own clock (wall_t) is dropped, and the career and kitten ids are relabelled per player — same shape, stable for that player, unrelatable between players. Flights flagged by the mod's stock-data checks and events of players without a public handle are excluded rather than shown. See privacy for why.

Ingest

POST /v1/ingest takes Brotli-compressed NDJSON — one event envelope per line — with Content-Type: application/x-ndjson and Content-Encoding: br.

Two headers authenticate it, both ES256 compact JWS:

  • X-Catlog-License — the long-lived license catlog signed when you claimed your handle. It names your handle and binds a public-key thumbprint in cnf.jkt.
  • X-Catlog-Proof — signed per batch by the private key in your credential file, over the batch id, the request method and URL, and a hash of the exact bytes being sent.

Because the proof is signed by the key the license names, a stolen license alone ships nothing. Batches chain: each proof carries a sequence number and the hash of the previous batch's body, so a replayed or forked stream is detectable and is rejected rather than silently merged.

Errors

Every error, everywhere, is the same shape:

{"error": "handle_taken", "detail": "that handle is already taken"}

Codes: bad_request, malformed_batch, unsupported_encoding, license_invalid, license_expired, license_revoked, proof_invalid, clock_skew, banned, stream_fork, rate_limited, too_large, not_found, handle_taken, handle_invalid, handle_reserved, quota_exceeded, account_too_new, internal.

A clock_skew response includes server_time in unix milliseconds, and every response carries a Date header — that is how the mod corrects its own clock rather than failing forever because a machine is five minutes fast.

Dashboard API

Session-authenticated and same-origin only. This is what the dashboard page itself calls:

GET /api/meyour provider, quotas and license lifetime
GET /api/handlesyour handles and their credential metadata
POST /api/handles{handle, jwk}{license}
POST /api/handles/{handle}/reissue{jwk}{license}
POST /api/handles/{handle}/revokerevokes every live credential for that handle
POST /api/me/deletedeletes everything (see privacy)
POST /api/logoutclears the session cookie

jwk is a public EC P-256 key as {kty, crv, x, y}. A JWK carrying a private d is rejected outright: catlog will not accept your private key even by accident.

Numbers are raw

Every value is published in the unit the event carried — metres, metres per second, milliseconds, joules — and never as a formatted string. A formatted string is not a number you can sort, and freezing the presentation into a response that is cached for thirty seconds would make a decimal place an API change. Both of catlog's own frontends format it themselves, from the same rules, and are checked against the same table of worked examples.