Recorded attempts support recovery, but a response loss or crash before recording can still cause a duplicate, so deduplicate by webhook-id.
A brief outage at your receiver does not cost it the notification. A retryable failure is retried with jittered backoff, starting near thirty seconds, for up to three days from the moment the platform recorded the event. A
Retry-Afterheader on a failed reply raises the next wait to at least that long, within the time left.A 4xx other than 408 or 429 is terminal, and so is a counter-signed receipt with a valid signature but a body hash that does not match. An address the platform refuses is terminal too, and nothing is sent. Correct the endpoint URL, then choose “Resend” on the failed event in the Console.
Recovery reuses recorded attempts, so a crash rarely resends what already worked. A POST your receiver got before the platform recorded it can still arrive twice, which is why
webhook-idis the field to deduplicate on.
What the platform does and what your receiver does
The platform sends each event to your endpoint as a signed HTTPS POST, retries the failures worth retrying for three days, and records every attempt. Your receiver verifies the signature, deduplicates on webhook-id, answers 2xx once it holds the message, and handles the event's finality state.
The three days run from the moment the platform recorded the event, not from the first attempt. An event that waited before its first attempt has less than three days of retrying left. Attempt and delivery times are the platform server's clock, not the time of the block that produced the event, so they can read later than the newest block on an idle network.
The signature lets your receiver establish two things without trusting the network: that the platform sent the message, and that nothing altered it in transit. It does not tell you the fact is final. Check the event's finality state and your reconciliation requirements before acting on it.
Why a notice needs its own delivery system
A Platform Event is a fact the platform has already recorded. A recorded event tells nobody outside the platform anything until somebody looks. Without a delivery system, a receiver polls the platform's read APIs and diffs the answer, or relies on a bare callback URL with no retry, backoff or record of what was sent. Both put the retry logic, the endpoint health tracking and the record of delivery on the receiver.
Webhook Delivery keeps that work on the platform side. It owns the retry schedule, the per-endpoint circuit breaker and the attempt records an operator reads back when a receiver later disputes whether it was told.
The contract is at least once, never exactly once. A crash or a lost response can still cause a resend, so the platform hands over a message identifier your receiver checks before it acts. Delivery owns getting the notice there. Deduplicating it is the one piece of the work only your receiver can do.
Receiving, verifying and answering
Each delivery reaches your receiver as one POST request with a JSON body. Three of its headers follow Standard Webhooks, an open scheme with verification libraries in most languages: webhook-id identifies the message, webhook-signature holds the signature or signatures, and webhook-timestamp gives the signing time in whole seconds. The platform adds webhook-delivery-id for the individual attempt and webhook-endpoint-id for the endpoint it addressed.
Verify with a Standard Webhooks library rather than by hand, using the endpoint's signing secret, and let the library reject a timestamp outside its tolerance. The library accepts a five-minute spread between that timestamp and your clock, so keep the receiver's clock synchronised. The platform re-signs an envelope before an attempt once its timestamp has reached four minutes old, so a message that waited in backoff does not arrive already expired. While a signing secret rotates, webhook-signature holds one signature per secret, separated by a space, and verifying against any one of them is enough.
A duplicate reaches you in two situations. The first is a retry of a delivery whose outcome the platform never confirmed. The second is a receiver that did the work but did not reply inside the twenty-second attempt window, so the platform retries a request you already fulfilled. A retry repeats the same webhook-id, while webhook-delivery-id changes on every attempt. Record the identifiers you have handled and return success for one you recognise. Store the handled identifier and the resulting business change atomically, or use an equivalent idempotent design.
A replayed event is the exception. Replay gives each event a new webhook-id and marks it as a replay, so deduplicate a replay on the identifiers inside its payload.
When an attempt fails, the platform records what kind of failure it hit:
Sending the identical request again would reproduce the identical refusal, so the platform does not retry a terminal failure. The failure kind tells an operator whose problem it is, and whether the platform is still trying, before anyone opens a log.
An endpoint can require counter-signed receipts. A 2xx reply only shows that the platform reached you and you answered. A counter-signed receipt goes further: your receiver signs the delivery identifiers and a hash of the exact body with the endpoint's signing secret. The dalp.whr.v1 scheme ties the receipt to that delivery and payload. The receipt contract defines the signing string, request fields and deadline. A missing receipt causes a retry. A receipt whose signature does not verify is refused without changing the delivery.
Registering an endpoint
An organisation Owner or a Platform administrator registers and manages endpoints on the Console's Webhooks page, which also shows each endpoint's breaker state and every event's attempts. The signing secret appears once, when the endpoint is created or its secret is rolled. Rolling keeps the previous secret valid for 24 hours, and the platform signs each delivery with both secrets during that overlap.
Thin and fat payloads
A thin payload leaves out personal data, and every endpoint starts thin. A fat payload includes the personally identifying fields that the endpoint's subscriptions expose. The platform computes that list of fields from the subscriptions and refuses the switch to fat until the endpoint owner acknowledges every field on it. An endpoint cannot be created fat: register it thin, then move it to fat once the fields it would expose are known.
Address checks
Registering or updating an endpoint checks the address first. The URL must use HTTPS and resolve only to a public address, so a private, loopback or link-local target is refused before it is saved. Register the address your receiver is reachable at from the public internet.
At send time the platform resolves the endpoint's host, validates the address it got back and connects to that address directly, so no second name lookup can redirect the request after the check passed. Where a deployment allows specific internal addresses, those addresses lose this protection. The original host name still travels in the Host header and the security handshake, so virtual hosting and certificate checks behave normally.
The platform follows no redirects. A redirect reply is recorded as an invalid response and retried. Register the address the redirect points to. The platform reads at most eight kilobytes of a reply and stores that on the attempt record.
Recovery reuses recorded attempts
Sending a webhook and recording its outcome are separate effects. A process can stop after your receiver got the POST but before the attempt record is written, and recovery may repeat that send.
On recovery, the platform uses the endpoint, event and attempt number to look for an existing delivery record. When the record exists, recovery reuses its outcome. When it does not, recovery sends the request. This reduces repeated sends but does not deliver exactly once.
Once retrying stops, an Owner or Platform administrator can still send the event again. “Resend” on a failed event adds one attempt to the same event with the same webhook-id, and only the latest attempt of an event can be resent. “Replay events” sends events the platform already recorded for the endpoint, by block range or one at a time, each with a new webhook-id. The platform refuses a replay while the endpoint is switched off or its circuit breaker is open.
The circuit breaker that watches endpoint health
Webhook Delivery gives every registered endpoint its own circuit breaker, with three states: closed, open and half-open. It keeps one failing endpoint from consuming delivery capacity meant for every other receiver, and it lets recovery happen without an operator noticing and re-enabling the endpoint.
Thirty consecutive failed deliveries open the breaker, which pauses live sends to that endpoint. The platform then probes the endpoint at most once a minute. Two successful probes in a row move the breaker to half-open, and live deliveries resume. One further successful delivery closes the breaker and resets the failure count to zero, while one failed live delivery sends it back to open. The platform records each state change as it happens.
The platform probes an open circuit breaker, and successful probes admit live traffic through half-open before normal delivery resumes.
Test events stay out of that accounting. An operator can send a test event to a disabled or recovering endpoint. It moves neither the breaker nor the failure count, and it neither replaces nor speeds up the automatic probes.
Evidence that the notice went out
Webhook Delivery records every attempt it makes toward your receiver, not only the one that finally succeeds. Every attempt leaves a row with the attempt number, the status your receiver returned, its response headers and the first part of its body. A failed attempt's row also records the failure kind and the trace identifier that ties it to the wider operation. The platform keeps these records for 30 days after a delivery settles, so export what your dispute and audit policy needs before then.
Delivery also adds an entry to the event's chain of hop hashes, with a hash of the exact payload sent and the endpoint it was signed for. That records which content went out to which endpoint, which matters when a receiver disputes what it received.
The delivery signature and the counter-signed receipt both use a secret that the platform and your receiver share. Each side can check the other, but neither signature proves to a third party which side produced it.
When retrying stops, the platform writes a warning in its logs and marks the operation failed in its traces. Both name the attempt number, the failure kind and the event's age, so an operations team finds abandoned events there.
The platform versions its signing secrets. A successful delivery updates the last-used time of each secret version that signed it, and the endpoint's secret.lastUsedAt reports the most recent of those times.
Frequently asked questions
Can a private-network deployment point a webhook at an internal address?
Not from the endpoint form. The public-address check applies to every organisation the same way. A private-network installation can allow specific internal destinations at the deployment level. The deployment operator makes that decision for the whole installation, and an organisation cannot make it by editing its own endpoint URL. If a legitimate internal address keeps being refused, ask the operator about that allowance.
What happens to deliveries already in flight when I change an endpoint's URL?
Nothing in flight retargets silently. If the endpoint still has pending deliveries, a plain URL change is refused. You either wait for those deliveries to finish, or resend the update with an explicit acknowledgement that pending attempts retry against the new address. A completed attempt keeps its original destination. Each attempt reads the endpoint's current URL at send time, so a delivery still retrying when you switch lands on the new address from its next attempt onward.