Guide

Receive webhooks reliably

A webhook handler that verifies, answers in time, never processes an event twice, and recovers from anything it missed.

Five rules#

  1. Verify the signature on the raw body, and reject anything that fails. See Verify signatures.
  2. Answer fast. Return 2xx within 10 seconds; queue the real work.
  3. Deduplicate on the event id. Store it with a unique constraint, so a second delivery does nothing.
  4. Don't trust order. When state matters, re-read the object from the API instead of applying events in arrival order.
  5. Watch the delivery log. Failures, retries and dead deliveries show there.
import express from "express";
import { verifyFluxSignature } from "./verify-flux-signature.js"; // from "Verify signatures"

const app = express();

app.post("/flux", express.raw({ type: "application/json" }), async (req, res) => {
  // 1. Verify against the raw body.
  if (!verifyFluxSignature(process.env.FLUX_WEBHOOK_SECRET, req.get("Flux-Signature"), req.body)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);

  // 2. Record the event id once. A unique index on event_id turns duplicates into no-ops.
  const isNew = await db.query(
    "INSERT INTO flux_events (event_id, type, body) VALUES ($1, $2, $3) ON CONFLICT (event_id) DO NOTHING",
    [event.id, event.type, req.body.toString("utf8")],
  ).then((r) => r.rowCount === 1);

  // 3. Queue the work and answer at once (well inside 10 seconds).
  if (isNew) await queue.add("flux-event", { eventId: event.id });
  res.sendStatus(200);
});

// 4. The worker re-reads current state when order matters.
queue.process("flux-event", async ({ eventId }) => {
  const event = await loadEvent(eventId);
  if (event.type.startsWith("invoice.")) {
    const invoice = await flux(`/invoices/${event.data.object.id}`).then((r) => r.json());
    await saveInvoiceState(invoice);
  }
  // ...other types; ignore the ones you don't handle.
});

Monitoring and recovery#

  • Alert when your endpoint returns errors. After 50 failures in a row Flux switches the endpoint off and emails the organization.
  • Check the delivery log for dead deliveries, fix the cause, then redeliver them. Re-enable a disabled endpoint with PATCH {"active": true}.
  • As a safety net, reconcile on a schedule: for invoices you still consider open, read them with GET /invoices/{id} (or list a customer's invoices) and compare statuses. Webhooks are fast; reconciliation catches anything missed.