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#
- Verify the signature on the raw body, and reject anything that fails. See Verify signatures.
- Answer fast. Return 2xx within 10 seconds; queue the real work.
- Deduplicate on the event id. Store it with a unique constraint, so a second delivery does nothing.
- Don't trust order. When state matters, re-read the object from the API instead of applying events in arrival order.
- 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.
});import json
import os
from flask import Flask, request
from verify import verify_flux_signature # from "Verify signatures"
app = Flask(__name__)
@app.post("/flux")
def flux_webhook():
raw = request.get_data()
# 1. Verify against the raw body.
if not verify_flux_signature(os.environ["FLUX_WEBHOOK_SECRET"], request.headers.get("Flux-Signature"), raw):
return "", 400
event = json.loads(raw)
# 2. Record the event id once. A unique index on event_id turns duplicates into no-ops.
is_new = db.execute(
"INSERT INTO flux_events (event_id, type, body) VALUES (%s, %s, %s) ON CONFLICT (event_id) DO NOTHING",
(event["id"], event["type"], raw.decode()),
).rowcount == 1
# 3. Queue the work and answer at once (well inside 10 seconds).
if is_new:
queue.enqueue(process_flux_event, event["id"])
return "", 200
# 4. The worker re-reads current state when order matters.
def process_flux_event(event_id):
event = load_event(event_id)
if event["type"].startswith("invoice."):
invoice = flux("GET", f"/invoices/{event['data']['object']['id']}")
save_invoice_state(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
deaddeliveries, fix the cause, then redeliver them. Re-enable a disabled endpoint withPATCH {"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.