Skip to main content
A webhook endpoint is an HTTPS URL of yours that Vainona POSTs events to: a document entering or leaving a subscription, a job finishing, judging pausing at a budget. This page covers what your endpoint receives, how to check it came from us, and what happens when your endpoint is slow or down. Every event is also in the events feed, for receivers that would rather pull.

Set up an endpoint

Create an endpoint with its URL and the platform events it should receive, as patterns such as job.* 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_write key that manages the endpoint can read it again with a get. Lists and PATCH responses never show it, and we keep it encrypted.
  • namespace_prefix limits the endpoint to events of namespaces under it, such as acme/staging/ for a staging receiver. It takes a key scope’s forms: *, a prefix such as acme/staging/, acme/*, or one namespace. It defaults to your key’s scope, and one outside it is forbidden. A key scoped to a prefix sees and manages only the endpoints inside its scope.
  • events are platform event types or patterns, such as job.*. A pattern that matches only subscription events or webhook.test is refused: a subscription names its endpoint itself.
  • description is up to 256 characters, and max_per_second caps the delivery rate (see retries).
  • A get shows the endpoint’s status (active, failing or disabled), failing_since, previous_secret_expires_at during a rotation, and stats: deliveries_24h, the deliveries whose latest attempt was in the last day, success_rate_24h, and last_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-Key returns the endpoint the first request made, with its secret, so a retried create is safe.
  • A test send delivers webhook.test, or with type a 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, whose details name 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-id is the event’s id. Every retry, redelivery and replay of an event repeats it, so use it to drop duplicates.
  • webhook-timestamp is when this attempt was signed, in Unix seconds.
  • webhook-signature is v1, and a base64 HMAC-SHA256. During a secret rotation it holds two, separated by a space.
  • user-agent names us and links to this page.
  • The body is at most 64 KB. When an event would be larger, its attributes and answers are left out, truncated is true, and url fetches 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.
A request that fails a check raises 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:
  1. Read the body as raw bytes, before any JSON parsing.
  2. Refuse a webhook-timestamp more than 5 minutes from your clock, so a captured request can’t be replayed later.
  3. Compute the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, keyed by the secret’s bytes after whsec_, base64-decoded.
  4. Accept the request if the result equals any v1, entry in webhook-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 for previous_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 200 before 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.
We keep the first 1 KB of your response in the endpoint’s delivery log, with the status code, the latency and, on a failure, why it failed (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 by status (pending, succeeded, failed or skipped), shows each delivery with its event’s id and type, its number of attempts, next_attempt_at while 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}/redeliver with the endpoint queues a new delivery of that event, with the same webhook-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}/recover with since queues again every failed or skipped delivery 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.
Slowing us down. Answer 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 was failing first, and they get an email. Nothing more is sent to it, and its pending deliveries become skipped. 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 with PATCH /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 be https://, 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.