Delivery behavior
The payload envelope, the 5-second budget, why there are no retries, and what every delivery status means.
Webhook delivery is deliberately simple, and simple in a way that has consequences for your receiver: one HTTP attempt, five seconds, no retry. Read this page before you decide how much work your handler does inline.
The envelope
Every event except listing.submission arrives in the same top-level shape,
with the event-specific fields nested under data.
{ "event": "connection.location_connected", "timestamp": "2026-08-25T14:32:10Z", "account_id": 11073, "location_id": "279381", "data": { "platform": "google", "connected_account_id": "ba03bc4b-9f8c-4d3b-8e93-9628034c63cc" }}Optional fields inside data are omitted when empty rather than sent as
null. Code defensively: treat a missing key and an explicit null the same
way, and never assume a key that is documented as optional will be present.
Two exceptions
The 5-second budget
Each delivery is a single POST with a 5-second connect timeout and a
5-second read timeout. If your endpoint has not responded in time, the
attempt is abandoned and recorded as timeout.
That budget covers your entire handler. The only safe design is:
- Verify the signature.
- Write the payload somewhere durable: a queue, a table, a log.
- Return
200.
Everything else (enrichment, fan-out, calling back into the REST API, anything that touches a third party) belongs on the other side of that queue.
There are no retries
A delivery is attempted exactly once. There is no backoff, no retry queue, and no dead-letter queue. If your endpoint is down for two minutes, the events that fired in those two minutes are gone. They will not arrive later.
Two consequences worth being explicit about:
- Redirects are not followed. Redirects are disabled outright, so a
301or302from your endpoint is recorded as a failed delivery, not followed to a second URL. Configure your webhooks URL as the final URL. Watch for the bare-domain-to-wwwand the trailing-slash redirects that many web servers add by default. - Only a
2xxcounts as delivered.3xx,4xxand5xxare all recorded as failures.
Duplicates and ordering
- You may receive the same event twice, and the same id for two different
changes. The identifiers inside
data(an interaction id, a post item id, a connected-account id) name the resource, not the change.interaction.reviewre-fires with the same interaction id every time the review changes, and no payload carries a version or an updated-at field, so a handler that permanently suppresses on id +eventsilently throws real edits away. Detect exact duplicates instead: hash the raw body bytes and ignore a repeat of the same hash inside a short bounded window (minutes, not forever). A different body for the same id is a new change, not a duplicate. Then read the resource back from the REST API and treat that read as authoritative; make the write idempotent on the resource id rather than skipping it. - Ordering is not guaranteed, and
timestampwill not fix it. Events come from several independent rails and are delivered as they arrive, so alocal_post.publishedcan land before thelocal_post.createdyou expected first. Do not order two changes to the same resource bytimestamp: it is stamped when the delivery is built, not when the change happened, and there is no version or sequence number anywhere in the payload. When order matters, read the resource back and use what the API returns. profile.updatedis debounced to one delivery per location per 60 seconds, on the leading edge. This is a real collapse, not a delay: the first edit opens the window and is delivered, every edit inside the window is dropped and never arrives, andchanged_fieldsdescribes only the edit that opened the window. The event therefore reaches you at the start of the window, so a read-back triggered by its arrival can only ever see that first edit. Schedule a second read-back after the window closes — at least 60 seconds after the event — and reconcile locations on a timer as well, because the notification itself can be lost: one attempt, no retry. Nothing else is debounced.
Recovering from a missed event
There is no retry and no replay, so every family needs an authoritative read you can fall back on. Reconcile on a schedule, not only when something looks wrong.
Delivery outcomes
Every attempt, including one that never leaves the building, is recorded with a status. These are the exact strings.
A receiver checklist
- Verify the signature on every request; return
401and process nothing on a mismatch. - Hash the raw body bytes, never a re-serialized object.
- Return
2xxin well under 5 seconds; queue the work. - Make handlers idempotent: key the write on the IDs inside
data, but never treat a repeated id as nothing to do. - Don't infer ordering from arrival order, and don't order by
timestamp. - Normalize
location_idto a string on the way in. - Ignore
eventvalues you don't recognize, and still return200. - Serve the webhooks URL as a final URL, with no redirects.
- Reconcile periodically against the REST API; there is no retry to save you.