glassprint · API reference

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.

Self-host ships today: this is the surface of @noidme/glassprint-server, the image you deploy inside your own perimeter. The hosted managed plan is design-partner onboarding, not open self-signup. The deployed server resolves identity by exact match (the fuzzy ML linker exists but is not wired into the live ingest path); deviceId is a placeholder that is always null today, and smartSignals.incognito is always false (both roadmap).
Customer key id (kid)

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.

MethodRoute
POST/_fp/i/v1
Bound transparency token

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.

MethodRoute
POST/memin role: subject
OIDC Bearer JWT

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.

MethodRoute
POST/privacy/contestmin role: subject
POST/privacy/contest/resolvemin role: reviewer
POST/privacy/dsarmin role: subject
POST/privacy/erasemin role: subject
No key

Open / unauthenticated · 2

Liveness and public seal-key provisioning. The public key is safe to embed — only the server holds the private key.

MethodRoute
GET/_fp/pubkey
GET/healthz
POST /_fp/i/v1

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.

StatusreasonMeaning
200OKokIdentified. Sets a fresh httpOnly gp_session cookie; transparencyToken present on a resolved visitorId.
451Unavailable For Legal Reasonsconsent-missingUnavailable For Legal Reasons — the device-identification purpose is not in the GPC-honored granted set.
409ConflictreplayThe bundle nonce was already claimed (idempotency conflict).
400Bad RequeststaleclientTs is outside the allowed clock skew (future-dated is also rejected).
400Bad Requestdecrypt-failedUnseal / auth-tag failure, or a missing asymmetric opener.
400Bad Requestmalformed-sealed-bundlePre-core: the JSON envelope was not a valid SealedBundle.
401Unauthorizedunknown-keyUnknown customer kid on the symmetric path.
413Payload Too Largepayload-too-largePre-core: body exceeded the 32 KB cap.
429Too Many Requestsrate-limitedBucket 'ingest' (default 600/60s), keyed on the trust-resolved client IP. Includes Retry-After.
500Internal Server ErrorunknownTyped backstop — handleIngest never throws (or an unhandled internal error).
The 451 is not an error to route around — it is the product. Consent is asserted in the sealed bundle (asserted, not proven); the server honors GPC and fails closed when the lawful basis is absent.
POST/_fp/i/v1

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.

StatusDescription
200OKreason='ok' — identified. Sets Set-Cookie: gp_session=...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=2592000.
400Bad RequestEither 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'}).
401Unauthorizedreason='unknown-key' — unknown customer kid on the symmetric path. Body is an IngestResponse.
409Conflictreason='replay' — the bundle nonce was already claimed (idempotency conflict). Body is an IngestResponse.
413Payload Too LargePayload too large (> 32 KB). Pre-core.
429Too Many RequestsRate limited. Includes a Retry-After header.
451Unavailable For Legal Reasonsreason='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 Errorreason='unknown' (typed backstop; handleIngest never throws) OR an unhandled exception ({error:'internal'}).
POST/me

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.

StatusDescription
200OKview payload (action=view) or {forgotten:true} (action=forget).
401UnauthorizedToken invalid/expired/cross-session/cross-origin.
404Not FoundNo retained per-signal data (the server deliberately retains no raw per-signal values).
POST/privacy/contest

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.

StatusDescription
202AcceptedContest filed.
401UnauthorizedUnauthenticated.
403ForbiddenForbidden: 'subject-only' (non-subject) or 'not-your-decision'.
404Not FoundNo such decision.
429Too Many RequestsRate limited (bucket 'privacy').
POST/privacy/contest/resolve

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).

StatusDescription
200OKContest resolved (the updated record).
401UnauthorizedUnauthenticated.
403ForbiddenForbidden: 'reviewer-only' (subject), 'cross-tenant-denied', or 'reviewer-tenant-unbound'.
404Not FoundNo such contest.
429Too Many RequestsRate limited (bucket 'privacy').
POST/privacy/dsar

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').

StatusDescription
200OKThe subject's decision trail + override.
401UnauthorizedUnauthenticated, or subject not identified.
403ForbiddenForbidden (a reviewer/dpo export goes through a separate audited path).
404Not FoundNo data for the subject.
429Too Many RequestsRate limited (bucket 'privacy').
POST/privacy/erase

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.

StatusDescription
200OKErasure receipt (with scopes[] and the incomplete flag).
401UnauthorizedUnauthenticated, or subject not identified.
403ForbiddenForbidden: '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 RequestsRate limited (bucket 'privacy').
GET/_fp/pubkey

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.

StatusDescription
200OKThe tenant public key.
404Not FoundUnknown kid.
GET/healthz

Liveness probe. No auth.

StatusDescription
200OKAlive.
Wire schemas

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).

FieldTypeReqDescription
kidstringyesCustomer key id selecting the per-customer seal key. Must be non-empty and must not contain '|' (the tenant delimiter in durableId).
ivstringyesbase64 IV (12 bytes for GCM).
ctstringyesbase64 ciphertext (includes the GCM auth tag).
epkstringbase64 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`.

FieldTypeReqDescription
classVisitorClassyes
visitorIdstring | nullyesResolved tenant-distinct visitorId (HMAC, epoch-rotated), or null when not identified.
deviceIdstring | nullPlaceholder — always null today (roadmap).
declaredAgentIdstring | nullWeb Bot Auth declared-agent id when a verified agent is present.
confidencenumberyesIDENTITY-resolution confidence (exact-match: 0.99 repeat sighting, 0.6 first sighting) — NOT bot-ness.
suspectScorenumber | nullAdditive risk score in [0,1]. A privacy defense pulls it to <= 0.2.
smartSignalsSmartSignals
needsAttestationbooleanTrue 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.
consentStateobjectyesThe (GPC-honored) consent state the verdict was computed under.
reason"ok" | "consent-missing" | "replay" | "decrypt-failed" | "stale" | "unknown-key" | "unknown"yesTyped 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).
requestIdstringyesServer-generated id for this ingest (used to file an Art-22 contest).
transparencyTokenstringOpaque 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.

FieldTypeReqDescription
botbooleanyesAutomation/headless evidence present, reconciled with the final class after any human-review override (decoupled from the spoof verdict — FR-SPOOF-004).
incognitobooleanyesPrivate/incognito heuristics. Always false today (roadmap: 'lands with more signals').
tamperedbooleanyesGenuine cross-signal contradictions AND only when no legitimate privacy defense explains them — deliberately NOT flagged for Brave/Tor (a fairness choice).
inconsistencyintegeryesCross-signal contradiction count (0 = consistent).

ConsentReceipt

Client-asserted consent receipt carried inside the sealed bundle (asserted, not proven — GAP-1).

FieldTypeReqDescription
purposesarrayyesPurposes the subject granted.
gpcbooleanyesGlobal Privacy Control signal asserted by the client.
versionintegeryesConsent-policy version the client gated against.
issuedAtintegeryesClient epoch-ms the receipt was issued.

PubKeyResponse

A tenant's PUBLIC seal key, safe to embed; only the server holds the private key.

FieldTypeReqDescription
kidstringyes
alg"ECDH-P256+HKDF-SHA256+AES-256-GCM"yes
publicKeystringyesbase64 raw P-256 public key.

TransparencyView

The safe, user-facing view (never includes the raw visitorId).

FieldTypeReqDescription
handlestringyesA non-reversible display handle (e.g. 'you-ab12cd34ef').
signalsarrayyes
totalEntropyBitsnumberyes
purposesarrayyes
qualitativeOnlybooleanyesTrue when uniqueness is reported as bands (population below the k-anon floor).
The route set, methods, status codes and schemas here are authored from the production route table (src/server.ts) and the typed contracts (src/contracts.ts), so they track the deployed server. Generated server-side caller SDKs are roadmap — the OpenAPI spec this page renders is the input that unblocks them.

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.