Skip to main content

Frontend device-fingerprinting integration

What this covers / who it's for. The SPA-side contract for device-risk screening (item 11 / D102): which vendor agent the frontend loads, how it collects a per-action device token, which request fields carry it on which endpoints, and the response/UX contract (fraud-block, screening-required, step-up, rate-limit). The tenant client SPA and the back-office SPA live in separate repos (D12), so this page — not a shared codebase — is the integration contract between them and the backend. For backend internals (the guard, stores, verdict mapping) defer to the fraud README; this page links down and does not restate it.

The device-risk feature is deploy-gated and default-disabled (D102): the backend always accepts the device fields below, but screening only runs once ops enables the fraud.device-risk feature for a tenant (and, for the central operator/sign-up surfaces, sets fraud.central_provider). You can and should ship the collection code now — before enablement it is a harmless no-op; after enablement it is what makes screening work. Until you send a token, an enabled tenant's screening has nothing to assess.

Why the frontend has to do anything

Device intelligence identifies the device behind a request (bots, emulators, tampering, VPN/proxy, account-takeover velocity). That signal can only be collected in the browser/app, by a vendor JavaScript agent, which returns an opaque handle. The SPA sends that handle to the backend, which pulls the authoritative risk verdict server-side (the client is never trusted to report risk). No agent on the frontend ⇒ no token ⇒ nothing to screen. The three screened moments are sign-up, login, and payout.

Which vendor agent to load — an OPEN integration decision

The fraud provider is routed server-side per tenant jurisdiction (the D102 grant: Fingerprint primary, SEON permitted across all six baseline jurisdictions). Today there is no endpoint that tells the SPA which fraud provider its tenant is routed to — provider routing is a backend concern the SPA cannot currently see. That leaves an open decision for the integrating team:

  • Option A — expose the routed fraud provider in the bootstrap payload. Extend the abilities feed (GET /api/v1/me/abilities, also embedded as meta.abilities on /me — see auth & RBAC and the authentication README) with the tenant's fraud provider + its public browser key + region, so the SPA loads the correct agent dynamically. This is the general solution and the natural home for the data, but it is not built and would need a backend change + a security review (it publishes the provider choice and a public key to the browser).
  • Option B — ship Fingerprint-only collection. Because Fingerprint is the sole primary in every jurisdiction today (SEON is permitted but primary nowhere), a frontend can integrate Fingerprint only and stay correct for the current grant. Simplest path to shipping; must be revisited if SEON is ever made primary for a jurisdiction.

This decision is tracked in 11-fraud-device-login.md §7 and points back at this page. Pick per your rollout; the collection flows for both vendors are below so either option is ready.

Collection flows per vendor

Verify at integration. The spike (spike/11-fraud-device-login.md §3) froze the process model ("client agent get() → tokens"; "JS SDK → encrypted session blob") but not the exact SDK package names / method signatures. Treat every code snippet below as illustrative and confirm the precise API against current vendor docs when you wire it up. Flags are marked ⚠ inline.

Fingerprint (Pro / Smart Signals) — the primary

  1. Load the Pro agent in the browser with two values:
    • the public browser API key — ⚠ this is Fingerprint's public key, meant to ship in client-side JS. It is distinct from the backend FINGERPRINT_SECRET_API_KEY (the Server-API bearer secret, which must never reach the browser — see config/fingerprint.php). Do not confuse the two.
    • the region endpoint, which must match the Fingerprint account's pinned data-residency region (region is fixed at account creation): global api.fpjs.io · EU eu.api.fpjs.io · APAC (Mumbai) ap.api.fpjs.io. The backend's region is set by FINGERPRINT_REGION; the agent's endpoint must agree.
  2. Call get() per screened action. It returns { requestId, visitorId }:
    • requestId — a per-event token (one identification). This goes in device_token.
    • visitorId — the stable device id. This may go in device_hint (see semantics below).
  3. Freshness (load-bearing): a requestId identifies one event. Collect a fresh one immediately before each screened submit — never cache or reuse a requestId across attempts. Reuse either fails to screen or (worse) re-screens a stale event.
// ⚠ ILLUSTRATIVE — confirm the @fingerprintjs/fingerprintjs-pro package + init signature at integration.
const fp = await FingerprintJS.load({ apiKey: PUBLIC_BROWSER_KEY, endpoint: REGION_ENDPOINT });
const { requestId, visitorId } = await fp.get(); // call once, per screened action
// → device_token: requestId ; device_hint (optional): visitorId

SEON (Fraud API v2) — permitted secondary

  1. Configure + start the SEON JS SDK in the browser (module toggles are applied server-side in config/seon.php; the browser SDK just collects).
  2. Collect the encrypted session blob — a getSession()-style call ⚠ (confirm the exact SEON SDK method name at integration) returns an encrypted base64 session string. That blob goes in device_token. The backend POSTs it to SEON, which decrypts it server-side and returns the device details + verdict in one synchronous call.
  3. device_hint is generally n/a for SEON. SEON's stable device id (true_device_id) is only known server-side after the vendor call — it is not exposed to the browser — so there is no client-side stable id to send as a hint under the SEON path in v1.
// ⚠ ILLUSTRATIVE — confirm the SEON JS SDK config + session-collection method at integration.
seon.config({ /* … */ });
const session = await seon.getSession(); // encrypted base64 blob
// → device_token: session

The endpoint × field matrix

Two fields, both optional (nullable), never required by validation. The backend always derives ip/user_agent itself from the request ($request->ip() / $request->userAgent()) — the SPA must never send IP or user-agent claims; only the opaque vendor handle is read from the body.

Endpointdevice_tokendevice_hintPurpose
POST /api/v1/registermax:8192max:256sign_up
POST /api/v1/login (client)max:8192max:256login
POST /api/v1/mfa/challenge (client)max:8192max:256login (completes an MFA login)
POST /api/v1/backoffice/login (operator)max:8192max:256login (central)
POST /api/v1/payouts (initiate)max:8192❌ — ignored by designpayout
POST /api/v1/payouts/{payout}/approve (vote)max:8192❌ — ignored by designpayout

Load-bearing: device_token/device_hint are TOP-LEVEL request params, not JSON:API attributes. Every endpoint above is a plain-params request body — device_token and device_hint sit flat alongside email/password/code, exactly like this:

{ "email": "…", "password": "…", "device_token": "<requestId>", "device_hint": "<visitorId>" }

Do not nest them under a JSON:API data.attributes envelope — that envelope shape applies to responses in this app (see API conventions), not to these request bodies. Mis-nesting is silent on login/sign-up: both fields are nullable and login is fail-open, so a mis-nested (effectively missing) token just means screening never runs — no error, nothing to notice. On payout it surfaces only once the feature is enabled, and only as 403 FRAUD_SCREENING_REQUIRED with no indication that nesting was the cause — budget debugging time for this if a payout integration reports unexplained screening-required errors.

Notes grounded in the controllers:

  • device_hint is auth-surfaces-only. It is accepted on the four auth surfaces above and not on either payout endpoint (the maker/checker path reads device_token only). The operator MFA challenge (POST /api/v1/backoffice/mfa/challenge) carries no device fields either — an operator login's screening happens on the password step.
  • MFA-enrolled client logins screen twice. The password step and the POST /api/v1/mfa/challenge verify step both carry the device fields and both screen; the login completes on the verify request, so send a fresh device_token on the challenge request too (do not reuse the login one). The backend's dedup window keeps this from double-charging the meter.
  • device_token size is max:8192 everywhere (SEON's encrypted session blob is large — hence the generous ceiling); device_hint is max:256.

What device_hint actually does

device_hint is an optimisation hook, not a security input, and it is inert by default. It is consumed only behind the default-OFF fraud.hint_reuse_enabled flag, and only on login/sign-up (never payout): when enabled, the backend may match the hint against a same-tenant cached Allow/Review verdict to skip a fresh vendor call. It is never persisted as a device id, a cached Deny is never reused, and it changes nothing while the flag ships off. Sending visitorId as the hint is safe and forward-useful; omitting it is equally fine. See the fraud README "Client device-hint reuse" section.

Response contract + required UX

All device-risk outcomes are 403s in the standard JSON:API error envelope (see API conventions); the discriminator is the error code. The authoritative code table is in the foundation README.

Status / codeMeaningRequired SPA behaviour
403 FRAUD_BLOCKEDThe device was denied — a real fraud verdict (terminal). Opens an operator-visible, appealable fraud case server-side.Do NOT auto-retry. Show neutral, non-accusatory messaging ("we couldn't complete this right now; contact support") — never reveal a fraud signal or a device score. The action is over for this attempt.
403 FRAUD_SCREENING_REQUIREDPayout only — screening could not complete (token missing, vendor outage, misconfig) and there was no fresh step-up. Recoverable — not a deny.Trigger the existing step-up MFA flow (POST /api/v1/mfa/step-up), then retry the same payout. A fresh step-up is the compensating control that lets it through.
403 STEP_UP_REQUIREDThe existing sudo-window gate (e.g. the payout vote requires a fresh second factor).Same as today: run POST /api/v1/mfa/step-up, then retry. This is the ordinary step-up flow, not device-specific.
429 + Retry-AfterA login rate-limit bucket tripped (see below).Back off for Retry-After seconds and show a cooldown; do not hammer-retry.

The POST /api/v1/mfa/step-up route shown above is the client route; operator/back-office flows use the /api/v1/backoffice/ prefix instead (e.g. POST /api/v1/backoffice/mfa/step-up).

A passing screen is invisible to the SPA: there is no screening field in a success response to read or surface — a screened-and-allowed login/sign-up/payout just returns the normal 200/201 response as if screening never happened.

The two login limiter buckets (429)

POST /login (client + operator) and POST /api/v1/mfa/challenge are throttled by throttle:login, which returns an array of two limits — whichever trips first 429s (authentication README):

  1. a tight (email|ip) bucket at 5/min;
  2. an IP-independent identity-alone companion at 20/min (closes distributed IP-rotation).

Both return 429 with Retry-After (plus standard X-RateLimit-* headers). This limiter is load-bearing precisely because an enabled device-risk feature makes each login a metered/paid vendor call — respect the cooldown.

Silent fail-open on login vs. the payout posture

  • Login / sign-up = fail-OPEN. If device_token is omitted (or the agent was blocked, or the vendor is down), the login/sign-up proceeds with reduced protection — no error, no friction. A missing token never bricks a login. (This is why you can ship the fields before enablement.)
  • Payout = never-silent, fail-CLOSED-to-step-up. On the money path a screening that can't complete is not silently ignored: it is durably recorded and the request is blocked with FRAUD_SCREENING_REQUIRED unless a fresh step-up is present. So on a payout, omitting the token routes into the screening-required posture once the feature is enabled — collect a real token for payout submits.

Operational notes

  • CSP connect-src. The vendor agent phones home from the browser, so add the agent host(s) to your Content-Security-Policy connect-src (and script-src if you load the SDK from the vendor CDN rather than bundling):
    • Fingerprint: the region endpoint you loaded — api.fpjs.io or eu.api.fpjs.io or ap.api.fpjs.io (match the account region).
    • SEON: the SEON JS SDK / collection host ⚠ (confirm the exact host at integration).
  • Ad-blockers are a real failure mode. Fingerprinting agents are commonly blocked by ad/tracker blockers → get()/getSession() fails → no token → the documented postures apply (login proceeds fail-open; a payout routes to screening-required). Handle a failed collection gracefully: submit without the token on login/sign-up; on a payout, surface the step-up path. Vendors offer a first-party proxy / custom-subdomain option to reduce blocking ⚠ (verify availability + setup against vendor docs); it is not wired server-side today.
  • Never persist or log device_token client-side. It is a single-use, per-event opaque handle — do not write it to localStorage/sessionStorage, application logs, or analytics. Collect a fresh one immediately before each submit (see the freshness note above) rather than storing one for reuse.
  • Privacy / consent. IP, user-agent, and device data are DLP-classified as Pii and encrypted at rest server-side (fraud README, DLP section). The frontend surface that collects device signals should align with the tenant's privacy policy / cookie-consent posture — flag this for legal review at enablement time, per jurisdiction.
  • Sandbox testing. Both vendors offer sandbox modes / test keys ⚠ (confirm current setup at integration). Because the feature is deploy-gated, end-to-end verification needs the fraud.device-risk feature enabled for a test tenant (and fraud.central_provider set for the operator/sign-up central surfaces). Note two known go-live gates from D102: the Fingerprint plan must include Smart Signals (an identification-only plan silently bands everything to Allow), and Fingerprint's per-second rate-limit ceiling is plan-scoped and unpublished (sandbox-confirm).

Where to read the backend side

  • fraud README — the guard, the two stores, verdict mapping, dedup, the webhook, retention.
  • authentication README — the device fields on the auth commands, the 429 login-limiter contract, the abilities feed.
  • foundation README — the ErrorCode table (FRAUD_BLOCKED / FRAUD_SCREENING_REQUIRED / STEP_UP_REQUIRED) and the bus gate order.
  • D102 — the go-live decision, deploy gates, and guard postures.
  • 11-fraud-device-login.md — the program tracking file, including §7 where the "expose routed provider to the SPA" decision is tracked.

← Engineering wiki index