
How to read a Steam inventory for a trading bot
Every trading bot starts the same way: ask Steam what a customer owns. Here is how Steam’s public inventory endpoint works, what it sends back, how to page through a big inventory, and what each error really means.
1. The inventory endpoint
Steam serves every public inventory, one page at a time, from this address:
https://steamcommunity.com/inventory/{steamid64}/{appid}/{contextid}?l=english&count=2000- steamid64 is the 17 digit SteamID, starting
7656119. - appid and contextid pick which inventory. The ones trading bots use most:
753 / 6 Steam items: trading cards, gems, backgrounds, emoticons 440 / 2 Team Fortress 2 (keys, weapons, hats) 730 / 2 Counter-Strike 2
- l is the language for names and descriptions. Use
englishso your bot sees the same text every time. - count is how many items a page holds. More on that under paging.
The inventory must be public. Nothing on this endpoint reads a private one, and no key or login changes that for someone else’s inventory.
2. What Steam sends back
A page of JSON with two lists and a few fields about the whole inventory. Trimmed, it looks like this:
{
"assets": [
{
"appid": 753,
"contextid": "6",
"assetid": "12345678901",
"classid": "667924416",
"instanceid": "3874601840",
"amount": "1"
}
],
"descriptions": [
{
"appid": 753,
"classid": "667924416",
"instanceid": "3874601840",
"market_hash_name": "440-Scout",
"market_fee_app": 440,
"type": "Team Fortress 2 Trading Card",
"tradable": 1,
"marketable": 1,
"tags": [
{ "category": "Game", "internal_name": "app_440" },
{ "category": "item_class", "internal_name": "item_class_2" },
{ "category": "cardborder", "internal_name": "cardborder_0" }
]
}
],
"total_inventory_count": 3512,
"more_items": 1,
"last_assetid": "12345678901",
"success": 1
}- assets are the actual items: one entry per item (or per stack, with
amountabove 1), each with its ownassetid. This is what you put in a trade offer. - descriptions say what each kind of item is: its name,
type, whether it istradableandmarketable, and itstags. Two copies of the same card share one description. - total_inventory_count is how many items the whole inventory holds, across every page.
- more_items and last_assetid appear only when there is another page.
- success is
1on a good read.
Join the two lists yourself. An asset and its description share a classid and an instanceid, so build a lookup keyed on both and attach the description to each asset. Keying on classid alone mixes up items that differ only by instance.
const key = (x) => `${x.classid}_${x.instanceid}`;
const byKey = new Map(page.descriptions.map((d) => [key(d), d]));
const items = page.assets.map((a) => ({ ...a, description: byKey.get(key(a)) }));3. Paging with count and start_assetid
Big inventories come in pages. Three rules cover it:
- Ask for 2,000 items a page.
count=2000is the safe page size, and the one bots use. - Ask for the next page from where the last one ended. Send the same URL with
start_assetidset to thelast_assetidyou were just given. - Stop when there is no more_items. The last page leaves it out.
Send the headers a Steam client sends. In our tests, Node.js’s default user agent was refused every time, where the same request with these was answered:
User-Agent: Valve/Steam HTTP Client 1.0
Accept: application/json
Accept-Encoding: gzip
Referer: https://steamcommunity.com/profiles/{steamid64}/inventoryThe same read from a terminal, first page, then the second:
curl --compressed \
-H "User-Agent: Valve/Steam HTTP Client 1.0" \
-H "Accept: application/json" \
-H "Referer: https://steamcommunity.com/profiles/76561198000000000/inventory" \
"https://steamcommunity.com/inventory/76561198000000000/753/6?l=english&count=2000"
curl --compressed \
-H "User-Agent: Valve/Steam HTTP Client 1.0" \
-H "Accept: application/json" \
-H "Referer: https://steamcommunity.com/profiles/76561198000000000/inventory" \
"https://steamcommunity.com/inventory/76561198000000000/753/6?l=english&count=2000&start_assetid={last_assetid}"--compressed asks for gzip and unzips the answer for you. Zipped, a full page is a fraction of its size.
4. A Node.js example
Every page, joined, in one list. Node 18 or later, no packages. The lookup is kept across pages because a page only describes its own items.
async function readInventory(steamId, appId = 753, contextId = 6) {
const headers = {
"User-Agent": "Valve/Steam HTTP Client 1.0",
Accept: "application/json",
"Accept-Encoding": "gzip",
Referer: `https://steamcommunity.com/profiles/${steamId}/inventory`,
};
const key = (x) => `${x.classid}_${x.instanceid}`;
const descriptions = new Map();
const assets = [];
let start = null;
do {
const url = new URL(`https://steamcommunity.com/inventory/${steamId}/${appId}/${contextId}`);
url.searchParams.set("l", "english");
url.searchParams.set("count", "2000");
if (start) url.searchParams.set("start_assetid", start);
const res = await fetch(url, { headers });
if (res.status === 401) return []; // owns nothing in this game
if (res.status === 403) throw new Error("private"); // ask them to make it public
if (!res.ok) throw new Error(`steam ${res.status}`); // 429 or 5xx: try again later
const page = await res.json();
if (page.success !== 1) throw new Error("steam did not answer");
for (const d of page.descriptions ?? []) descriptions.set(key(d), d);
assets.push(...(page.assets ?? []));
start = page.more_items ? page.last_assetid : null;
} while (start);
return assets.map((a) => ({ ...a, description: descriptions.get(key(a)) }));
}Note what it does on a failure: it throws. It never returns an empty list for a read that did not work. That is the next section.
5. Status codes and what they mean
- 200: a page. Check
successis1. An inventory with nothing in it can also come back as a 200 with no assets. - 403: the inventory is private. Retrying will not help. Tell the customer to make it public.
- 401: in our measurements, Steam answers 401 when the user owns nothing in that game. Treat it as an empty inventory for that app, not as an auth problem.
- 429: rate limited. Your address has asked too often. Back off and try later. The full story is in Steam inventory 429 Too Many Requests.
- 500, 502, 503: Steam had a bad moment. Wait a little and retry.
Never treat a failure as an empty inventory. A bot that reads a 429 or a 502 as “owns nothing” quotes the customer for nothing, or for far fewer sets than they have, and they leave. If a read fails, say so and ask them to try again.
6. Counting card sets
A level up bot cares about one thing in the 753/6 inventory: normal trading cards, grouped by game.
- A trading card has the tag
item_class/item_class_2. Foils carry the same tag pluscardborder/cardborder_1; leave them out of normal sets. - Group by the
Gametag (app_440), or bymarket_fee_appwhen the tag is missing. - Count only cards whose description says
tradable: 1. A card that cannot be traded yet cannot be sold to your bot.
const tag = (d, category, name) =>
(d?.tags ?? []).some((t) => t.category === category && (!name || t.internal_name === name));
const byGame = new Map(); // game -> card name -> how many
for (const item of items) {
const d = item.description;
if (!tag(d, "item_class", "item_class_2") || tag(d, "cardborder", "cardborder_1")) continue;
if (d.tradable !== 1) continue;
const game = d.tags.find((t) => t.category === "Game")?.internal_name ?? `app_${d.market_fee_app}`;
const cards = byGame.get(game) ?? new Map();
cards.set(d.market_hash_name, (cards.get(d.market_hash_name) ?? 0) + Number(item.amount));
byGame.set(game, cards);
}For each game, the number of full sets is the lowest count among its cards, provided they hold every card in the set. The inventory does not say how many cards a game’s set has, so your bot needs that list from its own data.
7. Rate limits
Steam does not publish its limit and changes it without notice. From one server in October 2026, we measured about 10 pages back to back per IP address, then roughly one page every 18 seconds. Read other people’s inventories anonymously, without your bot’s login cookies: logged in reads were refused sooner in our tests. A busy bot outgrows one address quickly. If you add more addresses, only the inventory reads may go through them, never the bot’s Steam login, which stays on your server’s own address. The details and fixes are in the 429 guide.
8. The easier route: an inventory API
The SteamTradeX Inventory API does the reading for you, from our own pool of addresses, and hands back Steam’s own JSON. The same assets, descriptions, more_items and last_assetid, so code written for Steam reads it unchanged. Only the address and one header change:
GET https://steamtradex.com/api/inventory/{steamid}/{appid}/{contextid}?count=2000
X-API-KEY: inv_your_keyOptional count (up to 2,000, and 2,000 when you leave it out), start_assetid for the next page, and l for the language (English by default). In the Node.js example above, change the URL and the headers and nothing else:
const url = new URL(`https://steamtradex.com/api/inventory/${steamId}/${appId}/${contextId}`);
const headers = { "X-API-KEY": process.env.STEAMTRADEX_API_KEY };- A game the user owns nothing in comes back as an empty inventory with a 200, so you do not need the 401 special case. Private is still a 403.
- Every answer to a read that reaches Steam carries
x-requests-charged,1or0, so you always know whether it used a request. - Private, empty and failed reads are free, within a fair-use limit for private and empty ones (20 a minute, 120 an hour and 600 a day per account). You pay only for a page with items in it.
- Up to 2,000 items a request, from 7,000 requests a TF2 key, and 100 free requests to try it, once per Steam account that has spent $5 on Steam.
- The data is always live, read from Steam when you ask, never cached.
Every parameter and error is in the API reference. Running a level up or gem bot you did not write? Add it with one command.
Questions
Is there an official Steam inventory API?
Not one that most bots use. Steam’s Web API has had game-specific item calls, and many of those have been switched off. Most trading bots read the public community endpoint in this guide, which works the same way for every game.
Can I read a private inventory?
No. A private inventory answers 403 to everyone but its owner. Ask the customer to set their inventory to public in their Steam privacy settings, then read it again.
How many items can I get per request?
Ask for 2,000 with count=2000. A bigger inventory takes more pages: use start_assetid set to the previous page’s last_assetid until there is no more_items.
Do I need a Steam Web API key?
Not for the community endpoint. Public inventories are read without a key or a login, and reading them anonymously is what we recommend. Our Inventory API uses its own key, which starts inv_.
Why do I get an empty inventory when the customer has items?
Check the app and context first: cards and gems are 753/6, not 440/2 or 730/2. Then check the status code. If your code turns a 429 or a 5xx into an empty list, that is the bug.