The ingest server's HTTP surface
8 routes on the deployable glassprint ingest server, grouped by the credential each requires. Authored from the live route table — the browser agent seals a bundle behind a fail-closed consent gate; this server decrypts it, classifies the visitor, ledgers the verdict, and exposes the privacy-rights API.
Ingest · 1
The core identification surface. The caller is keyed by the kid carried INSIDE the sealed bundle — there is no auth header. The seal/HPKE keypair derived from that kid is the real credential.
| Method | Route |
|---|---|
| POST | /_fp/i/v1 |
Transparency (data subject) · 1
Subject-facing view / self-service forget. Authorized by the opaque transparency token (issued at ingest) bound to the gp_session cookie + a first-party origin + a 5-minute TTL — it never carries the raw visitorId.
| Method | Route |
|---|---|
| POST | /memin role: subject |
Privacy-rights API · 4
GDPR/DPDP Art 15/17/22 rights. Verified to a role (subject / reviewer / dpo) from an OIDC JWT (ES256/RS256 only). Fail-closed: production defaults to deny-all until a real authenticator is configured.
| Method | Route |
|---|---|
| POST | /privacy/contestmin role: subject |
| POST | /privacy/contest/resolvemin role: reviewer |
| POST | /privacy/dsarmin role: subject |
| POST | /privacy/erasemin role: subject |
Open / unauthenticated · 2
Liveness and public seal-key provisioning. The public key is safe to embed — only the server holds the private key.
| Method | Route |
|---|---|
| GET | /_fp/pubkey |
| GET | /healthz |
Ingest status map
Every ingest outcome is a typed reason on the body that maps 1:1 to the HTTP status. The gate that makes glassprint lawful by construction is the 451: with no granted device-identification purpose there is no lawful basis to identify, so the server refuses — it physically cannot run without consent.
| Status | reason | Meaning |
|---|---|---|
| 200OK | ok | Identified. Sets a fresh httpOnly gp_session cookie; transparencyToken present on a resolved visitorId. |
| 451Unavailable For Legal Reasons | consent-missing | Unavailable For Legal Reasons — the device-identification purpose is not in the GPC-honored granted set. |
| 409Conflict | replay | The bundle nonce was already claimed (idempotency conflict). |
| 400Bad Request | stale | clientTs is outside the allowed clock skew (future-dated is also rejected). |
| 400Bad Request | decrypt-failed | Unseal / auth-tag failure, or a missing asymmetric opener. |
| 400Bad Request | malformed-sealed-bundle | Pre-core: the JSON envelope was not a valid SealedBundle. |
| 401Unauthorized | unknown-key | Unknown customer kid on the symmetric path. |
| 413Payload Too Large | payload-too-large | Pre-core: body exceeded the 32 KB cap. |
| 429Too Many Requests | rate-limited | Bucket 'ingest' (default 600/60s), keyed on the trust-resolved client IP. Includes Retry-After. |
| 500Internal Server Error | unknown | Typed backstop — handleIngest never throws (or an unhandled internal error). |
The core identification surface. Accepts a JSON SealedBundle (32 KB cap), decrypts it (symmetric or HPKE), captures the network tier behind the trust boundary, optionally verifies Web Bot Auth, resolves identity (exact-match in the deployed path), classifies the visitor, ledgers the verdict, and returns the typed IngestResponse. Always mints a fresh httpOnly gp_session cookie (session-fixation fix) and OVERWRITES any inbound value; the transparency token binds to it. Rate-limited under bucket 'ingest' (default 600/60s) keyed on the trust-resolved client IP. The caller is keyed by the kid INSIDE the body — see the ingestKid scheme.
| Status | Description |
|---|---|
| 200OK | reason='ok' — identified. Sets Set-Cookie: gp_session=...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=2592000. |
| 400Bad Request | Either a typed core failure (reason='stale' — clientTs outside maxClockSkew, or 'decrypt-failed' — unseal/auth-tag failure or missing asymmetric opener; body is an IngestResponse) OR a pre-core malformed envelope (body {error:'malformed-sealed-bundle'}). |
| 401Unauthorized | reason='unknown-key' — unknown customer kid on the symmetric path. Body is an IngestResponse. |
| 409Conflict | reason='replay' — the bundle nonce was already claimed (idempotency conflict). Body is an IngestResponse. |
| 413Payload Too Large | Payload too large (> 32 KB). Pre-core. |
| 429Too Many Requests | Rate limited. Includes a Retry-After header. |
| 451Unavailable For Legal Reasons | reason='consent-missing' — Unavailable For Legal Reasons. No lawful basis to identify (the 'device-identification' purpose is not in the GPC-honored granted set). Body is an IngestResponse. |
| 500Internal Server Error | reason='unknown' (typed backstop; handleIngest never throws) OR an unhandled exception ({error:'internal'}). |
Transparency view / self-service forget ('a fingerprint you can see through'). Authorized by the bound transparency token (in the body `token` field) validated against the request's httpOnly gp_session cookie + a validated first-party origin + a 5-min TTL. action='forget' IS a verified self-service Art-17 erasure on the durable id behind the token. Rate-limited under bucket 'privacy' (default 30/60s). The server retains no raw per-signal values, so action='view' returns 404 today.
| Status | Description |
|---|---|
| 200OK | view payload (action=view) or {forgotten:true} (action=forget). |
| 401Unauthorized | Token invalid/expired/cross-session/cross-origin. |
| 404Not Found | No retained per-signal data (the server deliberately retains no raw per-signal values). |
Art 22 — a subject files a contest against one of THEIR automated decisions. Subject-only; ownership is checked on the epoch-stable durableId behind requestId.
| Status | Description |
|---|---|
| 202Accepted | Contest filed. |
| 401Unauthorized | Unauthenticated. |
| 403Forbidden | Forbidden: 'subject-only' (non-subject) or 'not-your-decision'. |
| 404Not Found | No such decision. |
| 429Too Many Requests | Rate limited (bucket 'privacy'). |
Art 22 — a human reviewer resolves a contest. reviewer/dpo only; tenant-guarded. An 'overturned' outcome writes a durable-keyed neverAutomation override (a confirmed human is never re-labeled a bot).
| Status | Description |
|---|---|
| 200OK | Contest resolved (the updated record). |
| 401Unauthorized | Unauthenticated. |
| 403Forbidden | Forbidden: 'reviewer-only' (subject), 'cross-tenant-denied', or 'reviewer-tenant-unbound'. |
| 404Not Found | No such contest. |
| 429Too Many Requests | Rate limited (bucket 'privacy'). |
Art 15 / 22(3) export. Subject-only; exports the decision trail + override for the subject's durable id. Requires an authenticated principal (OIDC Bearer JWT mapped to role 'subject').
| Status | Description |
|---|---|
| 200OK | The subject's decision trail + override. |
| 401Unauthorized | Unauthenticated, or subject not identified. |
| 403Forbidden | Forbidden (a reviewer/dpo export goes through a separate audited path). |
| 404Not Found | No data for the subject. |
| 429Too Many Requests | Rate limited (bucket 'privacy'). |
Art 17 erasure. A subject may erase only their own verified visitorId; a dpo may erase on behalf but only within its own tenant (the kid prefix of the durableId), fail-closed.
| Status | Description |
|---|---|
| 200OK | Erasure receipt (with scopes[] and the incomplete flag). |
| 401Unauthorized | Unauthenticated, or subject not identified. |
| 403Forbidden | Forbidden: 'not-your-data' (subject erasing another id), 'cross-tenant-denied' (dpo across tenants), or 'reviewer-tenant-unbound' (dpo with no bound tenant). Fail-closed. |
| 429Too Many Requests | Rate limited (bucket 'privacy'). |
Publish a tenant's PUBLIC seal key so the SDK can seal asymmetrically (HPKE). The public key is safe to embed; only the server holds the private key. No auth.
| Status | Description |
|---|---|
| 200OK | The tenant public key. |
| 404Not Found | Unknown kid. |
Liveness probe. No auth.
| Status | Description |
|---|---|
| 200OK | Alive. |
The core shapes
The envelope that travels over the wire, the typed verdict it returns, and the privacy shapes — straight from the OpenAPI components. The verdict is the deliverable: a typed class plus additive risk DATA (suspectScore, smartSignals), never a single number to act on blindly.
SealedBundle
The sealed envelope that travels over the wire (FR-INGEST-006: AES-256-GCM). Body cap is 32 KB. When `epk` is present the bundle was sealed asymmetrically (HPKE-style) to the tenant PUBLIC key and the client cannot decrypt it; when absent it is legacy symmetric sealing (client holds the shared key).
| Field | Type | Req | Description |
|---|---|---|---|
| kid | string | yes | Customer key id selecting the per-customer seal key. Must be non-empty and must not contain '|' (the tenant delimiter in durableId). |
| iv | string | yes | base64 IV (12 bytes for GCM). |
| ct | string | yes | base64 ciphertext (includes the GCM auth tag). |
| epk | string | — | base64 ephemeral ECDH public key (P-256, raw uncompressed). PRESENT => asymmetric/HPKE sealing; ABSENT => legacy symmetric. |
IngestResponse
The authoritative typed result of an ingest. Identity-resolution confidence (`confidence`) is separate from bot-ness, which is conveyed via `class` + `suspectScore` + `smartSignals`.
| Field | Type | Req | Description |
|---|---|---|---|
| class | VisitorClass | yes | |
| visitorId | string | null | yes | Resolved tenant-distinct visitorId (HMAC, epoch-rotated), or null when not identified. |
| deviceId | string | null | — | Placeholder — always null today (roadmap). |
| declaredAgentId | string | null | — | Web Bot Auth declared-agent id when a verified agent is present. |
| confidence | number | yes | IDENTITY-resolution confidence (exact-match: 0.99 repeat sighting, 0.6 first sighting) — NOT bot-ness. |
| suspectScore | number | null | — | Additive risk score in [0,1]. A privacy defense pulls it to <= 0.2. |
| smartSignals | SmartSignals | — | |
| needsAttestation | boolean | — | True when the fingerprint was too common to identify (routed to the attestation tier); the visitorId is best-effort and may collide — require App Attest / passkey / Web Bot Auth for a confident identity. |
| consentState | object | yes | The (GPC-honored) consent state the verdict was computed under. |
| reason | "ok" | "consent-missing" | "replay" | "decrypt-failed" | "stale" | "unknown-key" | "unknown" | yes | Typed outcome; maps 1:1 to the HTTP status (ok=200, consent-missing=451, replay=409, stale=400, decrypt-failed=400, unknown-key=401, unknown=500). |
| requestId | string | yes | Server-generated id for this ingest (used to file an Art-22 contest). |
| transparencyToken | string | — | Opaque AES-GCM token bound to this request's gp_session + origin + a 5-min TTL. Present only on reason='ok' with a resolved visitorId and a valid first-party origin. Pass it to POST /me. |
VisitorClass
Final classification of the visitor. 'declared_agent' is set iff Web Bot Auth (RFC 9421 Ed25519) verified — authenticated, NOT trusted (the agent is still scored). 'human'/'undeclared_automation'/'unknown' come from the automation classifier; a lone weak tell resolves to 'unknown' (refuse to label) to protect privacy users.
VisitorClass = "human" | "declared_agent" | "undeclared_automation" | "unknown"
SmartSignals
Additive risk/quality signals (à la FingerprintJS Smart Signals) — DATA, never a verdict the customer must act on blindly.
| Field | Type | Req | Description |
|---|---|---|---|
| bot | boolean | yes | Automation/headless evidence present, reconciled with the final class after any human-review override (decoupled from the spoof verdict — FR-SPOOF-004). |
| incognito | boolean | yes | Private/incognito heuristics. Always false today (roadmap: 'lands with more signals'). |
| tampered | boolean | yes | Genuine cross-signal contradictions AND only when no legitimate privacy defense explains them — deliberately NOT flagged for Brave/Tor (a fairness choice). |
| inconsistency | integer | yes | Cross-signal contradiction count (0 = consistent). |
ConsentReceipt
Client-asserted consent receipt carried inside the sealed bundle (asserted, not proven — GAP-1).
| Field | Type | Req | Description |
|---|---|---|---|
| purposes | array | yes | Purposes the subject granted. |
| gpc | boolean | yes | Global Privacy Control signal asserted by the client. |
| version | integer | yes | Consent-policy version the client gated against. |
| issuedAt | integer | yes | Client epoch-ms the receipt was issued. |
PubKeyResponse
A tenant's PUBLIC seal key, safe to embed; only the server holds the private key.
| Field | Type | Req | Description |
|---|---|---|---|
| kid | string | yes | |
| alg | "ECDH-P256+HKDF-SHA256+AES-256-GCM" | yes | |
| publicKey | string | yes | base64 raw P-256 public key. |
TransparencyView
The safe, user-facing view (never includes the raw visitorId).
| Field | Type | Req | Description |
|---|---|---|---|
| handle | string | yes | A non-reversible display handle (e.g. 'you-ab12cd34ef'). |
| signals | array | yes | |
| totalEntropyBits | number | yes | |
| purposes | array | yes | |
| qualitativeOnly | boolean | yes | True when uniqueness is reported as bands (population below the k-anon floor). |
Run it inside your own perimeter
Deploy the ingest server self-hosted so your fraud signals never leave your network — or talk to us about managed onboarding.