Each delivery includes a single header X-Praxium-Signature: t=<unix_ts>,sha256=<hmac_hex>, where the HMAC-SHA256 is computed server-side over ${timestamp}.${rawBody} using the per-webhook secret. The shared secret never crosses the wire — only the HMAC output does. The signature proves two things at once: the body wasn't tampered with in transit (integrity), and the call genuinely came from Praxium and not an attacker who guessed your endpoint URL (authenticity). All deliveries are dispatched over HTTPS — Praxium refuses to register webhook URLs that aren't HTTPS in deployed environments.
As a webhook recipient, you're responsible for verifying every delivery — Praxium signs and dispatches, but enforcement happens in your handler. If you're using @praxium/sdk, you don't write any of this by hand: processWebhook() (framework-agnostic) and createRevalidationHandler() (Next.js ISR) bake in all four steps plus replay protection. Hand-implementing in another runtime? The four steps are: (1) parse the timestamp and signature from the header, (2) reject deliveries older than your replay window — 5 minutes is the standard, (3) recompute the HMAC over ${timestamp}.${rawBody} with your shared secret, (4) compare with constant-time comparison (e.g. crypto.timingSafeEqual on Node).
This is the same scheme Stripe uses for webhook signatures. Per-webhook secrets are returned exactly once — in the response when you create the webhook and in each rotation response — and never re-surface afterwards. That single-exposure model means there's no long-lived attack surface for the secret on the platform side: even a compromised admin session can't pull it out again. Need a fresh one? Rotate from the admin portal — the new secret arrives in the rotation response, the previous one is invalidated immediately, and other subscriptions are untouched.