Skip to content

Signed delivery

Verify first. Dedupe before side effects.

Webhooks are application-scoped notifications, not a source of tenant access. A recipient must authenticate the raw request body, enforce freshness and store the event id atomically.

Delivery contract

PartContract
Eventsbooking.confirmed, booking.cancelled, booking.reschedule_requested, booking.rescheduled, booking.updated
HeadersX-Fenix-Event-Id, X-Fenix-Delivery-Id, X-Fenix-Timestamp, X-Fenix-Signature and Idempotency-Key
Compatibility namesX-Fenix-*, data-fenix-* and FENIX_* are stable legacy protocol names. Keep them exact in existing integrations.
Signaturev1=<HMAC-SHA256(timestamp + "." + raw_body)> using the one-time whsec_ secret
FreshnessAccept only the five-minute timestamp window and reject malformed envelope version/type/reference values.
RetriesFive bounded attempts with 1m, 5m, 30m and 2h backoff. 2xx completes; 3xx is rejected and never followed.

Minimal verification flow

js
import { createHmac, timingSafeEqual } from 'node:crypto';

const rawBody = await request.text();
const timestamp = request.headers.get('x-fenix-timestamp') || '';
const signature = request.headers.get('x-fenix-signature') || '';
const ageSeconds = Math.abs(Date.now() - Number(timestamp) * 1000) / 1000;
const expected = 'v1=' + createHmac('sha256', process.env.FENIX_WEBHOOK_SECRET)
  .update(timestamp + '.' + rawBody)
  .digest('hex');
const received = Buffer.from(signature);
const candidate = Buffer.from(expected);

if (!Number.isInteger(Number(timestamp)) || ageSeconds > 300 ||
  received.length !== candidate.length || !timingSafeEqual(received, candidate)) {
  return new Response(null, { status: 400 });
}

const event = JSON.parse(rawBody);
if (await events.has(event.id)) return new Response(null, { status: 204 });

await database.transaction(async () => {
  await events.insert({ id: event.id });
  await applyBookingEvent(event);
});

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

Receiver processing policy

  • Verify the signature against the unmodified raw request body before parsing or processing JSON. Validate the v1 signature format, timestamp window and expected event schema.
  • Persist X-Fenix-Event-Id atomically before side effects. X-Fenix-Delivery-Id identifies an attempt; Idempotency-Key repeats the event identifier for receiver deduplication.
  • Return a 2xx only after the event can be safely considered handled. Redirects are rejected and never followed; delivery failures receive at most five attempts with 1m, 5m, 30m and 2h delays after the initial attempt.
  • Treat webhook payloads as operational data. Keep them out of browser telemetry and redact them from logs, tickets and incident reports.

Endpoint safety

  • Register only a public HTTPS hostname. URLs with credentials, fragments, query strings and custom ports are rejected.
  • The platform resolves and validates DNS before configuration and on every delivery; loopback, private, link-local, reserved and mixed destinations are blocked.
  • The one-time whsec_ signing secret is shown only to the owner at creation or rotation. Store it in a server-side secret manager, rotate after exposure, and never send it to support.
  • Pause an endpoint during an incident. Replay only failed/skipped deliveries after the receiver is ready to dedupe the original event id.
Webhooks | Feny Developers