WebhooksDelivery, Retry & Dedup

Delivery, Retry & Dedup

What your endpoint must do

Your handler must:

  1. Acknowledge fast: respond with 2xx within ~10 seconds. Do the heavy work asynchronously after responding, or queue the event and return 202.
  2. Be idempotent — the same X-Event-Id may arrive more than once (see dedup contract).
  3. Tolerate unknown event types and unknown fields by returning 2xx, not 4xx (versioning).

If you respond with anything other than 2xx, see the retry policy.

Retry policy

Each event carries a per-merchant attempt counter (DeliveryAttempts in the cabinet’s Webhook Events view). Outcomes are classified as the request returns:

OutcomeWhat triggers itWhat happens next
SuccessHTTP 2xxMarked delivered. No further attempts.
TransientHTTP 408, 429, any 5xx; network error / DNS failure; per-attempt timeout (30s wall-clock).Auto-retried on a fixed backoff (see below).
TerminalHTTP 4xx (except 408/429); SSRF block; webhook URL or secret missing; unexpected serialization.Marked failed immediately. No auto-retries.

The auto-retry backoff is fixed: 1 minute → 10 minutes → 1 hour → marked failed. Three retries after the first attempt is the cap. Schedule is keyed off DeliveryAttempts after the latest failure, not from created_at.

attempt #1 fails transiently → schedule attempt #2 in   1 min
attempt #2 fails transiently → schedule attempt #3 in  10 min
attempt #3 fails transiently → schedule attempt #4 in   1 hour
attempt #4 fails transiently → terminal `failed` (no further auto-retries)

A 4xx (other than 408/429) short-circuits the chain immediately. We don’t re-validate your URL on a 404 because the failure is on your side; rotate your webhook URL and ask support for a replay (see Manual Replay).

Per-attempt timeout

Each delivery attempt is bounded by 30 seconds wall-clock including DNS, TLS, request, and response body read. Anything slower is treated as a transient timeout and retried. Don’t try to do work synchronously in the handler that won’t fit in that budget.

Dedup contract

We deliver at-least-once. Network glitches, your transient 5xx, and edge-case duplicate insertions (e.g. concurrent state transitions) can all cause the same logical event to arrive twice. Your receiver MUST dedup. The key depends on the event:

Event typeDedup key
purchase.created, purchase.sent_to_steam, purchase.completed, purchase.failed, purchase.refunded(data.purchase_id, event) — one delivery per pair.
balance.depositeddata.provider_uuid (Heleket payment UUID, stable).

X-Event-Id is also unique per delivery and is the simplest dedup key for receivers that store events generically — but the contractual keys above are what we guarantee on our side, and they are what you should use for business-level dedup (e.g. “did I already mark this purchase complete?”).

A retried attempt keeps the same X-Event-Id and body. Only X-Timestamp and X-Signature change between attempts.

Operational tuning

These constants are not configurable per merchant in v1 — they are listed only so you know what to expect:

KnobValue
Sender poll interval5 seconds
Batch size per poll50 events
Per-attempt wall-clock cap30 seconds
Stale-claim reclaim window120 seconds
Auto-retry backoffs1m → 10m → 1h
Max auto-retries3 (after attempt #1)
Stored response body lengthFirst 500 chars
Worker startup delay10 seconds (after deploy/restart)