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

PropertyValue
MethodPOST
Content-Typeapplication/json
User-AgentListoGlobal-Gateway/1.0
Source hostgateway.listoglobal.com
TLSTLS 1.2+ required on your endpoint
Per-attempt timeout30 seconds from connect to last byte received
Success criteriaAny HTTP 2xx response body

Your endpoint should:

  • Accept POST only — return 405 for 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.

AttemptApprox. delay since previousReason
1(immediate, ~seconds after the change)First delivery
2~2 secondsRetry on non-2xx, network error, or timeout
3~4 secondsRetry 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-2xx response code (including 3xx redirects — 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-id and return 200, then replay from your own queue. Returning 5xx and 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-id is identical.
  • webhook-timestamp differs (it's the wall clock at signing).
  • webhook-signature differs (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-id is used across all retry attempts of the same event.
  • webhook-id is 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?

A 4xx is also treated as a retryable failure. Listo deliberately does not distinguish "permanent" 4xxs from "transient" 5xxs:

  • A 404 could mean "we deleted the endpoint" (permanent) or "we haven't deployed yet" (transient).
  • A 401 could 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-id and persists before responding.
  • Receiver tolerates unknown types and unknown data fields (return 200, 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