Skip to main content
Every webhook delivery is signed with the endpoint’s signing secret using HMAC-SHA256. Verify the signature before processing any event. parseWebhookEvent (sync, node:crypto) and parseWebhookEventAsync (WebCrypto) throw WebhookVerificationError when the signature is invalid, the timestamp is stale (older than 5 minutes by default), or a required header is missing. Both return a typed WebhookEvent on success. Next.js app router uses the async function.

The signature scheme

The HMAC is computed over:
  • webhook_id is the Webhook-Id header
  • timestamp is the Webhook-Timestamp header (Unix seconds)
  • body is the raw request body (not parsed JSON)
The result is base64-encoded and prefixed with v1,. The Webhook-Signature header may contain multiple space-separated signatures during key rotation. Accept any match. For a verifier in another language, remove the whsec_ prefix from the signing secret and base64-decode the remainder to obtain the HMAC key. Compare signatures in constant time, and reject timestamps more than five minutes in the past or future. The SDK handles these checks.
Always read the raw body for signature verification. Parsing JSON first and re-stringifying changes whitespace and breaks the HMAC.

Next.js (app router)

Use parseWebhookEvent where node:crypto exists, and parseWebhookEventAsync for WebCrypto. Next.js pages-router handlers need export const config = { api: { bodyParser: false } }. In Express, mount express.raw({ type: "application/json" }) on the webhook route before any JSON middleware. Both must pass the original body bytes to the verifier. The handler above illustrates signature verification. For production processing, add durable deduplication as described below before running your event handlers.

Idempotency

Deduplicate on Webhook-Id, which stays the same across retries. Concurrent deliveries can overlap, so a separate check, process, and mark sequence is not sufficient. One option is a durable inbox in your own database. Create a unique key on the delivery ID:
After verification, insert the event atomically. Here db is your server’s database client with parameterized queries. Return 2xx only after the insert commits; database errors must return a failure so AGG can retry.
Your worker processes unprocessed rows and commits its database changes and processed_at together in one transaction. Use the delivery ID as an idempotency key for external side effects too. Keep processed IDs for your replay window, not just the automatic retry period; the replay endpoint can resend older deliveries. Events can arrive out of order. Use the event’s resource IDs to read current API state before updating a balance or order display. See delivery behavior.