Skip to content

End-to-end: webhook handling

Terminal window
curl -X POST https://api.rieckflow.com/v1/webhook-endpoints \
-H "Authorization: Bearer $RIECK_API_KEY" \
-H "Idempotency-Key: webhook-production" \
-H "Content-Type: application/json" \
-d '{
"url": "https://erp.acme.example/webhooks/rieck",
"description": "Production events",
"eventTypes": ["invoice.paid", "customer.past_due"]
}'

The response includes data.secret once. Store the complete whsec_... value in your secret manager before acknowledging setup.

Every delivery carries webhook-id, webhook-timestamp and webhook-signature. Verify the signature against the raw request body bytes before parsing JSON. The default timestamp tolerance is five minutes.

import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyRieckWebhook(
rawBody: string,
headers: Headers,
secret: string,
): unknown {
const id = headers.get("webhook-id");
const timestamp = headers.get("webhook-timestamp");
const candidates = (headers.get("webhook-signature") ?? "").split(" ");
if (!id || !timestamp || candidates.length === 0) throw new Error("Missing webhook headers");
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!Number.isFinite(age) || age > 300) throw new Error("Stale webhook timestamp");
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key)
.update(`${id}.${timestamp}.${rawBody}`, "utf8")
.digest();
const valid = candidates.some((candidate) => {
const value = Buffer.from(candidate.replace(/^v1,/, ""), "base64");
return value.length === expected.length && timingSafeEqual(value, expected);
});
if (!valid) throw new Error("Invalid webhook signature");
return JSON.parse(rawBody);
}

During secret rotation the signature header can contain two space-separated signatures. Accept a match against either signature while the 24-hour overlap is active.

  1. Verify the signature.
  2. Insert webhook-id into a unique/deduplication table.
  3. Return any 2xx response quickly.
  4. Process the event asynchronously.
  5. Fetch the referenced invoice, customer, subscription or case before changing state.

Rieck retries 408, 429 and 5xx responses with backoff. Other 4xx responses are treated as permanent rejections. Redirects are never followed. Use GET /v1/events for catch-up and the replay endpoint for failed deliveries.