Delivery contract
| Part | Contract |
|---|---|
| Events | booking.confirmed, booking.cancelled, booking.reschedule_requested, booking.rescheduled, booking.updated |
| Headers | X-Fenix-Event-Id, X-Fenix-Delivery-Id, X-Fenix-Timestamp, X-Fenix-Signature and Idempotency-Key |
| Compatibility names | X-Fenix-*, data-fenix-* and FENIX_* are stable legacy protocol names. Keep them exact in existing integrations. |
| Signature | v1=<HMAC-SHA256(timestamp + "." + raw_body)> using the one-time whsec_ secret |
| Freshness | Accept only the five-minute timestamp window and reject malformed envelope version/type/reference values. |
| Retries | Five 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.
Feny