Delivery
Retries, idempotency, timeouts, and how to catch up after an outage.
Retries
On 5xx, 429, a timeout, or a connection failure: exponential
backoff across roughly 24 hours.
A suggested schedule of delays between attempts:
1m → 5m → 30m → 2h → 6h → 15h six attempts, ≈24h totalA quick first retry covers a momentary blip; the long tail covers a real outage. The shape matters more than the exact numbers — if your platform has a standard retry policy, use it and tell us the window.
Do not retry 400 or 401. Those will fail identically on every
attempt; see Errors.
Sign every attempt at the moment you send it
A retry carries the same body and the same eventId, but a new
t and therefore a new signature. Do not store the original request
and replay it as-is: its t is outside the five-minute window by the
time the second attempt goes out, so it is rejected with 400 — and
under the rule above, never tried again. The signature is a property of
the attempt, not of the event.
Idempotency
Every retry must carry the eventId of the original attempt, not a
fresh one.
We store events keyed on that id with a unique constraint, so a
duplicate delivery returns 202 and does nothing else. Retrying is
free for you and safe for us. A new id per attempt turns a retry into a
second event, which is the one way to make at-least-once delivery
produce duplicate work.
Timeouts
Please allow a 10-second read timeout.
We expect to answer in well under a second — the work is verify, insert, respond — but a cold start under load should not be read as a failure and trigger a retry that was never needed.
Catching up after a gap
Retries stop after about a day, and a 401 is never retried. Both
leave a permanent hole if something was wrong on either side for long
enough.
So we would like a way to ask for events we missed. Either shape works:
A) GET /getEventsSince?cursor=<eventId|timestamp> paged
B) an operator action that re-sends a given time window(A) is considerably better — it lets us recover automatically, without involving anyone at your end. (B) still beats nothing, because it turns a silent gap into a support request rather than a permanent one.
Why this is worth building
Without a catch-up path, every gap has to be closed by falling back to the polling this integration exists to replace — which means we can never fully retire it, and the integration keeps a manual recovery step forever.
With one, the webhook becomes genuinely self-healing: we notice a gap, ask for the range, and catch up. It is the difference between a delivery mechanism and a reliable one.
If neither is feasible, tell us. We will keep a reconciliation sweep
running against /getLoadsByList permanently instead, and size its
cadence to how much loss we are willing to carry.
Ordering
We do not ask for ordering guarantees, and we would rather you did not hold events back to deliver them in sequence. Promptness is worth more to us, and in this version there is one event per load anyway.
occurredAt is still required on every event. We use it to guard
against a stale delivery overwriting fresher data — which costs you
nothing now, and is exactly what we would need on the day update events
arrive.
Batching
None, please. One event per request.
A batch endpoint means partial failures, and "which of these fifty did you accept" becomes a protocol we would both have to invent and then maintain.