Furlpay Docs
Open App

Architecture

Webhook Events

Every webhook handler verifies a signature over the raw body before trusting a byte, rejects stale timestamps, and fails closed when its secret is unset. This page lists the inbound handlers and shows how to verify events Furlpay sends you.

Inbound handlers

MethodPathDescription
POST/api/webhooks/stripeStripe events — stripe-signature verified via constructEvent
POST/api/webhooks/sumsubSumsub KYC outcomes — HMAC digest over the raw body
POST/api/webhooks/personaPersona inquiry outcomes — persona-signature HMAC
POST/api/webhooks/wiseWise transfer state changes — signature verified
POST/api/webhooks/bridgeBridge fiat rail events — signature verified
POST/api/webhooks/marqetaCard processor callbacks (JIT funding, auth events)
POST/api/webhooks/card-auth3DS2 card authentication events — endpoint secret
  • Signatures are computed over the raw request body — parse only after verification.
  • Comparisons are constant-time; timestamped schemes reject events older than the tolerance window.
  • A handler whose secret is not configured refuses the event (fail closed) rather than accepting unsigned input.
  • Webhooks are exempt from the mutating-request IP rate-limit baseline (providers batch deliveries from few IPs) — the signature is the gate.

Verifying events from Furlpay

Events delivered to your endpoint carry a Furlpay-Signature header: an HMAC-SHA256 over timestamp + "." + rawBody with your endpoint secret. Verify before trusting:

typescriptapp/api/webhooks/furlpay/route.ts
import crypto from "crypto";

export async function POST(req: Request) {
  const raw = await req.text();                       // RAW body — do not parse first
  const sig = req.headers.get("furlpay-signature") ?? "";
  const ts = req.headers.get("furlpay-timestamp") ?? "";

  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
    return new Response("stale", { status: 400 });    // replay window: 5 minutes
  }
  const expected = crypto
    .createHmac("sha256", process.env.FURLPAY_WEBHOOK_SECRET!)
    .update(`${ts}.${raw}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(sig);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return new Response("bad signature", { status: 401 });
  }

  const event = JSON.parse(raw);                      // safe to parse now
  // handle event.type ...
  return new Response("ok");
}

Local development

Forward live events to localhost with the CLI — it prints the signing secret to verify against:

bash
furlpay listen --forward-to localhost:3000/api/webhooks/furlpay

Never credit balances from an unverified event

A webhook body is attacker-controllable input until the signature passes. Verify first, act second, and make handlers idempotent — providers redeliver.
Did this page help?
Edit this page on GitHub