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"
}
}
| Field | Type | Notes |
|---|---|---|
event | string | The dotted event name. Branch on this. |
timestamp | string | ISO-8601 UTC, stamped when the delivery was built — not when the underlying change happened. |
account_id | integer | The account the event belongs to. |
location_id | string | A numeric string — "279381", not 279381. Not coerced on interaction.*; see the exception below. |
data | object | Event-specific fields. Documented per event in the event reference. |
agency_account_id | integer | Only present when the account is managed by a parent agency account. |
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 more than once. De-duplicate on the stable
identifiers inside
data(an interaction id, a post id, a connected-account id) combined withevent. Handlers should be idempotent. - Ordering is not guaranteed. Events are produced by several independent
rails and delivered as they arrive. Do not infer a sequence from arrival
order; use the
timestampand the IDs indata, and be prepared for alocal_post.publishedto land before thelocal_post.createdyou expected first. profile.updatedis debounced to one delivery per location per 60 seconds. This is a real collapse, not a delay: edits inside the window are never delivered, andchanged_fieldsdescribes only the edit that opened the window. If you need the full current state, read the location back from the REST API when the event arrives. Nothing else is debounced.
Delivery outcomes
Every attempt — including one that never leaves the building — is recorded with a status. These are the exact strings.
| Status | Outcome | What it means | What to do |
|---|---|---|---|
success | Delivered | Your endpoint returned a 2xx. | Nothing. |
an HTTP code as a string — e.g. "404", "500" | Failed | Your endpoint answered with that non-2xx code. 3xx lands here too, because redirects are not followed. | Check the path, the redirect chain, and your handler's error rate. |
999 | Failed | A response came back with no usable status code. | Usually a proxy or load balancer in front of your endpoint. |
timeout | Failed | No response within the 5-second budget. | Move work off the request path and acknowledge sooner. |
network_error | Failed | The host could not be resolved or reached. | Check DNS and that the host is publicly resolvable. |
error | Failed | The attempt raised something that isn't one of the above — a TLS handshake failure, for instance. | Check your certificate chain and server logs. |
blocked_url | Blocked | The URL failed its safety check at dial time: not HTTPS, no host, or it resolved to a private, loopback, link-local or metadata address. | Point the URL at a public HTTPS host. Note this is re-checked on every delivery, so a DNS change can start blocking a URL that used to work. |
endpoint_unverified | Blocked | The account has a signing secret, but the endpoint has not passed verification — or verification was reset. | Re-run Verify endpoint from the dashboard. |
rate_limited | Rate limited | The per-minute or per-day cap was exceeded. The event was dropped. | Reduce volume or move to a plan with a higher cap. See plans and limits. |
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 and key them on the IDs inside
data. - Don't infer ordering from arrival order.
- 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 — no redirects.
- Reconcile periodically against the REST API; there is no retry to save you.