Read API

Everything public on this site is also readable as JSON, from the same data the pages render. No key, no registration — the endpoints below are open and rate-limited at the edge. Within /api/v1, evolution is additive: clients must ignore unknown fields and tolerate omitted optional fields. Authenticated write endpoints (ingest, catalog session registration) exist but are private to the observatory's own capture infrastructure and are not documented here.

Endpoints

endpointreturnsparameters
GET /api/v1/top/{signal} Ranked fingerprints for one family — the JSON twin of browse. Rows carry the dimension id, canonical value, deduplicated count, and first/last seen. window = 24h|7d|30d|all (default 30d) · new=1 only fingerprints first seen inside the window · limit 1–500 (default 100)
GET /api/v1/fp/{signal}/{id} One fingerprint: canonical value, decomposed attributes, first/last seen, top co-observed fingerprints per family, and distinct clients per day for the last 30 days. Every fingerprint page links its own JSON. —
GET /api/v1/catalog The full controlled-client catalog: every published session with client/version/OS/engine provenance and its captures' fingerprints and wire tuples. Self-generated, exportable research data. —
GET /api/v1/catalog.csv The catalog flattened to one row per capture, for spreadsheets and quick joins. —
GET /api/v1/probe The caller's own fingerprints, reflected as JSON — what the home page shows, machine-readable. This is the browser beacon's endpoint; each request is recorded as one deduplicated observation. —

A formal dataset licence has not yet been published. Export availability should not be interpreted as a broader reuse grant. Consumers should retain the source URL and retrieval date and treat additive fields as expected.

{signal} is one of: tls, http, h2, tcp, ja4t, qtp, ua, country, asn, tls-groups — the families defined on the about page. GET /api/v1 returns this endpoint list as JSON.

Catalog schema 1

/api/v1/catalog carries top-level schema_version: 1 and generated_at (a UTC timestamp for this representation), alongside the existing site and sessions fields. The CSV appends schema_version and generated_at columns, repeated per capture row, after its existing columns. These exports contain published reference sessions. Fields may be added within a version; removing or renaming a field, changing its meaning, or changing which sessions are included requires a schema-version change documented here.

Captures may include these additional fields inside wire:

fieldtypemeaning
tls_groupsstringThe advertised supported groups as four lowercase hex IANA ids, comma-separated in wire order. Each GREASE value becomes an in-place 0a0a placeholder; order and duplicates are preserved. Present only with status ok.
tls_groups_rawstringThe edge's OpenSSL-rendered list before mapping, bounded to 2048 bytes. Oversized input is discarded whole; no truncated prefix is retained or keyed. This rendered list is catalog evidence, not raw ClientHello bytes.
tls_groups_statusstringok: non-empty mapped list; absent: the edge saw the ClientHello and the list was empty, with no cause inferred; unmapped: an unknown rendered name; invalid: malformed input; oversize: input exceeded the bound. Only ok identifies a groups dimension.
tls_groupstringThe negotiated group as one four-digit hex IANA id, when known. This is connection context, influenced by server preference.
tls_session_reusedbooleanWhether the server accepted session reuse. false is known non-reuse; an omitted field means unknown, including historical captures. An offered session alone does not establish reuse.

Each capture also carries session_state, derived at export time from its TLS version, ja4_r and accepted-reuse evidence. Historical wire tuples are unchanged. The CSV appends tls_groups, tls_groups_status, tls_session_reused and session_state after its existing metadata columns. Unknown groups and reuse are blank CSV cells; a known non-reused session is false. The derived state is always present:

stateevidence
coldNo reuse accepted and, on TLS 1.3, no 0029 PSK offer in ja4_r. TLS 1.2 offers are not visible in JA4, so TLS 1.2 non-reuse is cold.
offeredTLS 1.3 PSK offered; the server did not accept it.
resumedThe server accepted reuse.
inconsistentAccepted reuse reported on TLS 1.3 without a PSK offer; kept as recorded.
unknownReuse evidence, TLS version or a usable ja4_r is missing. Older captures are unknown, never assumed cold.

Both formats use a strong ETag: the SHA-256 of the exact response body. Send it in If-None-Match to receive 304 when that representation is unchanged. generated_at stays fixed while the representation is cached; it is not a capture timestamp. Responses retain Cache-Control: public, max-age=300 and omit Last-Modified.

Examples

Fair use

Read endpoints are rate-limited at the edge and the catalog exports are additionally served from a 300-second cache. Cache on your side too: the exports change only when capture waves run, and the rolled-up counts advance on the order of minutes, so sustained polling buys nothing. A client that stays under roughly one request per second will never notice the limits. Counting methodology, privacy posture, and what the numbers mean are on the about page.