Set up an endpoint
Create an endpoint with its URL and the platform events it should receive, as patterns such asjob.* or namespace.budget_paused. Subscriptions name their endpoint themselves, so an endpoint with no events receives only what its subscriptions send it.
- The secret comes back when you create the endpoint and when you rotate it, and a
read_writekey that manages the endpoint can read it again with a get. Lists andPATCHresponses never show it, and we keep it encrypted. namespace_prefixlimits the endpoint to events of namespaces under it, such asacme/staging/for a staging receiver. It takes a key scope’s forms:*, a prefix such asacme/staging/,acme/*, or one namespace. It defaults to your key’s scope, and one outside it isforbidden. A key scoped to a prefix sees and manages only the endpoints inside its scope.eventsare platform event types or patterns, such asjob.*. A pattern that matches only subscription events orwebhook.testis refused: a subscription names its endpoint itself.descriptionis up to 256 characters, andmax_per_secondcaps the delivery rate (see retries).- A get shows the endpoint’s
status(active,failingordisabled),failing_since,previous_secret_expires_atduring a rotation, andstats:deliveries_24h, the deliveries whose latest attempt was in the last day,success_rate_24h, andlast_delivery_at. - Deleting an endpoint removes its pending deliveries and its log. Subscriptions that name it keep sending their events to the events feed only.
- Creating an endpoint again with the same
Idempotency-Keyreturns the endpoint the first request made, with its secret, so a retried create is safe. - A test send delivers
webhook.test, or withtypea sample of that event type, to this endpoint only. Both carry"test": true, and the feed never lists them. - How many. Developer has 2 endpoints, Team 20 and Scale 100 (pricing). One more is refused with
plan_required, whosedetailsname the plan that allows more. After a downgrade, endpoints over the new limit keep delivering. Deliveries are never billed. unavailable(503): a create or a rotation can’t make a secret right now. Nothing was changed; retry it.
In the dashboard
Everything on this page can also be done in the dashboard. Admins and owners make changes; every member can look.- Webhooks lists your endpoints with their status, namespaces, last delivery and the last day’s deliveries, and how many your plan allows. Creating an endpoint shows its secret once, with a copy button: copy it then, because the dashboard never shows it again. At your plan’s limit, the page offers the plan with more.
- An endpoint’s page rotates the secret (choosing how long the old one keeps signing), sends a test event of any type, disables and enables the endpoint, and deletes it. Its delivery log shows each delivery’s status, attempts, response code, latency and next retry, filtered by status, with Redeliver on each row. Recover since… queues again everything that failed or was skipped since a time.
- A namespace’s Subscriptions tab lists its subscriptions and creates them. On the Documents tab, Subscribe to this filter opens the create form with the query’s filter filled in, or says why the filter can’t be subscribed to. A template’s page lists and creates the template’s subscriptions.
- Events is the feed, filtered by type and namespace prefix and paged oldest first, and opens each event’s JSON and its deliveries.
What your endpoint receives
Each delivery is one event as JSON, signed with your endpoint’s secret. The headers follow the Standard Webhooks spec:webhook-idis the event’s id. Every retry, redelivery and replay of an event repeats it, so use it to drop duplicates.webhook-timestampis when this attempt was signed, in Unix seconds.webhook-signatureisv1,and a base64 HMAC-SHA256. During a secret rotation it holds two, separated by a space.user-agentnames us and links to this page.- The body is at most 64 KB. When an event would be larger, its
attributesandanswersare left out,truncatedistrue, andurlfetches the rest with your own key.
Verify the signature
Check every delivery before you trust it. The SDKs do it in one call:verifyWebhook(payload, headers, secret) in TypeScript and verify_webhook(payload, headers, secret) in Python check the signature and the timestamp, accept either signature during a rotation, compare in constant time, and return the event, typed by its type. They need nothing beyond the SDK in Python and WebCrypto in TypeScript, so they also run on edge runtimes.
WebhookVerificationError, whose reason is headers, timestamp or signature: answer it with 400. secret can also be a list, and the timestamp’s tolerance can be changed (tolerance in Python, toleranceSeconds in TypeScript). A malformed secret is a bug on your side, not a bad request, so it raises an ordinary error rather than WebhookVerificationError. SDKs has Express and FastAPI versions, and how to narrow the event on its type.
Without the SDK. The check is the Standard Webhooks one, so any Standard Webhooks library verifies our deliveries too. To write it yourself:
- Read the body as raw bytes, before any JSON parsing.
- Refuse a
webhook-timestampmore than 5 minutes from your clock, so a captured request can’t be replayed later. - Compute the base64 HMAC-SHA256 of
{webhook-id}.{webhook-timestamp}.{body}, keyed by the secret’s bytes afterwhsec_, base64-decoded. - Accept the request if the result equals any
v1,entry inwebhook-signature, which holds one entry per signing secret, separated by spaces. Compare in constant time.
Rotating the secret
Rotating an endpoint’s secret returns the new one, and the old one keeps signing alongside it forprevious_valid_for: 24 hours unless you choose otherwise, at most 7 days, and 0s to end it at once. While both are valid every delivery carries both signatures, so deploy the new secret to your receiver at any point in that window and nothing is refused. At most two secrets sign at once: rotating again inside the window ends the older one at once.
Answer 2xx, fast
A delivery succeeds when your endpoint answers any 2xx within 15 seconds, including at most 5 seconds to connect. Anything else is a failure and is retried: a timeout, a refused connection, a TLS error, and any other status, 3xx included, because redirects aren’t followed.- Queue the work, then answer. Store the event (or push it onto your own queue) and return
200before doing anything slow. A receiver that does its work inline is the usual cause of timeouts and duplicate deliveries. - Deduplicate on
webhook-id. Delivery is at least once. A 2xx whose response is lost, for example to a timeout, is sent again. - Don’t rely on order. A retried event can arrive after a later one. Subscription events carry
sequence, which grows with each change in the namespace: for one subscription and document, keep the highest you’ve seen.
timeout, dns, tls, connect, blocked_address or http_status).
Retries
A failed delivery is retried on this schedule, each wait moved by up to 20% either way so retries from one outage don’t arrive together:
That’s 9 attempts over about 2.8 days. After the ninth the delivery is
failed. It stays in the delivery log for 30 days, and you can send it again from the dashboard or the API:
- The delivery log.
GET /webhook-endpoints/{id}/deliveries, newest first, 100 to a page, and filtered bystatus(pending,succeeded,failedorskipped), shows each delivery with its event’s id and type, its number ofattempts,next_attempt_atwhile it waits for a retry, and its latest attempt’s time, status code, latency, error and first 1 KB of response. Only the latest attempt of each delivery is kept. - Redeliver one.
POST /events/{id}/redeliverwith theendpointqueues a new delivery of that event, with the samewebhook-id, whatever became of the earlier ones. The endpoint must cover the event’s namespace, and a test send goes again only to the endpoint it tested. - Recover since a time.
POST /webhook-endpoints/{id}/recoverwithsincequeues again everyfailedorskippeddelivery to the endpoint since then, at most 30 days back, each with its full retry schedule, and says how many it queued. Each is reset in place, so its earlier attempts leave the log. Calling it again queues nothing twice. - A disabled endpoint refuses both with
conflict: enable it first.
429 or 503 with a Retry-After (seconds, or an HTTP date) and we pause all deliveries to that endpoint for that long, up to 5 minutes, and wait at least that long before retrying the delivery, up to its next step in the schedule. Without Retry-After the pause is 5 seconds. We never have more than 16 deliveries in flight to one endpoint, and an endpoint can set max_per_second to cap its rate: any positive number, such as 0.5 for one delivery every 2 seconds, or null to remove it.
Failing and disabled endpoints
Both are counted from the first failed attempt after the endpoint’s last success, and checked at each attempt.- Failing. When nothing has reached an endpoint for 24 hours and at least 10 attempts have failed, it is marked
failing, and your organization’s owners and admins get an email. Deliveries and retries continue. - Disabled. After 5 days with no successful delivery it is
disabled, whether or not it wasfailingfirst, and they get an email. Nothing more is sent to it, and its pending deliveries becomeskipped. New events are still recorded, and the events feed lists them, so nothing is lost. - Active again. Any successful delivery returns a failing endpoint to
active. For a disabled one, fix the receiver, re-enable it withPATCH /webhook-endpoints/{id}and{"enabled": true}, which also clears its failure count, then recover what it missed since it started failing. You can disable an endpoint yourself with{"enabled": false}.
Our egress IP
Every delivery comes from a fixed address, so you can allow-list it in a firewall: It is published as a list so that adding an address, for a new region, isn’t a breaking change: allow every address listed here.HTTPS only
Endpoint URLs must behttps://, with a certificate a public CA issued, and no user or password in the URL. Before every new connection we resolve the host and refuse to send (blocked_address, which counts as a failure) when any of its addresses isn’t public: loopback, RFC 1918, carrier-grade NAT, link-local, multicast, the other reserved ranges, their IPv6 equivalents, and IPv6 addresses that embed a private IPv4 one. We then connect to the address we checked, so an endpoint can’t point us at your internal network, or ours, through DNS.
Pull instead of push
Every event is also in the events feed,GET /events, in the order it was recorded, for 30 days. A receiver that was down can read the feed from the last event it handled instead of waiting for retries: an event’s id, which is the webhook-id of its deliveries, works as a cursor. A receiver that can’t accept inbound requests can poll the feed and need no endpoint at all.