Skip to main content
pokemon.card_prices.snapshot and pokemon.product_prices.snapshot announce a file, not a set of changed records. Card and sealed product prices rewrite most of their rows every day, so the useful unit is not one changed price, it is the current file. This page is about that file. For the event that announces it, see Events.

The file

Each snapshot is flat NDJSON, gzipped: one price row per line, and every line applies on its own. Read and apply one line at a time rather than holding the whole file in memory.

Card prices

One line per row, exactly as it appears in the file:
variant_id is the same id the card object’s variants[].id carries, and the same id pokemon.card_variants.deleted names: join on it directly, with no lookup table in between. Rows are ordered by (card_id, variant_id, grade), so a byte-diff between two days’ files lands on exactly the rows that changed. A card carries every grade tier here, unlike the card object, which serves ungraded alone. A bulk file is applied wholesale, so trimming it would leave anyone wanting graded prices with no bulk path at all.

Sealed product prices

A sealed product has no printing to key on, so its row carries product_id where a card row carries card_id and variant_id. Rows are ordered by (product_id, grade). Both rows share the rest of their shape. currency is always USD today: read it per row rather than assume it, the same as the event’s own currency field. price is integer cents, and it is one figure rather than a spread: there is no low/high and no listing count, because a price here is derived from completed sales rather than from what sellers are currently asking.

Naming

Each file is named by the snapshot date (the UTC date the file was published, not any date inside it) and its own sha256.
Read url from the event or from the manifest below rather than building it yourself. A url always answers with the same bytes, so cache it for as long as you keep it. Two publishes on one date answer at two urls. Each file supersedes every earlier one, including an earlier publish of the same date. If a delivery never arrives, there is nothing to replay: fetch the current file instead.

observed_on rides on the row

The file is named by the date it was published. A row’s own observed_on is the day that price was actually seen. The two are ordinarily the same day, but not by construction: a capture that had to be retried, or one replayed from an earlier day, carries the day it observed rather than the day it was published. Trust each row’s own date for when that figure was true, never the date in the file’s name.

Verifying the download

sha256 is the digest of the gzipped bytes exactly as downloaded, before decompression. Hash what you fetched and compare it to the field of the same name, on the event or on the manifest below, before you read a single row.

Finding the current file

GET /v1/prices/snapshots answers with the newest snapshot of each kind:
Either value is null where nothing has published for that kind yet. Call this to bootstrap a mirror without waiting for a delivery, or to pick up a snapshot whose event you missed: the same six fields, the same file, whichever way you learned about it. The call itself is one ordinary /v1 request, and the file it names costs nothing beyond that: it lives on assets.cromos.so, outside /v1 entirely.

Retention

Each file, and the manifest entry naming it, stays available for 30 days. GET /v1/prices/snapshots always answers with the newest of each kind, never a history. A url IS a permanent name for one set of bytes, because the digest is part of it (see Naming): a (url, sha256) pair you stored keeps matching for as long as the file is retained, and a second publish of the same date lands at a second url rather than overwriting the first. What expires is the file, not its identity: after 30 days the url is gone rather than answering with something else.

Applying a snapshot

Fetch the file, verify it, then read it one line at a time:
manifest is one manifest object: snapshot_date, rows, subjects, url, sha256, bytes. GET /v1/prices/snapshots hands you that object directly. A delivery does not: unwrap it first. A snapshot event’s data is { currency, snapshot }, and snapshot is an array, under the same key every other event carries its records under. It holds one entry in the ordinary case, and can hold more than one when a single delivery announces several days’ files together. Entries are sorted by snapshot_date ascending, so snapshot[0] is the OLDEST file in the delivery, not the newest: always read the last entry, snapshot.at(-1). GET /v1/prices/snapshots answers differently: its data.cards and data.products are each already a single manifest object (or null), not an array, because that endpoint only ever answers with the newest of each kind. Write your own upsertPrice, keyed on (card_id, variant_id, grade) for cards, or (product_id, grade) for products: a row for a key you already hold replaces it. The file is a complete snapshot, not a set of changes. A printing priced yesterday and missing from today’s file is not priced today: if your own store still shows a price for it, its absence from the file is the signal to clear it.