BUFF sale history is now available - POST /v1/archive/buff returns sale prices and daily total supply since September 2024.

How to Scrape Official Steam Community Market Prices

Updated

Endpoints used: GET /v1/prices/latest

Related pages: Steam price API

Valve's official Steam Community Market exposes a public priceoverview route that returns one item's lowest sell listing, median price, and volume without an API key or API fee. The route is undocumented and is not an official Steam Market API, so design for throttling and response changes.

Request one exact item#

Send a GET request to https://steamcommunity.com/market/priceoverview/ with country=US, currency=1, appid=730, and an exact URL-encoded market_hash_name.

For example, the name portion of AK-47 | Redline (Field-Tested) must be encoded before it is placed in the query string. Do not remove the wear or change punctuation: Steam treats the complete market hash name as the item identity.

Parameter Purpose
appid 730 for Counter-Strike 2
market_hash_name Exact URL-encoded Steam item name
currency Steam currency identifier; 1 requests USD
country Country code used with price display and fees

Try the request from a terminal before writing a collector:

bash
curl --fail --silent --show-error --get \
  'https://steamcommunity.com/market/priceoverview/' \
  --data-urlencode 'country=US' \
  --data-urlencode 'currency=1' \
  --data-urlencode 'appid=730' \
  --data-urlencode 'market_hash_name=AK-47 | Redline (Field-Tested)'

--data-urlencode matters. Building this URL by concatenating the item name is a common source of failures for names containing |, spaces, parentheses, or non-ASCII characters.

Inspect the response shape#

A representative successful body for Fever Case is {"success":true,"lowest_price":"$0.89","volume":"59,569","median_price":"$0.89"}. Prices and volume are display strings, not JSON numbers.

Field Type Meaning
success Boolean Whether Steam returned a recognized result
lowest_price Localized string Current lowest sell listing
median_price Localized string Steam's current median summary
volume Localized string Steam's displayed sale-volume summary

Keep the raw strings, then parse them with rules tied to the requested currency and locale. A failure may be {"success":false}, an empty body, or HTML instead of JSON.

Do not substitute one field for another. lowest_price is a current sell listing. median_price is Steam's summary statistic, not a historical series. volume is Steam's summary volume value, not the number of active listings. Fields can be absent even when the JSON object itself is valid.

Build a runnable Node.js collector#

The following example uses the built-in fetch in Node.js 20 or newer. Save it as priceoverview.mjs, then run node priceoverview.mjs "AK-47 | Redline (Field-Tested)".

js
const marketHashName =
  process.argv.slice(2).join(" ") || "AK-47 | Redline (Field-Tested)";

const listingUrl =
  `https://steamcommunity.com/market/listings/730/${encodeURIComponent(marketHashName)}`;
const url = new URL("https://steamcommunity.com/market/priceoverview/");
url.search = new URLSearchParams({
  country: "US",
  currency: "1",
  appid: "730",
  market_hash_name: marketHashName,
});

const response = await fetch(url, {
  headers: {
    accept: "application/json",
    referer: listingUrl,
  },
  signal: AbortSignal.timeout(15_000),
});

const raw = await response.text();
if (response.status === 429) throw new Error("Steam rate limited the request");
if (!response.ok) throw new Error(`Steam returned HTTP ${response.status}`);
if (raw.trimStart().startsWith("<")) {
  throw new Error("Expected JSON but Steam returned HTML");
}

let payload;
try {
  payload = JSON.parse(raw);
} catch (error) {
  throw new Error(`Steam returned malformed JSON: ${error.message}`);
}
if (payload.success !== true) throw new Error("Steam returned success=false");

function parseUsd(value, field) {
  if (value == null) return null;
  if (typeof value !== "string" || !/^\$[\d,]+(?:\.\d{2})?$/.test(value)) {
    throw new Error(`Unexpected ${field}: ${JSON.stringify(value)}`);
  }
  return Number(value.slice(1).replaceAll(",", ""));
}

function parseVolume(value) {
  if (value == null) return null;
  if (typeof value !== "string" || !/^[\d,]+$/.test(value)) {
    throw new Error(`Unexpected volume: ${JSON.stringify(value)}`);
  }
  return Number(value.replaceAll(",", ""));
}

const observation = {
  market_hash_name: marketHashName,
  observed_at: new Date().toISOString(),
  currency: "USD",
  lowest_price: parseUsd(payload.lowest_price, "lowest_price"),
  median_price: parseUsd(payload.median_price, "median_price"),
  volume: parseVolume(payload.volume),
  raw: payload,
};

console.log(JSON.stringify(observation, null, 2));

This parser intentionally accepts missing price fields as null but rejects an unfamiliar format. That distinction lets downstream code separate "Steam omitted this value" from "the parser no longer understands the response."

Turn a request into a collector#

  1. Start with exact names from a maintained CS2 catalog.
  2. Encode every name and record the requested country and currency.
  3. Require success=true before accepting fields.
  4. Store an observation timestamp beside the raw response.
  5. Treat HTTP 429, HTML, null JSON, and missing fields as failures rather than zero prices.
  6. Add bounded retry delays and cache results instead of polling unchanged items repeatedly.

Steam publishes no request allowance for this Market route. The 100,000-call figure in the Steam Web API terms applies to the documented Web API, not priceoverview.

The managed alternative#

The endpoint is free to call, but it handles only one exact item per request. Catalog scheduling, blocks, missing fields, retries, storage, freshness checks, and historical retention become the real work. A collector that quietly accepts HTML or a missing field can publish a false zero while still reporting successful HTTP requests.

If one Steam item is all you need, scraping can still be reasonable. If collection is only a prerequisite, cs2.sh provides current prices and historical OHLC without operating a Steam collector.

The same data from cs2.sh#

priceoverview returns one item per request as localized display strings. GET /v1/prices/latest returns every tracked item at once as typed numbers, across six marketplaces, on all plans.

bash
curl -X POST https://api.cs2.sh/v1/prices/latest \
  -H "Authorization: Bearer <<YOUR_API_KEY>>" \
  -H "Accept-Encoding: gzip" --compressed \
  -H "Content-Type: application/json" \
  -d '{"items": ["AK-47 | Redline (Field-Tested)"]}'
json
"steam": {
  "updated_at": "2026-07-26T18:52:21.42Z",
  "collected_at": "2026-07-26T18:52:56.46Z",
  "ask": 169.14,
  "ask_volume": 70,
  "bid": 155.05,
  "bid_volume": 2951
}
priceoverview cs2.sh
lowest_price, a localized string ask, a number in USD
volume, a localized summary string ask_volume and bid_volume, active order counts
No buy-order price bid, the highest buy order
No timestamp updated_at and collected_at per source
One item per request Every tracked item, or up to 100 by name
Steam only BUFF, Youpin, CSFloat, Skinport, Steam, C5Game

median_price has no cs2.sh equivalent in current prices because it is a summary statistic rather than a live order. The comparable series is POST /v1/archive/steam, which returns Steam's own median sale price and purchase count per bucket, daily from April 26, 2013.

Disclosure: this free DIY guide is published by cs2.sh. The Steam Subscriber Agreement currently restricts automated interaction with Steam. This article documents a public read route and does not grant permission to automate it. Tutorial code was reviewed against current collector behavior on August 25, 2026.