Anything a browser or an app returns can be edited by whoever controls it: the visitor ID, the score, the verdicts. Verification closes that gap. Your backend reads the stored event from Fingerly with a secret key, confirms it belongs to the action being taken, and only then decides.
The flow
Step 1: The client identifies
Call
identify({ tag })when the visitor acts. Send only therequestIdto your backend, with the action.Step 2: Your server reads the event
Fetch the event by request ID with your secret key. The answer comes from Fingerly, not from the browser.
curl "https://us.api.fingerly.io/api/v1/events/01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4" \ -H "x-api-key: $FINGERLY_SECRET_KEY"Step 3: Your server checks it
Run the four checks below.
Step 4: Your server decides
Allow, challenge, review or refuse, and record the request ID with the outcome.
Four checks
| Check | How | Stops |
|---|---|---|
| It exists | The read succeeds. A 404 means no such event in this key's environment. | Made-up or cross-environment request IDs. |
| It is this action | tag equals what you expect, such as checkout:8412. | An identification from a harmless page replayed at checkout. |
| It is recent | occurred_at is within the window your flow allows, such as two minutes. | Old request IDs saved and reused later. |
| It passes your policy | Read suspect_level, triggers and visitor_id. | The fraud you integrated Fingerly for. |
In code
The same four checks, with each server SDK.
import { load, FingerlyAPIError } from '@fingerly/node'
const fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })
const MAX_AGE_MS = 2 * 60 * 1000
export async function decide(orderId: string, requestId: string) {
let event
try {
event = await fingerly.events.get(requestId)
} catch (error) {
if (error instanceof FingerlyAPIError && error.status === 404) return 'refuse'
throw error
}
if (event.tag !== 'checkout:' + orderId) return 'refuse'
if (Date.now() - Date.parse(event.occurred_at) > MAX_AGE_MS) return 'refuse'
if (event.suspect_level === 'high') return 'review'
if (event.suspect_level === 'medium') return 'challenge'
return 'allow'
}
Timing
An event is usually readable within a few seconds of the identification. If your client sends the request ID in the same moment it receives it, retry a 404 a few times over a few seconds before refusing.
Keep secret keys secret
- Secret keys are refused when a request carries an
Originheader, so they cannot be used from front-end code. - A secret key only reads its own environment. Use a production secret key to verify production identifications.
- Store keys in your secret manager, and revoke and replace one immediately if it leaks.
Without a request ID
If identification failed in the client, your server receives no request ID. Treat that as missing evidence rather than as proof of fraud or of innocence: for example, allow low-risk actions and require a second factor for high-risk ones.