SDKs

Node.js

Read identification events with a secret key, verify signed webhooks, and serve the browser SDK from your own domain with the Fingerly Node.js SDK.

@fingerly/node does three things on your server: reads stored events by request ID, verifies webhook signatures, and forwards the browser SDK's requests through a route on your own domain. It has no runtime dependencies.

Requirements

  • Node.js 18.16 or newer, or any runtime with global fetch, Request and Response (Bun, Deno, Cloudflare Workers, Vercel Functions).
  • A secret key for reading events, a webhook signing secret, and a proxy key for the proxy.

Install

npm install @fingerly/node

Read an event

Create one client with your secret key and reuse it. The key decides the regional API and the environment the client reads.

Node.js
import { load } from '@fingerly/node'

const fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })
const event = await fingerly.events.get('01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4')

An event has the fields listed in Get an event. suspect_score is null when the request was not scored.

Verify a checkout

Read the event your client identified, check it belongs to this action and is recent, then decide on its level. See server-side verification.

Node.js
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'
}

Verify a webhook

Check the signature over the raw request body before parsing it. The helper rejects timestamps more than five minutes from now.

Node.js
import express from 'express'
import { verifyWebhook } from '@fingerly/node'

app.post('/webhooks/fingerly', express.raw({ type: 'application/json' }), async (req, res) => {
  const valid = verifyWebhook({
    secret: process.env.FINGERLY_WEBHOOK_SECRET!,
    payload: req.body,
    timestamp: req.get('x-fingerly-timestamp'),
    signature: req.get('x-fingerly-signature'),
  })
  if (!valid) return res.sendStatus(400)

  const event = JSON.parse(req.body.toString('utf8'))
  await queue.add(event.id, event)   // deduplicate on event.id
  res.sendStatus(204)
})

Serve the browser SDK from your domain

createProxy returns a (request: Request) => Promise<Response> handler that forwards the browser SDK's identify and deferred-report requests to Fingerly with the visitor's real address, origin and user agent. Mount it on any framework that speaks web Request and Response.

// app/metrics/[...path]/route.ts
import { createProxy } from '@fingerly/node'

export const POST = createProxy({
  proxyKey: process.env.FINGERLY_PROXY_KEY!,     // fly_px_us_production_…
  prefix: '/metrics',
  clientIp: (request) => request.headers.get('x-real-ip') ?? '',
})

API

load(options)

  • secretKeystringrequired
    A secret key. Its prefix decides the regional API.
  • endpointstring
    Override the API origin.
  • fetchImpltypeof fetchDefault globalThis.fetch
    A custom fetch.
MethodReturnsCalls
events.list(query?)Promise<EventPage>GET /events. query takes from, to (a Date or RFC 3339 string), page, limit, visitor and level, and an optional signal (an AbortSignal) to cancel the request.
events.get(requestId, signal?)Promise<Event>GET /events/{request_id}.

A non-2xx response throws FingerlyAPIError with the HTTP status. Network errors and aborts are thrown as they are.

verifyWebhook(options), returns boolean

  • secretstringrequired
    The endpoint's signing secret, whsec_….
  • payloadstring | Uint8Arrayrequired
    The raw request body, byte for byte.
  • timestampstring | nullrequired
    The x-fingerly-timestamp header.
  • signaturestring | nullrequired
    The x-fingerly-signature header.
  • toleranceSecondsnumberDefault 300
    How far the timestamp may be from now.

createProxy(options)

  • proxyKeystringrequired
    A proxy key, fly_px_…. Its prefix decides the regional API.
  • clientIp(request: Request) => string | Promise<string>required
    Resolves the visitor's address from infrastructure you trust.
  • prefixstringDefault '/api/fingerly'
    The path the proxy is mounted under.
  • maxBodyBytesnumberDefault 1048576
    Larger bodies are refused with 413.
  • timeoutMsnumberDefault 5000
    How long to wait for Fingerly.
  • upstreamstring
    Override the API origin.

The proxy answers 405 to anything but POST, 404 to any path except {prefix}/api/v1/identify and {prefix}/api/v1/events/{request_id}/supplement, and 401 when the request carries no public key.