Sellvik / developers
Webhooks

Delivery and retries

At-least-once semantics, retry schedule, and what 4xx/5xx do.

Delivery guarantees

  • At least once. The same event may be delivered more than once if your endpoint times out, returns a non-2xx, or briefly refuses connections. Use the event.id to deduplicate. See Idempotency.
  • In-order is not guaranteed. Events for the same resource may arrive out of order during retries. Use createdAt to sort and the resource state (e.g. order.status) to reconcile.
  • At-least-once, not exactly-once. Even after a 200, a duplicate may arrive if our acknowledgement was lost. Always deduplicate.

What counts as "delivered"

A delivery is considered successful when:

  • The HTTP response has status 2xx (200, 201, 202, 204 all work).
  • The response was received within 10 seconds of the request being sent.
  • The TLS handshake completed.

Anything else (timeout, connection refused, non-2xx, redirect, 1xx) is a failure and triggers the retry chain.

We follow up to two HTTP redirects (301, 302, 307, 308). After that the delivery fails.

Retry schedule

Exponential backoff with jitter, capped at 24 hours total:

AttemptTime after original event
1immediately
2~30 seconds
3~2 minutes
4~10 minutes
5~30 minutes
6~2 hours
7~6 hours
8~18 hours
9~24 hours (last attempt)

After the 9th failed attempt the delivery is marked dropped and a webhook.failed meta-event fires (if any other webhook subscribes to it).

We do not retry beyond 24 hours. If your endpoint is down for a day, those events are lost. For backfills, you can query the source resource via the admin API.

What status codes do

ResponseWhat we do
2xxSuccess. Stop retrying.
4xx (except 408, 429)Treat as permanent failure. Retry once after ~30s, then drop. The thinking: 4xx means the client (your endpoint) is rejecting the payload structurally, and retrying the same payload won't change that. The one retry catches the rare case where you just deployed a fix.
408 Request TimeoutFull retry chain.
429 Too Many RequestsFull retry chain, honouring Retry-After.
5xxFull retry chain.
No response (timeout, connection reset)Full retry chain.
Redirect (3xx)Follow up to 2 hops, then fail.

Respecting your Retry-After

If your endpoint returns 429 with a Retry-After header, we honour it for the next attempt. If the next scheduled attempt is sooner than Retry-After, we wait the longer of the two.

Concurrent delivery

Up to 4 deliveries per shop are in flight at any moment across all that shop's webhooks. Spikes (e.g. an inventory sync that fires 1000 inventory.adjusted events) are queued; they don't all hit your endpoint at once.

This is per-shop, not per-webhook URL — if you have three webhooks subscribing to the same events, each event fans out to all three but the total in-flight count for that shop stays bounded.

Latency expectations

  • p50: < 2 seconds from event firing to your endpoint receiving the POST.
  • p99: < 10 seconds.
  • Pathological: minutes, when the dispatcher cron is catching up after an outage on our side. The webhook.failed and panel UI surface these.

Inspecting delivery history

Every attempt is logged. Retrieve via:

curl "https://api.sellvik.app/api/v1/admin/webhooks/wh_a1b2.../deliveries?status=failed" \
  -H "Authorization: Bearer <admin-key>"

Or in the panel: Settings → Webhooks → click a webhook → Deliveries tab.

Deliveries are retained for 30 days.

On this page