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.
| Endpoint | Returns |
|---|---|
GET /v1/leaderboards | every board with its title, unit and population |
GET /v1/leaderboards/{stat}?limit=50&offset=0 | one 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=whisk | handle 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,c | up to eight players pivoted board-first, read against one view of the projections |
GET /v1/stats | the 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=30 | the activity feed as JSON rows, newest first |
GET /v1/feed/stream | the same feed rows live, as server-sent events (never cached) |
GET /v1/feed/sse | the live activity stream as datastar HTML fragments — this site's own transport; API clients want /v1/feed/stream |
GET /.well-known/catlog-jwks.json | the public keys catlog signs licenses with |
GET /.well-known/catlog-denylist.json | the 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 incnf.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/me | your provider, quotas and license lifetime |
GET /api/handles | your handles and their credential metadata |
POST /api/handles | {handle, jwk} › {license} |
POST /api/handles/{handle}/reissue | {jwk} › {license} |
POST /api/handles/{handle}/revoke | revokes every live credential for that handle |
POST /api/me/delete | deletes everything (see privacy) |
POST /api/logout | clears 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.