Guides

Webhooks

Receive identifications, high-risk visitors, refusals, billing changes and daily usage as signed HTTPS requests to your server, the moment they happen.

Webhooks are the server API in reverse: instead of reading events from Fingerly, Fingerly sends them to an HTTPS endpoint on your server as they happen. Use them to:

  • Keep your own copy of every identification for as long as you need it. Fingerly keeps events readable for 30 days.
  • React to high-risk visitors in your fraud tooling without polling.
  • Alert on refused requests before they become an outage.
  • Reconcile usage and billing with your own records.

Webhooks are delivered asynchronously and add no latency to identification.

Events

EventSent when
identification.completedAny identification finishes.
visitor.suspectAn identification reaches the high level.
identification.refusedAn identify request is refused.
billing.status_changedYour organization starts or stops accepting traffic.
usage.daily_settledA day of usage is settled.

Implement a handler

Create a route that accepts a POST with a JSON body, verifies the signature, hands the event to your own queue, and returns 2xx quickly.

// app/webhooks/fingerly/route.ts
import { verifyWebhook } from '@fingerly/node'

export async function POST(request: Request) {
  const payload = await request.text()

  const valid = verifyWebhook({
    secret: process.env.FINGERLY_WEBHOOK_SECRET!,
    payload,
    timestamp: request.headers.get('x-fingerly-timestamp'),
    signature: request.headers.get('x-fingerly-signature'),
  })
  if (!valid) return new Response('invalid signature', { status: 400 })

  const event = JSON.parse(payload)
  await queue.add(event.id, event)   // deduplicate on event.id

  return new Response(null, { status: 204 })
}

Responses and timeouts

  • Any 2xx response completes the delivery.
  • 410 Gone stops the delivery permanently, without retries.
  • Any other response, a timeout or a connection failure is retried.
  • Fingerly waits 10 seconds for a response. Acknowledge first and do slow work from your own queue.
  • Redirects are not followed. Register the final URL.

Retries

A failed delivery is attempted up to six times in total. The retries come after:

Retry12345
Delay after the previous attempt30 seconds2 minutes10 minutes1 hour6 hours

After the last attempt fails, the delivery is marked failed. An endpoint whose deliveries keep failing is shown as failing in the dashboard, and keeps receiving new events.

Duplicates and ordering

Delivery is at least once: when an outcome is ambiguous, such as a timeout after your server processed the request, the same event can arrive again. Its id never changes, so deduplicate on it. Events can arrive out of order; use created_at when order matters.

Register an endpoint

  1. Step 1: Open Webhooks

    In the dashboard, go to Integration > Webhooks and add an endpoint.

  2. Step 2: Configure it

    • URL: an https:// URL that resolves to a public address.
    • Environment: Live receives production traffic; Test receives staging and development traffic.
    • Events: the event types to receive.
    • Description: optional, to tell endpoints apart.
  3. Step 3: Save the signing secret

    The signing secret, whsec_…, is shown once, when the endpoint is created. Store it in your server's secret manager.

  • An organization can have up to 10 endpoints across both environments.
  • Owners, admins and developers can manage webhooks.
  • billing.status_changed is available to live endpoints only.
  • Pause an endpoint during maintenance on your side. A paused endpoint is not sent new events; deliveries already queued for it wait and are sent when you resume it.

Verify the signature

Every delivery is signed with the endpoint's secret. Verify it before you parse or act on the body: anyone can send a request to a public URL.

HeaderValue
X-Fingerly-TimestampUnix seconds when the attempt was signed.
X-Fingerly-Signaturesha256= and the lowercase hex HMAC-SHA256 of timestamp.body. During a secret rotation, one signature per secret, separated by commas.
X-Fingerly-Event-IDThe event ID, for deduplication.
X-Fingerly-Event-TypeThe event type, for routing.

Every server SDK's helper does this for you, as in the handlers above. Without an SDK, the check is one HMAC:

signed   = timestamp + "." + raw_body
expected = hex(hmac_sha256(key = signing_secret, message = signed))

valid = any(
          constant_time_equal(expected, candidate without "sha256=")
          for candidate in signature split on ","
        )
    and abs(now - timestamp) <= 300 seconds

Rotate a secret

In Integration > Webhooks, open the endpoint's menu and choose Rotate secret. The new secret is shown once. Choose how long the old secret stays valid, from immediately up to seven days; the default is 24 hours.

Until then, every delivery carries two signatures in X-Fingerly-Signature, separated by a comma: the new secret's first, then the old one's. Accept a delivery when any of them verifies, as the SDK helpers and the samples above do, and deploy the new secret at any point in the window.

Rotating again before the window ends ends it: only the secret being replaced stays valid alongside the new one.

Delivery history

Integration > Webhooks lists every delivery attempt from the last 30 days with its event, status (delivered, retrying or failed), response code, duration and attempt number, for live and test endpoints.

  • Redeliver sends an event from the history to the same endpoint again, with the same id. It is one attempt, and it does not affect the retries of the original delivery.
  • Send test event sends a signed webhook.test event to one endpoint, in one attempt, so you can check your URL and signature verification.
  • A failed test or redelivery does not mark the endpoint as failing. Neither is available while the endpoint is paused, and only one at a time can be queued for an endpoint.

Test locally

Webhook URLs must be public, so expose your local server with a tunnel such as cloudflared or ngrok, register the tunnel's URL on a Test endpoint, and identify with a development key.

Terminal
cloudflared tunnel --url http://localhost:3000