Elke bezorging bevat één header X-Praxium-Signature: t=<unix_ts>,sha256=<hmac_hex>, waarbij de HMAC-SHA256 op de server wordt berekend over ${timestamp}.${rawBody} met het per-webhook secret. Het gedeelde secret komt nooit op de wire — alleen de HMAC-output gaat over de lijn. De signature bewijst twee dingen tegelijk: de body is niet gemanipuleerd onderweg (integriteit), en de aanroep komt daadwerkelijk van Praxium en niet van een aanvaller die jouw endpoint-URL heeft geraden (authenticiteit). Alle bezorgingen gaan over HTTPS — Praxium accepteert geen webhook-URL's zonder HTTPS in deployed-omgevingen.
Als webhook-ontvanger ben je verantwoordelijk voor het verifiëren van elke bezorging — Praxium tekent en bezorgt, maar de handhaving gebeurt in jouw handler. Gebruik je @praxium/sdk, dan hoef je hier niets van met de hand te schrijven: processWebhook() (framework-agnostic) en createRevalidationHandler() (Next.js ISR) bakken alle vier stappen plus replay-protection in. Hand-implementeren in een andere runtime? De vier stappen zijn: (1) parse timestamp en signature uit de header, (2) wijs bezorgingen ouder dan je replay-window af — 5 minuten is de standaard, (3) bereken de HMAC opnieuw over ${timestamp}.${rawBody} met je eigen secret, (4) vergelijk met constant-time comparison (bijv. crypto.timingSafeEqual op Node).
Dit is hetzelfde schema dat Stripe gebruikt voor webhook-signatures. Per-webhook secrets worden precies één keer teruggegeven — in de response bij het aanmaken van de webhook en in de response bij elke rotatie — en verschijnen daarna nergens meer. Dat eenmalige tonen betekent dat het secret aan onze kant geen langdurig aanvalsoppervlak is: zelfs een gecompromitteerde admin-sessie kan het niet meer ophalen. Nieuwe nodig? Roteer vanuit de admin-portal — het nieuwe secret komt in de rotatie-response, het vorige wordt direct ongeldig, en andere subscriptions blijven ongemoeid.