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 names the environment the client reads.

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

const fingerly = new Fingerly({ 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 { Fingerly, FingerlyAPIError } from '@fingerly/node'

const fingerly = new Fingerly({ 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 { Fingerly } from '@fingerly/node'

app.post('/webhooks/fingerly', express.raw({ type: 'application/json' }), async (req, res) => {
  const valid = Fingerly.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

Fingerly.createProxy returns a (request: Request) => Promise<Response> handler that forwards the browser SDK's requests to Fingerly with the visitor's real address, origin and user agent. Create a proxy in Integration > Proxy keys, then mount the handler at the path it issued, such as /k7x2qm4a, on any framework that speaks web Request and Response. See proxy integrations.

// app/k7x2qm4a/route.ts
import { Fingerly } from '@fingerly/node'

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

The browser SDK posts both of its calls to that path exactly, with nothing appended. Where the handler is not already routed to that one path, pass path: '/k7x2qm4a' and it answers only that path, exactly. Keep the path the dashboard issued: a path of your own is refused if it contains a word blockers match, such as fingerly, tracking or metrics.

API

new Fingerly(options)

  • secretKeystringrequired
    A secret key. Its prefix names the environment read.
  • apiVersionstringDefault 'v1'
    API route version; the server must support the selected version.
  • endpointstring
    Override the API origin.
  • fetchImpltypeof fetchDefault globalThis.fetch
    A custom fetch.
  • timeoutMsnumberDefault 5000
    Deadline covering the request and response body.
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 status, and optional code, details and requestId. Deadlines throw FingerlyTimeoutError; external cancellation preserves its reason. load, createProxy and verifyWebhook remain compatible function exports.

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

Fingerly.createProxy(options)

  • proxyKeystringrequired
    A proxy key, fly_px_…. Its prefix names the environment it pairs with.
  • clientIp(request: Request) => string | Promise<string>required
    Resolves the visitor's address from infrastructure you trust.
  • pathstring
    The path the dashboard issued, such as /k7x2qm4a, matched exactly; trailing slashes are ignored. Omit it when the route already sends only that path to the handler.
  • maxBodyBytesnumberDefault 1048576
    Larger bodies are refused with 413.
  • timeoutMsnumberDefault 5000
    How long to wait for Fingerly.
  • apiVersionstringDefault 'v1'
    The version of the upstream ingest URL.
  • upstreamstring
    Override the API origin.

With path set, the proxy answers 404 to any other path. It answers 405 to anything but POST, and 401 when the request carries no public key. Every request is forwarded to https://api.fingerly.io/api/v1/ingest, and only content-type, x-request-id and the rate limit headers are relayed back, so nothing on the response names Fingerly. Event reads need a secret key and are unreachable through it. Responses are marked no-store, customer cookies and authorization headers are never forwarded, and the proxy reports itself upstream as node/0.1.0, which the dashboard shows per proxy.