Skip to main content
A webhook endpoint is how you hear about a change when it happens, carrying the records that changed. There is no daily batch and nothing to refetch to find out what moved — the delivery already has it.

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 delivery POSTs 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 means pokemon.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, gapless X-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:
which returns the exact body that delivery carried, inside the { 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:
adds each card’s current prices onto the normal, paginated card listing, so a full sync is one pass over the whole catalog instead of one request per card. Add 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.