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
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 ownsha256.
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:
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.