Skip to main content
A webhook endpoint is how you hear about a change when it happens. A catalog change, a card, sealed product, expansion or series, carries the changed records directly in the delivery: nothing to refetch. A price change carries a pointer to the current snapshot file instead, since card and sealed product prices rewrite most of their rows every day. See Price snapshots for that file. Webhook payloads are complete for the records they name. A consumer does not need to call back into the REST API to process an event.

When deliveries fire

Catalog changes, meaning a card, sealed product, expansion or series changed, are collapsed into one delivery about ten seconds after the first change in a burst. Continuous editing does not push that back: the window is anchored to the first change rather than 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 when a new snapshot file publishes: at most a few deliveries a day, never one per changed price. See Price snapshots for the file itself. Both paths guarantee the delivery is sent promptly relative to the change. Neither guarantees anything about the order deliveries arrive in relative to each other. See Ordering below. Receiving a delivery costs nothing against your rate limit: it is a request to your endpoint, not one you make. Fetching the file a price snapshot points to costs nothing either, since the file lives outside /v1, on assets.cromos.so. Only a call you make into /v1 yourself counts against your rate limit.

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, which belongs to /v1 reads.
created_at is inside the signed bytes at millisecond precision, so reconstructing it with whole-second precision produces a body that will never verify. Do not 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 event is shared by every endpoint subscribed to it while the sequence is counted per endpoint. See Sequence and replay.

Headers

Deduplicate on X-Webhook-Id

Delivery is at-least-once. A response that times out after your server already committed the change is indistinguishable from a failure, and the delivery is retried. 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. The wait is 10 seconds and no longer, so 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 no further attempt is made. 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. One that already succeeded cannot, 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 newly created card can arrive after you have already fetched a card price snapshot whose file carries a row for it, even though the underlying data was produced in the right order. 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 snapshot row, or any catalog record, naming a card id you do not recognise yet as buffer it, or fetch the card directly, never as an error. Apply every record idempotently, keyed on its own id (card_id, product_id, variant_id, and so on) rather than on delivery order. A card record that arrives twice, and a price row 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, which 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:
You get back the exact body that delivery carried, inside the { data, error } envelope every /v1 read answers with:
The outer data is the envelope and 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 sent to your endpoint 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. A gap older than that cannot be recovered this way, and a full resync (below) is the only option.

Cold start

Replay repairs a gap in a mirror that is already running. It cannot seed an empty one, which has no sequence to start from. For that, pull the whole catalog once with prices attached:
That attaches every printing, with its current prices, to each row of the normal paginated card listing, so a full sync is one pass over the 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 inside data 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.