Skip to Content
WebhooksDelivery and retries

Delivery and retries

At-least-once

Deduplicate on webhook-id. A POST that succeeded on your side but timed out on ours is retried, so the same event can arrive twice. The id stays identical across every retry of one event, while the timestamp and signature are fresh each attempt.

Exactly-once delivery is not a thing anybody offers over HTTP, because the network cannot tell “your server never got it” from “your server got it and the acknowledgement was lost”. At-least-once plus an idempotency key is the honest version, and the key is already in the payload.

What counts as success

Any 2xx. Answer as soon as you have stored the event; do the work afterwards.

Sailo waits 5 seconds. An endpoint that does its processing inline and takes six is an endpoint that gets retried while it is still working — which produces duplicate work, not lost work, but produces it every time.

What counts as a failure

  • Any non-2xx status.
  • A timeout past 5 seconds.
  • A connection that cannot be made.
  • A redirect. A 3xx counts as a failure, because Sailo does not follow them. Register the final URL.

Redirects are not followed deliberately rather than as an omission. The URL is checked for a public, non-loopback address before the request is made, and a redirect is a way to send that request somewhere else after every check has passed.

The schedule

6 attempts in total: the first, then retries after 1m, 5m, 30m, 2h, 12h. Then the delivery is abandoned.

Roughly 15 hours end to end, which is the shape every webhook system converges on: dense enough at the start to ride out a deploy or a restart, spread out enough at the end to survive an outage that lasts a working day, and finite because an event nobody has accepted by tomorrow is not one their CRM still wants.

There is no manual replay. If you lose events, backfill from the REST API — orders, products and contacts are all readable, and the pagination page has the pattern.

Endpoints that keep failing

After 20 consecutive failures the endpoint is switched off and the seller is emailed.

Consecutive, so a single bad deploy does not count against an endpoint that recovers. Switched off rather than throttled, because an endpoint that has rejected twenty deliveries in a row is not coming back on its own, and the alternative is Sailo posting into a void for weeks while a seller believes their integration is running.

Re-enable it in Settings → Integrations once it is fixed. Events that were abandoned while it was off are not replayed.

Ordering

Events are not ordered. Two events about the same object can arrive out of order, especially when the first one needed a retry and the second did not.

Order on timestamp in the envelope — it is when the event happened, not when the attempt was made — or, better, write your handler so order does not matter:

// Fragile: assumes shipped arrives after paid. if (event.type === "order.shipped") markPaidAndShipped(order); // Robust: the payload carries the whole state. upsertOrder(event.data);

The payload is the complete object, not a diff, so an upsert keyed on data.id converges regardless of arrival order. Where it genuinely matters — membership access — compare timestamp against what you have stored and ignore the older one.

Concurrency

Several deliveries to one endpoint may be in flight at once. Do not assume your handler is serialised, and make it safe to run twice concurrently for the same webhook-id — a unique constraint on that column is the simplest way.

Requirements on your endpoint

Schemehttps only
HostPublicly resolvable. No loopback, no RFC1918, no link-local, no single-label names.
RedirectsNot followed. Register the final URL.
ResponseAny 2xx, within 5 seconds
BodyIgnored. Sailo reads at most a few KB of it for the error log and discards the rest.
User-AgentSailo-Webhooks/1.0 (+https://sailo.store)

Basic auth in the URL — https://user:pass@host/hook — is accepted, since some self-hosted consumers express a shared secret that way.

Developing locally

localhost is refused, so use a tunnel — cloudflared tunnel, ngrok, or whatever your platform gives you — and register the public https URL it hands you.

The Send test button is the fast loop: it delivers a complete payload with "test": true and every field a real one carries, without waiting for a real order.

Monitoring

Watch two things:

Your 2xx rate. Sailo emails the seller after 20 consecutive failures, but that is a last resort, not an alert — by then you have lost 20 events’ worth of promptness.

Your handler’s duration. The 5-second timeout is generous for storing a row and tight for anything else. If your p99 is creeping toward it, the fix is to move work off the request rather than to make the request faster.

Last updated on