Create a webhook endpoint
Delivery retries
Failed deliveries are retried up to 12 attempts over roughly 24 hours with exponential backoff and +/-20% jitter. Delay before each retry: 30s, 1m, 2m, 5m, 15m, 30m, 1h, 2h, 4h, 8h, 8h (the ladder totals 23h53m30s before jitter). A response with any 2xx status counts as delivered; anything else (including timeouts after 10s) schedules the next attempt. When an event exhausts all attempts the delivery is marked failed, and repeated failures disable the endpoint automatically. webhook_endpoint.delivery_failed is sent to your OTHER endpoints, never to the endpoint whose delivery failed.
Ordering and idempotency
Events are NOT guaranteed to arrive in order, and any event may be delivered more than once (a retry after a slow response, or a redelivery). De-duplicate on the webhook-id header - the stable event id, identical across every retry of one event - and order by the payload created_at timestamp. Make your handler idempotent: processing the same event id twice must be a no-op.
Registers an HTTPS receiver for outbound events. Scope: webhooks:manage. Rate limit: 10 creations/hour.
The secret is returned exactly once - in this response and nowhere else. Store it immediately; if lost, rotate.
Registration checks
- SSRF validation - public DNS only, no private/reserved ranges, no cloud metadata endpoints, no localhost (
422 webhook_url_invalid) - URL text - at most 2,048 characters, no user name or password, no spaces, control characters or backslashes (
422 webhook_url_invalid). The same check runs onPATCHwhen you send a newurl - Event catalog check - unknown names in
enabled_eventsreturn422 webhook_event_unknown - Reachability probe - one HEAD request at create; DNS/TLS failures are hard errors (
422 webhook_url_unreachable), non-2xx is tolerated
What your receiver must do
Verify the HMAC on every request, enforce the 5-minute anti-replay window, dedup on the event id, and ack within 10 seconds. Full verification code + retry schedule in Webhooks.
How deliveries are signed (Standard Webhooks)
Every delivery is a POST with three headers - the Standard Webhooks scheme, the same one Svix-based platforms use, so you can verify with any off-the-shelf standard-webhooks / svix library:
- Signed content:
{webhook-id}.{webhook-timestamp}.{raw body}- exact bytes, no normalization - Algorithm: HMAC-SHA256, key = the base64-decoded part of your
whsec_...secret - Reject timestamps older than 5 minutes (replay protection)
- During the 24 h rotation grace the header carries TWO space-delimited
v1,signatures (old + new) - accept if ANY matches; rotation needs no code change on your side
Full verification guide with code samples: Webhooks.
Delivery contract
- At-least-once, no ordering guarantee - dedupe by envelope
id, never assume sequence (fetch current state via the API when order matters) - Your receiver must return 2xx within 10 seconds - defer real work to a queue; anything else (timeouts, 3xx, 4xx, 5xx) counts as failed
- Failed deliveries retry 12 times across ~24 hours (exponential backoff + jitter - full table in the Webhooks guide); after the last attempt the delivery is
failedandwebhook_endpoint.delivery_failedfires to your OTHER endpoints - Auto-disable: 50 consecutive failures, or a single
HTTP 410 Gone, setsstatus: "auto_disabled"; re-enable withPATCH status: "enabled"(missed events are not replayed - reconcile via list endpoints) - Subscribe narrowly (
enabled_events) - fewer events, less load, and you can always widen later
Multiple endpoints, duplicate URLs, limits
- Up to 16 endpoints per organization (
409 webhook_endpoint_limit_reached) - Stripe’s proven ceiling - Duplicate URLs are allowed and each endpoint is independent (own secret, own subscriptions, own version). Two endpoints on the same URL with overlapping
enabled_eventsmeans your receiver gets the event twice, with different signatures - legitimate when splitting event subsets or migrating API versions, otherwise narrow the subscriptions. Your dedupe key is the envelopeid, which is identical across both deliveries - api_version is pinned per endpoint at creation - version migration = new endpoint on the new version, test, cut over, delete the old
Authorizations
Your secret API key as a Bearer token: Authorization: Bearer vxn_sk_.... Keys are created and scoped in the dashboard - see Authentication.
Headers
Required on every POST, PUT, PATCH, and DELETE. 24-hour dedup window keyed on (api_key_id, idempotency_key). Up to 255 chars, [a-zA-Z0-9_-].
255^[a-zA-Z0-9_-]+$Body
Register a new outbound endpoint. Scope: webhooks:manage. The response includes the signing secret exactly once. Registration runs SSRF validation (https only; private, loopback and reserved ranges rejected, including DNS-rebinding re-checks on the resolved address) plus a single HEAD reachability probe (any HTTP status is accepted; a DNS/TLS/connection failure is rejected as webhook_url_unreachable). Rate limit: 10 endpoint creations per hour.
HTTPS URL to deliver events to. Must pass SSRF validation: public DNS, no private/reserved IP ranges, no cloud metadata endpoints, publicly-trusted TLS in live mode.
"https://hooks.demo-coffee.example/vexion"
Event names to subscribe to, or ["*"] for everything (current + future). Unknown names return 422 webhook_event_unknown.
1What an endpoint SUBSCRIBES to: either one of the 22 catalog event names, or the wildcard * meaning every event, current and future.
This is deliberately distinct from WebhookEventName. A DELIVERED event always carries a concrete name, so * is never valid as WebhookEvent.type, WebhookDelivery.event_type, the event_type you send to POST /v1/webhook_endpoints/{id}/test, or the event_type filter on the deliveries list. It is valid ONLY inside enabled_events.
* must be sent alone: ["*", "customer.created"] returns 422 validation_failed.
customer.created, customer.updated, customer.deleted, customer.pass_installed, customer.pass_uninstalled, card.created, card.updated, card.deleted, custom_field.created, custom_field.updated, custom_field.deleted, product.created, product.updated, transaction.created, transaction.reversed, notification.sent, notification.delivery_updated, location.updated, team_member.added, team_member.updated, team_member.removed, webhook_endpoint.delivery_failed "customer.created"
Optional merchant-facing label.
200"Loyalty events to ERP"
Response
Created. The plaintext secret is returned exactly once.
The webhook endpoint INCLUDING its signing secret. Returned ONLY by POST /v1/webhook_endpoints (create) and POST .../rotate_secret. The secret never appears on any GET - if lost, rotate.
Endpoint identifier, whk_ prefixed opaque string. Never a database id.
^whk_[A-Za-z0-9]+$"whk_01HW9RBZ4GH7N8MTX1RVWHKDEF"
Always "webhook_endpoint".
"webhook_endpoint"HTTPS URL we POST events to. Validated against SSRF rules at registration AND re-validated per delivery attempt (DNS rebinding defense: every attempt re-resolves and re-checks the IP against the blocklist - no private ranges, no cloud metadata endpoints, no localhost).
"https://hooks.demo-coffee.example/vexion"
Explicit array of event names from the 22-event catalog. The wildcard ["*"] subscribes to every current AND future event - we recommend explicit names so a new event does not surprise your receiver.
What an endpoint SUBSCRIBES to: either one of the 22 catalog event names, or the wildcard * meaning every event, current and future.
This is deliberately distinct from WebhookEventName. A DELIVERED event always carries a concrete name, so * is never valid as WebhookEvent.type, WebhookDelivery.event_type, the event_type you send to POST /v1/webhook_endpoints/{id}/test, or the event_type filter on the deliveries list. It is valid ONLY inside enabled_events.
* must be sent alone: ["*", "customer.created"] returns 422 validation_failed.
customer.created, customer.updated, customer.deleted, customer.pass_installed, customer.pass_uninstalled, card.created, card.updated, card.deleted, custom_field.created, custom_field.updated, custom_field.deleted, product.created, product.updated, transaction.created, transaction.reversed, notification.sent, notification.delivery_updated, location.updated, team_member.added, team_member.updated, team_member.removed, webhook_endpoint.delivery_failed "customer.created"
enabled/disabled are merchant-toggled. auto_disabled is set by the platform after 50 consecutive failures OR a single 410 Gone response - requires explicit re-enable (dashboard or PATCH with status: "enabled"), forcing investigation before more events flow.
enabled, disabled, auto_disabled "enabled"
Consecutive FAILED delivery attempts across every event to this endpoint, reset to 0 on any successful delivery. The platform auto-disables the endpoint (status flips to auto_disabled) once this reaches 50, OR instantly on a single 410 Gone response regardless of this count. Watch this to alert before auto-disable happens rather than after.
x >= 00
Date version all event payloads to this endpoint serialize against. Set at creation, immutable - create a new endpoint to move to a newer version. Decouples your receiver upgrade schedule from your request code.
"2026-07"
Unix seconds.
1750691700
The signing secret - whsec_ + base64 key material (Standard Webhooks format). Shown only in this response and on rotation - store it now; it is never retrievable again (lost = rotate). Verify deliveries with any standard-webhooks/svix library, or manually: HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{raw body} with the base64-decoded secret.
^whsec_[A-Za-z0-9+/_-]{32}$"whsec_5f4dcc3b5aa765d61d8327deb882cf99"
Free-text merchant-facing label. Optional.
200"Loyalty events to ERP"
Unix seconds of last change.
1750691700
Until this unix time, deliveries carry BOTH signatures (old + new) space-delimited in webhook-signature - verify against either; no downtime. Null outside a rotation window.
1750778100