How to Scrape Official Steam Community Market Catalog
Updated
Valve's official Steam Community Market exposes
market/search/renderfor discovering CS2market_hash_namevalues and current search summaries. The route has no API key or provider fee, but it is undocumented, rate limited, and is not an official Steam Market API.
Build the search request
The request path is https://steamcommunity.com/market/search/render. Set appid=730, start=0, count=100, search_descriptions=0, and norender=1. Leave query empty to enumerate the catalog or URL-encode a phrase to narrow the results.
Useful optional parameters include sort_column, sort_dir, and CS2 category filters. Public implementations such as Steam Market API V2 demonstrate filters including category_730_Type[] and category_730_Exterior[]. Treat tag values as upstream identifiers and retain them separately from display labels.
Start by inspecting one page:
curl --fail --silent --show-error --get \
'https://steamcommunity.com/market/search/render/' \
--data-urlencode 'appid=730' \
--data-urlencode 'query=' \
--data-urlencode 'start=0' \
--data-urlencode 'count=100' \
--data-urlencode 'search_descriptions=0' \
--data-urlencode 'norender=1'Inspect the result and page the catalog
A shortened representative response is {"success":true,"start":0,"pagesize":10,"total_count":1,"results":[{"hash_name":"Fever Case","sell_listings":410171,"sell_price":89,"sell_price_text":"$0.89","asset_description":{"appid":730,"classid":"6918191812","market_hash_name":"Fever Case"}}]}. Counts and prices will change, but the nesting shows where the useful fields live.
The JSON response identifies start, pagesize, and total_count. Its results array can contain:
hash_name, the exact name required by item-specific Market routes.sell_listings, the current active sell-listing count shown in search.sell_price, an integer value in the response's minor currency unit.sell_price_text, the localized display value.asset_description, including class IDs, instance IDs, names, type text, and image paths.
Advance start by the returned page size until it reaches total_count. Do not assume Steam honors the requested count or that the total remains fixed during a long crawl. Deduplicate by appid plus hash_name, not by a localized display string.
Crawl and normalize the catalog with Node.js
This dependency-free Node.js 20 example fetches every page, validates the cursor, deduplicates results, and writes identity separately from the volatile search price. Save it as steam-catalog.mjs and run node steam-catalog.mjs.
import { writeFile } from "node:fs/promises";
const endpoint = "https://steamcommunity.com/market/search/render/";
const catalog = new Map();
let start = 0;
let pageNumber = 0;
while (pageNumber < 2_000) {
const url = new URL(endpoint);
url.search = new URLSearchParams({
appid: "730",
query: "",
start: String(start),
count: "100",
search_descriptions: "0",
norender: "1",
});
const response = await fetch(url, {
headers: { accept: "application/json" },
signal: AbortSignal.timeout(20_000),
});
const raw = await response.text();
if (response.status === 429) {
throw new Error(`Rate limited at start=${start}; resume from this cursor later`);
}
if (!response.ok) throw new Error(`HTTP ${response.status} at start=${start}`);
if (raw.trimStart().startsWith("<")) {
throw new Error(`Steam returned HTML at start=${start}`);
}
const page = JSON.parse(raw);
if (page.success !== true || !Array.isArray(page.results)) {
throw new Error(`Malformed search page at start=${start}`);
}
if (
!Number.isSafeInteger(page.pagesize) || page.pagesize <= 0 ||
!Number.isSafeInteger(page.total_count) || page.total_count < 0
) {
throw new Error(`Search cursor made no progress at start=${start}`);
}
const observedAt = new Date().toISOString();
for (const result of page.results) {
const asset = result.asset_description;
if (!asset || typeof result.hash_name !== "string") continue;
const key = `${asset.appid}:${result.hash_name}`;
catalog.set(key, {
identity: {
appid: asset.appid,
classid: asset.classid ?? null,
instanceid: asset.instanceid ?? null,
market_hash_name: result.hash_name,
icon_url: asset.icon_url ?? null,
},
observation: {
observed_at: observedAt,
sell_listings: result.sell_listings ?? null,
sell_price_minor: result.sell_price ?? null,
sell_price_text: result.sell_price_text ?? null,
},
});
}
const nextStart = start + page.pagesize;
console.error(`page=${pageNumber + 1} start=${start} items=${catalog.size}`);
if (nextStart >= page.total_count) break;
start = nextStart;
pageNumber += 1;
await new Promise((resolve) => setTimeout(resolve, 1_000));
}
await writeFile(
"steam-cs2-catalog.json",
JSON.stringify([...catalog.values()], null, 2),
);The page limit is a circuit breaker, not a prediction of catalog size. For a resumable production job, checkpoint start only after the corresponding page has been validated and stored. If the total changes during the crawl, finish the run, deduplicate, then reconcile additions on the next scheduled crawl.
Store identity separately from price
Search is most valuable as a discovery feed. Save the exact market hash name, class and instance IDs, icon path, and first-seen time. Store sell_price and sell_listings as observations with their own collection time because they can change while identity does not.
Run a full catalog crawl less often than per-item price checks. If a page returns HTTP 429, stop advancing the cursor and resume later from the last completed offset. A null or HTML body is not an empty catalog.
Use a maintained schema instead
The cs2.sh item schema provides the maintained CS2 catalog, stable item IDs, images, collections, wears, and supported Doppler, Gamma Doppler, and Case Hardened variants. It does not replace Steam search for non-CS2 applications or every upstream search tag.
Use Steam search when discovering the marketplace itself is part of the product. For a reliable full crawl, however, you must handle moving totals, duplicate results, partial pages, rate limits, blocked responses, localized values, and catalog changes indefinitely. When the goal is a CS2 item selector or stable identity layer, cs2.sh removes that collection and maintenance work.
The same data from cs2.sh
GET /v1/schema returns the maintained CS2 catalog in one response, on all plans including Demo and Developer, with no cursor to advance and no moving total to reconcile.
{
"schema_version": "v3",
"generated_at": "2026-08-12T22:18:51Z",
"counts": {
"items": 48696,
"by_category": { "skin": 21924, "sticker": 11132, "container": 479 },
"collections": 110
}
}| Property | Value |
|---|---|
| Contents | About 47,500 items and 110 collections, keyed by market_hash_name |
| Per item | Rarities, wears, float ranges, marketplace ids, images |
| Variants | Included, with collections and containers where applicable |
| Refresh | Automatically when Counter-Strike 2 game files update |
| Access | Available on all plans, including Demo and Developer |
Because the schema is keyed by market_hash_name, it is the same identity used by every price endpoint, so a catalog built from it joins directly to prices without a separate mapping table. Steam search remains the right tool when discovering the marketplace itself is the product, or for non-CS2 applications.
Disclosure: this free DIY guide is published by cs2.sh. Valve does not document this route as a supported API. Review the Steam Subscriber Agreement before automation and do not interpret a public response as a guaranteed right or service level. Tutorial code was reviewed against current collector behavior on August 25, 2026.