Delivery, Retries, and Idempotency
Listo's webhook delivery is at-least-once. The contract that follows is the only guarantee — design your handler around it and you will not be surprised in production.
Transport
| Property | Value |
|---|---|
| Method | POST |
| Content-Type | application/json |
| User-Agent | ListoGlobal-Gateway/1.0 |
| Source host | gateway.listoglobal.com |
| TLS | TLS 1.2+ required on your endpoint |
| Per-attempt timeout | 30 seconds from connect to last byte received |
| Success criteria | Any HTTP 2xx response body |
Your endpoint should:
- Accept
POSTonly — return405for everything else. - Respond within 30s. Respond first, do work after.
- Return a small JSON body (e.g.
{"ok": true}) — Listo logs the first ~1 KB of every response for audit, and a small body keeps that log useful.
Retry schedule
Each event is delivered to your subscription up to 3 times total — 1 initial attempt + 2 retries. Backoff is exponential starting at 2 seconds.
| Attempt | Approx. delay since previous | Reason |
|---|---|---|
| 1 | (immediate, ~seconds after the change) | First delivery |
| 2 | ~2 seconds | Retry on non-2xx, network error, or timeout |
| 3 | ~4 seconds | Retry on non-2xx, network error, or timeout |
| — | ~8 seconds | (Final post-attempt-3 wait — no further delivery happens) |
A retry is triggered by any of:
- A non-
2xxresponse code (including3xxredirects — Listo does not follow them). - A connection error (DNS failure, TCP reset, TLS handshake error).
- A read timeout after 30s.
After attempt 3 fails, the event is marked permanently failed for that subscriber. Listo does not deliver again. Recovery is your responsibility:
- Listo records every attempt's status, response code, and (truncated) response body in the dashboard's Event Logs view. Customers and the Listo team can both inspect that audit trail.
- A Listo engineer can manually re-trigger delivery of a specific event for a specific subscription on request, but there is no self-service replay endpoint today.
🛈 Plan for the failure mode. If your handler can't process an event right now (downstream is down, you OOM'd, etc.), still persist the raw envelope keyed by
webhook-idand return200, then replay from your own queue. Returning5xxand hoping Listo retries past attempt 3 is a guaranteed data-loss path.
Per-retry signing
Each attempt is signed afresh with a new webhook-timestamp. The webhook-id is the event's id and is stable across retries — that's what makes it a usable dedupe key.
Concretely, between attempt 1 and attempt 3 of the same event:
webhook-idis identical.webhook-timestampdiffers (it's the wall clock at signing).webhook-signaturediffers (because the timestamp differs).- Body bytes are identical.
The 5-minute replay window in your verifier compares against the fresh timestamp on each attempt, so retries always pass the freshness check even though the event itself may be older than 5 minutes by then.
Ordering
Order is not guaranteed. Two events for the same entity may arrive out of occurredAt order. Causes include:
- Independent retry timing for two events that were emitted close together.
- Different network paths between Listo and your endpoint per attempt.
- Late retries interleaving with newer events.
Use occurredAt (the producer's wall clock at emission, ISO-8601 UTC) to order events on your side if order matters. Never rely on arrival order for correctness.
If you depend on the latest state of an entity, prefer to refetch from the public API via data.links.self rather than diffing snapshots from ordered events — it's simpler and side-steps the ordering question entirely.
Idempotency
webhook-id (= the body's top-level id) is the canonical idempotency key. Listo guarantees:
- The same
webhook-idis used across all retry attempts of the same event. webhook-idis globally unique — the same value will not be reused for a different event.
Recommended handler shape:
1. Verify signature (against raw body) and timestamp window.
2. SELECT processed_at FROM webhook_deliveries WHERE id = :webhook_id.
3. If row exists: respond 200 immediately, do nothing else.
4. INSERT INTO webhook_deliveries (id, type, occurred_at, body, processed_at=NULL).
5. Respond 200.
6. Process the event asynchronously from your own queue, then UPDATE
processed_at when done.This pattern survives every failure mode:
- Duplicate delivery → caught at step 2.
- Crash between steps 4 and 5 → next retry replays steps 2–5 cleanly because step 4 was idempotent on the unique
id. - Crash during async processing → your own queue retries; Listo doesn't need to re-deliver.
Listo itself relies on the same key on the producer side — if a single event is published twice into Listo's internal queue (gateway POST retry, job replay, etc.), the second copy is dropped server-side and never fans out to your subscription.
What about 4xx responses?
4xx responses?A 4xx is also treated as a retryable failure. Listo deliberately does not distinguish "permanent" 4xxs from "transient" 5xxs:
- A
404could mean "we deleted the endpoint" (permanent) or "we haven't deployed yet" (transient). - A
401could mean "wrong key" (permanent) or "the secret manager is down" (transient).
Three retries are a reasonable bound for transient failures, and there is no public way today to ask Listo to give up earlier on a single delivery. If you need to stop a specific URL from receiving events entirely, ask Listo to pause or delete the subscription — see Updating or pausing a subscription.
Operational checklist
Before going live, confirm:
- HTTPS only. TLS 1.2+. Certificate is publicly trusted.
- Endpoint hostname is on your allowed domains list with Listo.
- Receiver verifies signature against the raw bytes, not parsed JSON.
- Receiver enforces the 5-minute timestamp window (default in
standardwebhooks; do not disable). - Receiver dedupes on
webhook-idand persists before responding. - Receiver tolerates unknown
types and unknowndatafields (return200, log). - Receiver responds within 30s; long work runs async on your side.
- Monitoring/alerting on 4xx/5xx response rates from your endpoint — after 3 failures Listo stops retrying that event.
- Runbook for webhook signing key rotation (regeneration is no-overlap; coordinate a brief receiver flip).
- API key and webhook signing key both stored in a secret manager, not in source.
Where to next
Updated 1 day ago
