Skip to main content
Every webhook delivery is signed with your endpoint secret (whsec_...) using HMAC-SHA256. Verify the signature before trusting any payload — an unverified webhook could be forged by anyone who knows your URL.

Header format

  • t — delivery timestamp in milliseconds.
  • v1 — hex HMAC-SHA256 of the string "<t>.<raw body>" keyed with your secret.
  • During the 24 hours after a secret rotation, the header carries two v1 entries — one per secret. A match on either is valid.

Verification steps

  1. Read the raw request body, before any JSON parsing or re-serialization.
  2. Parse the header: extract t and every v1 value.
  3. Compute HMAC-SHA256(secret, "<t>.<rawBody>") as lowercase hex.
  4. Compare against each v1 using a constant-time comparison. Any match passes.
  5. Reject if the timestamp is stale — 5 minutes tolerance is a good default.
The HMAC is computed over the raw bytes we sent. If your framework parses JSON before you can read the body, the re-serialized string will not match — configure a raw-body route for the webhook path (see the framework notes in each example).

Rotating secrets

Response
Rotation is zero-downtime: for 24 hours, deliveries are signed with both secrets (two v1 entries), so you can deploy the new secret at your own pace. After the window, only the new secret signs. Test the full path any time with POST /retailer/v1/webhook/test — it sends a real signed ping synchronously and reports your endpoint’s response code.

Checklist

  • Verify on the raw body, before JSON parsing
  • Constant-time comparison (timingSafeEqual, hmac.compare_digest, hash_equals)
  • Accept any matching v1 entry (rotation window has two)
  • Enforce a timestamp tolerance (about 5 minutes)
  • Deduplicate on the delivery id (X-Wizzgift-Delivery)
  • Respond 2xx fast; process asynchronously