[{"data":1,"prerenderedAt":183},["ShallowReactive",2],{"doc:\u002Fdocs\u002Fsdks\u002Fnode":3},{"page":4,"toc":174,"updated":182},{"path":5,"title":6,"seoTitle":7,"description":8,"blocks":9},"\u002Fdocs\u002Fsdks\u002Fnode","Node.js","Node.js SDK: Read Events, Verify Webhooks, Proxy Requests","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.",[10,13,18,23,26,39,42,44,49,51,54,56,60,63,65,69,72,74,85,89,92,109,124,126,148,172],{"type":11,"text":12},"p","`@fingerly\u002Fnode` 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.",{"type":14,"level":15,"text":16,"id":17},"heading",2,"Requirements","requirements",{"type":19,"items":20},"list",[21,22],"Node.js 18.16 or newer, or any runtime with global `fetch`, `Request` and `Response` (Bun, Deno, Cloudflare Workers, Vercel Functions).","A [secret key](\u002Fdocs\u002Fapi-keys) for reading events, a webhook signing secret, and a [proxy key](\u002Fdocs\u002Fproxy-integrations) for the proxy.",{"type":14,"level":15,"text":24,"id":25},"Install","install",{"type":27,"samples":28},"code",[29,33,36],{"label":30,"lang":31,"code":32},"npm","bash","npm install @fingerly\u002Fnode",{"label":34,"lang":31,"code":35},"pnpm","pnpm add @fingerly\u002Fnode",{"label":37,"lang":31,"code":38},"yarn","yarn add @fingerly\u002Fnode",{"type":14,"level":15,"text":40,"id":41},"Read an event","read-an-event",{"type":11,"text":43},"Create one client with your secret key and reuse it. The key decides the regional API and the environment the client reads.",{"type":27,"samples":45},[46],{"label":6,"lang":47,"code":48},"ts","import { load } from '@fingerly\u002Fnode'\n\nconst fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })\nconst event = await fingerly.events.get('01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4')",{"type":11,"text":50},"An event has the fields listed in [Get an event](\u002Freference\u002Fget-event#response). `suspect_score` is `null` when the request was not scored.",{"type":14,"level":15,"text":52,"id":53},"Verify a checkout","verify-a-checkout",{"type":11,"text":55},"Read the event your client identified, check it belongs to this action and is recent, then decide on its level. See [server-side verification](\u002Fdocs\u002Fserver-side-verification).",{"type":27,"samples":57},[58],{"label":6,"lang":47,"code":59},"import { load, FingerlyAPIError } from '@fingerly\u002Fnode'\n\nconst fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })\nconst MAX_AGE_MS = 2 * 60 * 1000\n\nexport async function decide(orderId: string, requestId: string) {\n  let event\n  try {\n    event = await fingerly.events.get(requestId)\n  } catch (error) {\n    if (error instanceof FingerlyAPIError && error.status === 404) return 'refuse'\n    throw error\n  }\n\n  if (event.tag !== 'checkout:' + orderId) return 'refuse'\n  if (Date.now() - Date.parse(event.occurred_at) > MAX_AGE_MS) return 'refuse'\n\n  if (event.suspect_level === 'high') return 'review'\n  if (event.suspect_level === 'medium') return 'challenge'\n  return 'allow'\n}",{"type":14,"level":15,"text":61,"id":62},"Verify a webhook","verify-a-webhook",{"type":11,"text":64},"Check the signature over the raw request body before parsing it. The helper rejects timestamps more than five minutes from now.",{"type":27,"samples":66},[67],{"label":6,"lang":47,"code":68},"import express from 'express'\nimport { verifyWebhook } from '@fingerly\u002Fnode'\n\napp.post('\u002Fwebhooks\u002Ffingerly', express.raw({ type: 'application\u002Fjson' }), async (req, res) => {\n  const valid = verifyWebhook({\n    secret: process.env.FINGERLY_WEBHOOK_SECRET!,\n    payload: req.body,\n    timestamp: req.get('x-fingerly-timestamp'),\n    signature: req.get('x-fingerly-signature'),\n  })\n  if (!valid) return res.sendStatus(400)\n\n  const event = JSON.parse(req.body.toString('utf8'))\n  await queue.add(event.id, event)   \u002F\u002F deduplicate on event.id\n  res.sendStatus(204)\n})",{"type":14,"level":15,"text":70,"id":71},"Serve the browser SDK from your domain","serve-the-browser-sdk-from-your-domain",{"type":11,"text":73},"`createProxy` returns a `(request: Request) => Promise\u003CResponse>` 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`.",{"type":27,"samples":75},[76,79,82],{"label":77,"lang":47,"code":78},"Next.js","\u002F\u002F app\u002Fmetrics\u002F[...path]\u002Froute.ts\nimport { createProxy } from '@fingerly\u002Fnode'\n\nexport const POST = createProxy({\n  proxyKey: process.env.FINGERLY_PROXY_KEY!,     \u002F\u002F fly_px_us_production_…\n  prefix: '\u002Fmetrics',\n  clientIp: (request) => request.headers.get('x-real-ip') ?? '',\n})",{"label":80,"lang":47,"code":81},"Hono","import { Hono } from 'hono'\nimport { createProxy } from '@fingerly\u002Fnode'\n\nconst proxy = createProxy({\n  proxyKey: process.env.FINGERLY_PROXY_KEY!,\n  prefix: '\u002Fmetrics',\n  clientIp: (request) => request.headers.get('cf-connecting-ip') ?? '',\n})\n\nconst app = new Hono()\napp.post('\u002Fmetrics\u002F*', (c) => proxy(c.req.raw))",{"label":83,"lang":47,"code":84},"Browser","import { load } from '@fingerly\u002Fweb-js'\n\nconst fingerly = await load({ apiKey: 'fly_pk_us_production_…', endpoints: '\u002Fmetrics' })",{"type":86,"tone":87,"text":88},"callout","warning","Resolve `clientIp` from a header your own infrastructure sets, such as your load balancer's or CDN's. Never use the left-most `X-Forwarded-For` value: the visitor can write it.",{"type":14,"level":15,"text":90,"id":91},"API","api",{"type":93,"title":94,"fields":95},"fields","`load(options)`",[96,101,104],{"name":97,"type":98,"text":99,"required":100},"secretKey","string","A secret key. Its prefix decides the regional API.",true,{"name":102,"type":98,"text":103},"endpoint","Override the API origin.",{"name":105,"type":106,"text":107,"default":108},"fetchImpl","typeof fetch","A custom `fetch`.","globalThis.fetch",{"type":110,"columns":111,"rows":115},"table",[112,113,114],"Method","Returns","Calls",[116,120],[117,118,119],"`events.list(query?)`","`Promise\u003CEventPage>`","[`GET \u002Fevents`](\u002Freference\u002Flist-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.",[121,122,123],"`events.get(requestId, signal?)`","`Promise\u003CEvent>`","[`GET \u002Fevents\u002F{request_id}`](\u002Freference\u002Fget-event).",{"type":11,"text":125},"A non-2xx response throws `FingerlyAPIError` with the HTTP `status`. Network errors and aborts are thrown as they are.",{"type":93,"title":127,"fields":128},"`verifyWebhook(options)`, returns `boolean`",[129,132,136,140,143],{"name":130,"type":98,"text":131,"required":100},"secret","The endpoint's signing secret, `whsec_…`.",{"name":133,"type":134,"text":135,"required":100},"payload","string | Uint8Array","The raw request body, byte for byte.",{"name":137,"type":138,"text":139,"required":100},"timestamp","string | null","The `x-fingerly-timestamp` header.",{"name":141,"type":138,"text":142,"required":100},"signature","The `x-fingerly-signature` header.",{"name":144,"type":145,"text":146,"default":147},"toleranceSeconds","number","How far the timestamp may be from now.","300",{"type":93,"title":149,"fields":150},"`createProxy(options)`",[151,154,158,162,166,170],{"name":152,"type":98,"text":153,"required":100},"proxyKey","A proxy key, `fly_px_…`. Its prefix decides the regional API.",{"name":155,"type":156,"text":157,"required":100},"clientIp","(request: Request) => string | Promise\u003Cstring>","Resolves the visitor's address from infrastructure you trust.",{"name":159,"type":98,"text":160,"default":161},"prefix","The path the proxy is mounted under.","'\u002Fapi\u002Ffingerly'",{"name":163,"type":145,"text":164,"default":165},"maxBodyBytes","Larger bodies are refused with `413`.","1048576",{"name":167,"type":145,"text":168,"default":169},"timeoutMs","How long to wait for Fingerly.","5000",{"name":171,"type":98,"text":103},"upstream",{"type":11,"text":173},"The proxy answers `405` to anything but `POST`, `404` to any path except `{prefix}\u002Fapi\u002Fv1\u002Fidentify` and `{prefix}\u002Fapi\u002Fv1\u002Fevents\u002F{request_id}\u002Fsupplement`, and `401` when the request carries no public key.",[175,176,177,178,179,180,181],{"id":17,"text":16,"level":15},{"id":25,"text":24,"level":15},{"id":41,"text":40,"level":15},{"id":53,"text":52,"level":15},{"id":62,"text":61,"level":15},{"id":71,"text":70,"level":15},{"id":91,"text":90,"level":15},"2026-09-17T16:57:52.000Z",1789667797817]