When deliveries fire
Catalog changes — a card, sealed product, expansion or series edited by hand or by the nightly crawl — are collapsed into one delivery about ten seconds after the first change in a burst. Editing continuously does not push that back: the window is anchored to the first change, not the most recent one, so a long editing session still delivers within ten seconds and later edits in that window ride along with it. Price changes fire the moment the sync that produces them finishes, independently for each currency arm. There is no fixed time of day this is promised for — only that it happens as soon as the source data lands, not on a schedule you have to guess at or wait out. Both paths guarantee the delivery is sent promptly relative to the change. Neither path guarantees anything about the order deliveries arrive in relative to each other — see Ordering below.The delivery body
Every deliveryPOSTs the same four-key body. It is not wrapped in the { data, error }
envelope the REST API answers with — that belongs to /v1 reads, and a delivery is not one.
created_at is inside the bytes we sign, at millisecond precision — reconstructing it with
whole-second precision produces a body that will never verify. Don’t reconstruct the body at
all; see Verifying deliveries.
chunk is present only when a run produced more records than fit in one delivery (500 per
delivery); a group small enough for one delivery carries no chunk key at all. currency is
present only on the two price events.
sequence is never a body field. It rides only as the X-Webhook-Sequence header, because one
underlying event is shared by every endpoint subscribed to it while the sequence is counted
per endpoint — it cannot live in a value every endpoint reads the same copy of. See
Sequence and replay.
Headers
Deduplicate on X-Webhook-Id
Delivery is at-least-once. A response that times out on our side after your server already
committed the change looks exactly like a failure, and we will try again.
X-Webhook-Id is the value to deduplicate on. It stays the same across every retry of one
delivery and differs between two endpoints receiving the same event — which is why it, and not
the event id in the body, is the right key. Record it, and treat a repeat as already handled.
Responding
Any 2xx is success. Anything else — including a 3xx — fails the attempt and schedules a retry. Return quickly. We wait 10 seconds and no longer; a slow 200 is a failed attempt. Acknowledge first, then apply the records on your own time. Redirects are not followed. A 302 from your endpoint fails the attempt rather than forwarding a signed payload to wherever it points.Retries
Failed deliveries are retried on an exponential schedule: 11 retries over roughly 15.8 hours, enough to ride out a bad deploy without losing the change.
After the eleventh retry the delivery is marked failed and we stop. Your endpoint is never
disabled automatically — a working integration switched off by a bad afternoon is a worse
failure than a dead endpoint receiving traffic. Every attempt, with its status code and the first
512 bytes of your response, is in the delivery log under Settings → Webhooks. A failed
delivery can be resent from there — not one that already succeeded, since a resend mints a new
X-Webhook-Id and a correctly deduplicating receiver would process it again.
Ordering is not guaranteed
A delivery that keeps failing retries for up to 15.8 hours. That meanspokemon.cards.updated
for a card that was only just created can arrive after a pokemon.card_prices.updated that
already references it, even though we produced the two events in the right order — the price
event’s delivery simply succeeded first.
Within one dispatch, chunks go out in chunk.index order and X-Webhook-Sequence is allocated
in that same order, so in-order arrival is the overwhelmingly common case. It is not a guarantee
you can build correctness on.
Design around it: treat a price record naming a card id you don’t recognize yet as
buffer it, or fetch the card directly — never as an error. Apply every record idempotently,
keyed on the record’s own id (card_id, product_id, and so on), not on delivery order. A
record for a card that arrives twice, or a price that arrives before its card, both need to leave
your mirror in the same correct state.
Sequence and replay
Every delivery to a given endpoint carries a monotonic, gaplessX-Webhook-Sequence, counted per
endpoint — not globally, and not per event. Track the highest value you have processed for each
endpoint; a hole in that sequence means a delivery you never received, something 15.8 hours of
exhausted retries cannot rule out on its own.
Worked example: your log shows sequence 127, then jumps straight to 129. Fetch the missing
delivery by endpoint and sequence:
{ data, error } envelope every
/v1 read answers with:
data.data is not a typo: the outer key is that envelope, the inner one is the event’s own
records. Unwrap exactly one level and you have the delivery body above, byte for byte — a
delivery we POST to you is never enveloped, so that one unwrap is all a replayed delivery
needs before it goes through the same handler.
The delivery’s sequence comes back on the response’s own X-Webhook-Sequence header — never in
the body, so a replayed delivery reaches your handler in exactly the shape a live one did. Your own
endpoint id travels on every delivery as X-Webhook-Endpoint, so you always have it on hand
without hardcoding it. See Get a delivery for the endpoint’s full reference.
Bounded by 30 days. Replay reads from the same storage a delivery is retried from, and that
storage is pruned after 30 days. A gap older than that cannot be recovered this way — past that
window, a full resync (below) is the only option.
Cold start
Replay repairs a gap in a mirror that is already running; it cannot seed one that is empty, since an empty mirror has no sequence to start from. For that, pull the whole catalog once with prices attached:expansion_id to do it an
expansion at a time.
The cold-start procedure: page that call to the end, note the sequence of the next delivery you
receive after the walk finishes, and stay incremental from there — processing deliveries as they
arrive and using replay to fill any gap you notice.
Setting one up
Endpoints are managed in the console under Settings → Webhooks. Add one, choose the events you want, and copy the signing secret — it is shown once when you create it, and revealable afterwards from the endpoint’s page (see Verifying deliveries). Send test event on the endpoint’s page sends a real, signed delivery for whichever event you pick, built from real catalog rows, through the same path a live delivery takes — so you can confirm your verification and your handler both work without waiting for a real change. It carries"test": true in the body and X-Webhook-Test: true on the request, and it is tried once rather
than retried.
There is no API for managing endpoints. The console is the only way, by design.