# Receive webhook events Get signed notifications when purchases, renewals or balances change. ## Create a subscription Create `POST /webhooks` with a name, your public HTTPS URL, `enabled: true` and the `event_types` you need. Send a saved `Idempotency-Key`. Your key needs write access and `webhooks:write`. Store the returned signing secret securely. Use the entire `whsec_…` value, including the prefix. Save the secret from the creation response for future signature checks. | Event | What it tells you | | --- | --- | | `fulfillment.updated` | A static resource or its fulfillment status changed | | `renewal.updated` | A renewal changed status | | `dynamic.purchase.updated` | A traffic purchase changed status | | `refund.completed` | A refund completed | | `wallet.low_balance` | The wallet crossed below your chosen threshold | For low-balance alerts, include `wallet_low_balance_threshold_fen` as an integer string. Notifications cover changes after subscription creation. ## Verify the signature Preserve the raw request body. Compute HMAC-SHA256 over the timestamp, a period and the exact body bytes, using your signing secret. Compare against `X-Webhook-Signature` in constant time. This Node.js helper assumes `rawBody` is a Buffer and `headers` is a Headers object: ```js import { createHmac, timingSafeEqual } from 'node:crypto'; function verifyWebhook(rawBody, headers, secret) { const timestamp = headers.get('X-Webhook-Timestamp') ?? ''; const signature = headers.get('X-Webhook-Signature') ?? ''; if (!/^\d+$/.test(timestamp)) return false; if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; if (!/^v1=[0-9a-f]{64}$/.test(signature)) return false; const expected = createHmac('sha256', secret) .update(timestamp + '.') .update(rawBody) .digest(); const actual = Buffer.from(signature.slice(3), 'hex'); return timingSafeEqual(expected, actual); } ``` Parse the JSON only after verification. Re-serializing JSON before verification changes the signed bytes. ## Handle duplicates and retries Use the event's `id` to avoid processing a notification twice. Events can arrive more than once or out of order. Read the related API resource when you need its latest status. Return `2xx` after safely storing or processing the event. Failed deliveries retry up to eight times per round, with delays starting at 30 seconds and rising to one hour. Read delivery history with `GET /webhooks/{id}/deliveries`. Retry a failed delivery with `POST /webhooks/{id}/deliveries/{delivery}/retry`. ## Rotate the secret Use `POST /webhooks/{id}/rotate-secret` to replace it. An in-flight delivery may still use the previous secret, so allow a short transition in your receiver. Your endpoint must accept requests directly over public HTTPS on port 443.