@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,RequestandResponse(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.
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.
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.
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)
secretKeystringrequiredA secret key. Its prefix decides the regional API.endpointstringOverride the API origin.fetchImpltypeof fetchDefaultglobalThis.fetchA customfetch.
| Method | Returns | Calls |
|---|---|---|
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
secretstringrequiredThe endpoint's signing secret,whsec_….payloadstring | Uint8ArrayrequiredThe raw request body, byte for byte.timestampstring | nullrequiredThex-fingerly-timestampheader.signaturestring | nullrequiredThex-fingerly-signatureheader.toleranceSecondsnumberDefault300How far the timestamp may be from now.
createProxy(options)
proxyKeystringrequiredA proxy key,fly_px_…. Its prefix decides the regional API.clientIp(request: Request) => string | Promise<string>requiredResolves the visitor's address from infrastructure you trust.prefixstringDefault'/api/fingerly'The path the proxy is mounted under.maxBodyBytesnumberDefault1048576Larger bodies are refused with413.timeoutMsnumberDefault5000How long to wait for Fingerly.upstreamstringOverride 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.