How to sync the full CS2 item catalog with float ranges and images
Sync every CS2 item with float ranges, collections, images, and per-market ids, then keep it current with ETags and incremental change cursors.
On this page
Almost every CS2 tool needs a local copy of the item catalog: the list of every skin, sticker, case, and agent, with images, rarities, collections, float ranges, and the ids each marketplace uses for the same item. Prices change by the second, but the catalog changes rarely, mostly when a game update adds a case or a collection. That difference is the whole design of a good sync: download the catalog once, then fetch only what changed.
SkinPricer serves the catalog as the item schema: one versioned snapshot at GET /v1/schema, a single-item lookup at /v1/schema/items/:idOrName, and an incremental feed at /v1/schema/changes. All three are included from the Developer plan up. This guide builds a mirror that stays current for a few hundred bytes on a quiet day.
What is in the schema#
The snapshot maps every tracked market_hash_name to a record, plus three shared registries (rarities, collections, containers) that items reference by key, so collection art and rarity colors are stored once rather than on every item. Records are sparse: a field that does not apply is omitted, never set to null. A sticker has no floatRange, and a case has a releaseDate but no weapon.
| Field | Why you want it |
|---|---|
id | The canonical item id, the same one pricing, NBBO, and liquidity responses return. It is your join key between catalog and prices. |
floatRange | { min, max } float bounds for items that have wear. Essential for float-aware pricing and trade-up tools. |
wears, hasStattrak, hasSouvenir | Which exteriors and variants exist for the base item, so you can build variant pickers without guessing. |
variants | Doppler and Gamma Doppler phase rows under a base item, each with its own priceable marketHashName. |
collections, containers | Keys into the registries: which collection an item belongs to and which cases drop it. |
ids | Each marketplace's own catalog id for the item, which is what you need to link out to listings or reconcile venues. |
image | An image URL for the item. |
Step 1: download the snapshot once#
The snapshot is precomputed and large (every item in the catalog), so ask for it compressed. The response carries an ETag header and a top-level schemaVersion. Store both along with the items: the ETag lets you check for changes cheaply (an unchanged catalog answers 304 with no body), and the version is where the change feed will start.
export SKINPRICER_API_KEY="sp_live_•••" # from your account's API keys page
# Download once and keep the ETag from the response headers.
curl -s --compressed -D headers.txt -o schema.json \
"https://pricing.skinpricer.com/v1/schema" \
-H "Authorization: ApiKey $SKINPRICER_API_KEY"
ETAG=$(grep -i '^etag:' headers.txt | cut -d' ' -f2- | tr -d '\r')
# Later: a conditional request returns 304 with no body if nothing changed.
curl -s -o /dev/null -w "%{http_code}\n" --compressed \
"https://pricing.skinpricer.com/v1/schema" \
-H "Authorization: ApiKey $SKINPRICER_API_KEY" \
-H "If-None-Match: $ETAG"const BASE = "https://pricing.skinpricer.com/v1";
const AUTH = { Authorization: `ApiKey ${process.env.SKINPRICER_API_KEY}` };
export interface SchemaItem {
id: string;
marketHashName: string;
slug: string;
category: string;
floatRange?: { min: number; max: number };
ids: Record<string, string>;
image?: string;
[field: string]: unknown; // sparse: other fields appear only when they apply
}
export interface SchemaSnapshot {
schemaVersion: number;
generatedAt: string;
itemCount: number;
items: Record<string, SchemaItem>;
}
/** Returns the snapshot and its ETag, or null when `etag` is still current. */
export async function fetchSnapshot(etag?: string) {
const res = await fetch(`${BASE}/schema`, {
headers: { ...AUTH, ...(etag ? { "If-None-Match": etag } : {}) },
});
if (res.status === 304) return null;
if (res.status === 503) {
throw new Error(`Snapshot not ready, retry in ${res.headers.get("retry-after") ?? 60}s`);
}
if (!res.ok) throw new Error(`GET /v1/schema failed: ${res.status}`);
return {
etag: res.headers.get("etag"),
snapshot: (await res.json()) as SchemaSnapshot,
};
}Right after a fresh deploy the snapshot can briefly be unavailable; the API then answers 503 with a Retry-After header. Wait and retry rather than treating it as an error in your data.
Step 2: stay current with the change feed#
Re-downloading the snapshot to catch one new case is wasteful. The change feed returns only the items added or changed strictly after a since value, which is the schemaVersion you stored (a millisecond epoch; an ISO-8601 timestamp also works). An item's version only moves when its content actually changes, so on a quiet day the answer is a nearly empty page.
The feed is keyset-paginated. Each page has hasMore and a nextCursor; pass the cursor back until hasMore is false. limit sets the page size, from 1 to 5000 (default 1000). When the last page is applied, store that page's schemaVersion as your next since. Saving it earlier risks skipping items if your process stops halfway.
interface SchemaChanges {
schemaVersion: number;
since: number;
count: number;
hasMore: boolean;
nextCursor: string | null;
items: SchemaItem[];
}
/** Apply every change after `since`; returns the version to store next. */
export async function syncChanges(
since: number,
upsert: (item: SchemaItem) => Promise<void>,
): Promise<number> {
let cursor: string | null = null;
let version = since;
do {
const url = new URL(`${BASE}/schema/changes`);
url.searchParams.set("since", String(since));
url.searchParams.set("limit", "1000");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: AUTH });
if (!res.ok) throw new Error(`GET /v1/schema/changes failed: ${res.status}`);
const page = (await res.json()) as SchemaChanges;
for (const item of page.items) await upsert(item); // key by item.id
version = page.schemaVersion;
cursor = page.hasMore ? page.nextCursor : null;
} while (cursor);
return version; // persist only after every page is applied
}import os
import requests
BASE = "https://pricing.skinpricer.com/v1"
AUTH = {"Authorization": f"ApiKey {os.environ['SKINPRICER_API_KEY']}"}
def sync_changes(since: int, upsert) -> int:
cursor, version = None, since
while True:
params = {"since": since, "limit": 1000}
if cursor:
params["cursor"] = cursor
res = requests.get(f"{BASE}/schema/changes", params=params, headers=AUTH, timeout=10)
res.raise_for_status()
page = res.json()
for item in page["items"]:
upsert(item)
version = page["schemaVersion"]
cursor = page["nextCursor"] if page["hasMore"] else None
if not cursor:
return version{
"schemaVersion": 1783069455007,
"since": 1782900000000,
"count": 1,
"hasMore": false,
"nextCursor": null,
"items": [
{
"id": "cmlofbol90hb801yozu37irb8",
"marketHashName": "Fever Case",
"slug": "fever-case",
"category": "container",
"releaseDate": "2025-02-04T00:00:00.000Z",
"image": "https://community.fastly.steamstatic.com/economy/image/...",
"ids": { "itrade": "ZG39o2", "swapgg": "68e9842efed1b98d8ba01b23:5" }
}
]
}Pages can also carry a registries sidecar with any rarity, collection, or container entries the returned items reference. Merge it into your local registries so a new collection's name and image arrive together with its first item.
Step 3: look up a single item#
When a price row arrives for an item you have not seen yet, you do not need the whole snapshot. The item lookup accepts either the canonical id that the pricing endpoints return or a URL-encoded market_hash_name, and returns that item's record plus only the registry entries it references.
curl -s "https://pricing.skinpricer.com/v1/schema/items/AK-47%20%7C%20Redline%20(Field-Tested)" \
-H "Authorization: ApiKey $SKINPRICER_API_KEY"Catalog with prices: the item list endpoints#
The schema is metadata only. If you want the catalog and its current prices in one read, there are two options. GET /v1/items/all returns every tracked item as one bare JSON array (capped at 50,000 rows) with the latest price per market and a cross-market aggregate; it is heavy, so cache it. For filtered or interactive reads, the paginated GET /v1/items search returns a full pricing snapshot per result.
// npm i @skinpricer/sdk
import { SkinpricerClient, formatUsd } from "@skinpricer/sdk";
const client = new SkinpricerClient({
apiKey: process.env.SKINPRICER_API_KEY!,
authScheme: "ApiKey",
});
const everything = await client.items.all({ timeoutMs: 60_000 }); // large response
const priced = everything.filter((item) => item.priceAggregates?.minPrice != null);
console.log(`${priced.length} of ${everything.length} items have a live price`);
const first = priced[0];
if (first?.priceAggregates?.minPrice != null) {
console.log(first.name, formatUsd(first.priceAggregates.minPrice));
}- Join on
idwhere you can. It is the canonical key that the schema and every pricing, NBBO, and liquidity response share. - Do not treat a missing field as a bug. Sparse records mean a field is absent when it does not apply to that item category.
- Rust has its own catalog. The schema routes also exist under
/v2with a requiredgameparameter, and change cursors are bound to the game they came from. See Games: CS2 & Rust.
When to reach for a different endpoint#
Once the catalog is mirrored, prices come from the pricing endpoints: the live CS2 skin prices guide covers per-item snapshots, and the comparison site guide shows the catalog and prices working together on a real page. If you are using floatRange to price specific floats, continue with how to price a CS2 skin by float range, which pairs the schema bounds with per-bucket prices.
Endpoints used in this guide
Plans start at $50/month on the Developer tier. Want to try first? Ask in our Discord for a free 2-day Developer key.
Related guides
- Build with the APIIntermediate
How to build a CS2 skin price comparison site
Build a CS2 skin price comparison site: search the catalog, show every market's price side by side, and surface market health and data freshness.
GET /v1/itemsGET /v1/pricing/:marketHashName6 min read - Floats & attributesIntermediate
How to price a CS2 skin by its float range
Price a CS2 skin by its exact float: how float-range buckets work per market, how to query them, and how to spot low-float premiums. Runnable API code.
GET /v1/attribute-prices/latestGET /v1/attribute-prices/history6 min read - Prices & quotesBeginner
How to get live CS2 skin prices from every marketplace
Pull live CS2 skin prices from every major marketplace in one call: per-market asks, bids, and aggregate stats in USD cents, with runnable curl and TypeScript.
GET /v1/pricing/:marketHashNameGET /v1/items6 min read