Guides

Server-side verification

Never trust a result the browser reports. Read the stored event by request ID with a secret key, check it belongs to this action, then decide.

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

  1. Step 1: The client identifies

    Call identify({ tag }) when the visitor acts. Send only the requestId to your backend, with the action.

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

    Request
    curl "https://us.api.fingerly.io/api/v1/events/01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4" \
      -H "x-api-key: $FINGERLY_SECRET_KEY"
    
  3. Step 3: Your server checks it

    Run the four checks below.

  4. Step 4: Your server decides

    Allow, challenge, review or refuse, and record the request ID with the outcome.

Four checks

CheckHowStops
It existsThe read succeeds. A 404 means no such event in this key's environment.Made-up or cross-environment request IDs.
It is this actiontag equals what you expect, such as checkout:8412.An identification from a harmless page replayed at checkout.
It is recentoccurred_at is within the window your flow allows, such as two minutes.Old request IDs saved and reused later.
It passes your policyRead 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 Origin header, 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.