Inventory API
Read one page of any public Steam inventory, live from Steam, through our readers. Your bot gets Steam’s own JSON back, so code written for Steam reads it unchanged.
This is the paid Inventory API: it reads inventories, with keys starting inv_. Keeping your listed bot’s rates up to date on the bot list is the free Listing API, a different thing with different keys (stx_): see the Listing API reference.
Introduction
A trading bot needs to see what is in a customer’s inventory before it builds a trade offer. Steam limits how often one server may ask, so a busy bot soon gets refused. This API asks for you, from our own readers, and hands back exactly what Steam said.
- Live. Every request reads Steam at that moment. Nothing is cached, so an offer is built on the inventory as it is now.
- Steam’s own format.
assets,descriptions,more_items,last_assetid: the answer is passed on as Steam sent it. - Pay for answers only. One request per page of up to 2,000 items. Private inventories, people who own nothing in that game, and reads that fail cost nothing.
Base address: https://steamtradex.com. Every answer is JSON.
Running a level-up or gem bot on the levelup-bot script? You do not need any of this: one copy-paste command sets it all up. Follow Use the Inventory API with your Steam bot, or see With a levelup-bot for the command in short. This page is for people writing their own code.
Authentication
Send your key in an X-API-KEY header with every request:
X-API-KEY: inv_your_key
Authorization: Bearer inv_your_key works too, and is used when there is no X-API-KEY header (or it is empty). A key in the address, such as ?key=, is not read: addresses end up in logs, so we never accept one there.
- Create keys at Dashboard, API, Keys. A key is shown once, so copy it then. Inventory API keys start
inv_. - Every key on your account draws from the same balance of requests. Use one key per bot, so you can see each bot’s use and revoke one without stopping the others.
- Rotate or revoke a key on the same page. A revoked key stops at once and gets
401. - Keep it on your server. Anyone holding your key can spend your requests. Never put it in a web page, a public repository or a screenshot.
Quick start
- Sign in and claim your 100 free requests on Dashboard, API, Buy (for Steam accounts that have spent $5 on Steam), or buy requests with TF2 keys.
- Create a key at Dashboard, API, Keys.
- Read an inventory. Put a real SteamID64 in place of the example:
curl "https://steamtradex.com/api/inventory/76561198012345678/753/6" \ -H "X-API-KEY: inv_your_key"
You get a page of Steam’s inventory JSON and the header x-requests-charged: 1. That is it. Or skip the terminal and use Try it below.
Endpoint
Reads one page of the inventory of steamid in the game appid, part contextid.
| What | appid | contextid |
|---|---|---|
| Steam items: trading cards, gems, backgrounds, emoticons | 753 | 6 |
| Team Fortress 2 | 440 | 2 |
| Counter-Strike 2 | 730 | 2 |
| Dota 2 | 570 | 2 |
| Rust | 252490 | 2 |
The older form
The same reader, the same answers and the same charging, with appid and contextid in the query string. Left out, they are 753 and 6 (Steam items). Use the first form for anything new.
Parameters
In the path
| Name | Rule | What it is |
|---|---|---|
| steamid required | 17 digits, starting 7656119 | The SteamID64 of the person whose inventory you want. Their inventory must be public. Example: 76561198012345678 |
| appid required | digits only, 1 to 10 of them | The game the items belong to. 753 is Steam itself (cards, gems, backgrounds). Example: 753 |
| contextid required | digits only, 1 to 10 of them | Which part of that game's inventory. 6 for Steam items, 2 for most games. Example: 6 |
In the query string (all optional)
| Name | Rule | What it is |
|---|---|---|
| count default 2000 | 1 to 2,000 | Items per page. A number outside 1 to 2,000 is moved to the nearest end; anything that is not a number counts as 2,000. A page costs one request whatever its size, so leave it at the most. Example: 2000 |
| start_assetid optional | digits only, up to 20 of them | Where the next page starts: the last_assetid from the page before. Leave it out for the first page. Example: 31846251911 |
| l default english | 2 to 20 letters, a Steam language name | The language item names and descriptions come back in, such as english, german or schinese. language is accepted as another name for it; when both are sent, l wins. Capitals are made lower case.Example: english |
Anything that breaks these rules is refused with 400 before we read anything, and it is free. Parameters we do not know are ignored.
Response
A 200 answer is Steam’s own inventory JSON, unchanged. The parts you will use:
| Field | What it is |
|---|---|
| assets[] | One entry per item (or stack): appid, contextid, assetid (this one item; it changes when the item is traded), classid, instanceid and amount (more than 1 for stacks such as gems). Steam sends the ids as strings. |
| descriptions[] | What each kind of item is: name, market_hash_name, type, tradable, marketable, icon_url, tags and more. One entry per kind, not per item: join it to an asset on classid and instanceid. |
| total_inventory_count | Items in this game’s inventory in total, across every page. |
| more_items | 1 when there is another page. Absent on the last page. |
| last_assetid | The last item on this page. Send it as start_assetid to get the next page. |
| success | 1. |
Steam sometimes adds fields of its own (such as rwgrsn). We pass them on; ignore what you do not use.
{
"assets": [
{
"appid": 753,
"contextid": "6",
"assetid": "31846250114",
"classid": "5249311904",
"instanceid": "3873503133",
"amount": "1"
},
{
"appid": 753,
"contextid": "6",
"assetid": "31846251911",
"classid": "667924416",
"instanceid": "0",
"amount": "1250"
}
],
"descriptions": [
{
"appid": 753,
"classid": "5249311904",
"instanceid": "3873503133",
"currency": 0,
"background_color": "",
"icon_url": "IzMF03bi9WpSBq-S-ekoE33L-iLqGFHVaU25ZzQNQcXdEH9myp0erksICfTcePMQFc1nqWSMU5OD2NwOx3sIyShXOjLx2Sk5MbV5MsLD3k3xpaqqvZhC",
"descriptions": [
{
"type": "html",
"value": ""
}
],
"tradable": 1,
"name": "Heavy",
"name_color": "",
"type": "Team Fortress 2 Trading Card",
"market_name": "Heavy",
"market_hash_name": "440-Heavy",
"market_fee_app": 440,
"commodity": 1,
"market_tradable_restriction": 7,
"market_marketable_restriction": 7,
"marketable": 1,
"tags": [
{
"category": "Game",
"internal_name": "app_440",
"localized_category_name": "Game",
"localized_tag_name": "Team Fortress 2"
},
{
"category": "item_class",
"internal_name": "item_class_2",
"localized_category_name": "Item Type",
"localized_tag_name": "Trading Card"
},
{
"category": "cardborder",
"internal_name": "cardborder_0",
"localized_category_name": "Card Border",
"localized_tag_name": "Normal"
}
]
},
{
"appid": 753,
"classid": "667924416",
"instanceid": "0",
"currency": 0,
"background_color": "",
"icon_url": "IzMF03bi9WpSBq-S-ekoE33L-iLqGFHVaU25ZzQNQcXdA3g5gMEPvUZZEfSMJ6dESN8p_2SVTY7V2NwOx3sIyShXOjLx2Sk5MbV5MsLD3k3xpaqqvZhC",
"descriptions": [
{
"type": "html",
"value": "These gems can be used to create Booster Packs or animated stickers."
}
],
"tradable": 1,
"name": "Gems",
"name_color": "",
"type": "Steam Gems",
"market_name": "Gems",
"market_hash_name": "753-Gems",
"commodity": 1,
"marketable": 0,
"tags": [
{
"category": "Game",
"internal_name": "app_753",
"localized_category_name": "Game",
"localized_tag_name": "Steam"
},
{
"category": "item_class",
"internal_name": "item_class_7",
"localized_category_name": "Item Type",
"localized_tag_name": "Gems"
}
]
}
],
"more_items": 1,
"last_assetid": "31846251911",
"total_inventory_count": 4318,
"success": 1,
"rwgrsn": -2
}The example is cut down to two items. A real page has up to 2,000 assets and a description for each kind of item among them.
{
"assets": [],
"descriptions": [],
"total_inventory_count": 0,
"success": 1
}When the person has no inventory at all in that game, Steam has nothing to send; we answer 200 with an empty inventory, so your code needs no special case, and it is not charged. The x-requests-charged header always tells you whether an answer used a request.
Paging
One request returns up to 2,000 items. A bigger inventory comes in pages, read one after the other:
- Ask for the first page with no
start_assetid. - If the answer has
more_items: 1, ask again withstart_assetidset to that answer’slast_assetid. - Repeat until an answer has no
more_items. Add upassetsanddescriptionsas you go.
GET /api/inventory/76561198012345678/753/6 -> more_items: 1, last_assetid: "31846251911" GET /api/inventory/76561198012345678/753/6?start_assetid=31846251911 -> more_items: 1, last_assetid: "31846263380" GET /api/inventory/76561198012345678/753/6?start_assetid=31846263380 -> no more_items: that was the last page
Each page is one request, so 10,000 cards is 5 requests at the full page size. Pages cannot be read in parallel, because each one starts where the last ended. Keep count the same on every page; leaving it out gives the full 2,000.
Errors
Anything other than 200 is JSON with success: false, an error sentence you can show or log, and the request_id (the same as the x-request-id header). Some also have message or where.
| Status | When | Charged | What to do |
|---|---|---|---|
| 400 | Not a SteamID64. The steamid is not 17 digits starting 7656119 (a profile name or a short id will not do). | No | Send the SteamID64. Your bot already has it from the trade offer or the chat. |
| 400 | Bad appid. appid is not plain digits. | No | Use the game's number, such as 753, 440 or 730. |
| 400 | Bad contextid. contextid is not plain digits. | No | Use 6 for Steam items and 2 for most games. |
| 400 | Bad start_assetid. start_assetid is not plain digits. | No | Pass the last_assetid from the previous page exactly as it came. |
| 400 | Bad language. l (or language) is not 2 to 20 letters. | No | Use a Steam language name such as english, or leave it out. |
| 401 | No key sent. There is no X-API-KEY header and no Authorization: Bearer header. A key in the address (the query string) is not read. | No | Send your key in the X-API-KEY header. |
| 401 | Key not valid. The key is wrong, revoked, or a Listing API key. Unknown and revoked keys get the same answer on purpose. | No | Copy the key again from your dashboard, or create a new one. Inventory API keys start inv_. |
| 402 | No requests left. Your account's balance of requests is used up. | No | Buy more with TF2 keys in your dashboard. Requests you buy never expire. |
| 403 | Inventory is private. The person's inventory is not public. No address or retry will change that. | No | Ask them to set their inventory to public in their Steam privacy settings, then try again. |
| 429 | Too fast. More than 120 requests in one minute on your account, counted across all of its keys. | No | Wait the number of seconds in the retry-after header, then carry on. |
| 429 | Too many free reads. Your account has read more private or empty inventories than the free allowance: 20 a minute, 120 an hour and 600 a day. Pages you pay for never count towards it once they are answered; a read still in progress does, so more than 20 reads at the same moment can also meet it. | No | Wait the number of seconds in the retry-after header (often less is enough when reads were only in progress). Paid reads work again at the same moment. |
| 502 | Could not get it from Steam. Steam did not give us that page after three tries through different addresses. | No | Try again. If it keeps happening for one inventory, wait a few minutes. |
| 503 | Steam refused the read. Steam turned the read away on every try. | No | Wait the seconds in retry-after (30), then try again. |
| 503 | Every reader busy. All of our readers were in use at that moment. | No | Wait the seconds in retry-after (60), then try again. |
| 503 | Service paused. Only if we ever pause the Inventory API for everyone. The message says why. | No | Nothing is charged while paused. Check the status page and try later. |
| 500 | Our fault. Something broke on our side. Not JSON. | No | Try again in a minute. Tell us if it keeps happening. |
Requests are only charged for a 200 with an inventory page in it. When a request fails after your key was accepted, its request is given back before the answer reaches you.
Every error, as it arrives
{
"success": false,
"error": "That is not a SteamID64. It is 17 digits, starting 7656119.",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}{
"success": false,
"error": "appid must be a number, such as 440 for TF2 or 753 for Steam.",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}{
"success": false,
"error": "contextid must be a number, such as 2 for most games or 6 for Steam.",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}{
"success": false,
"error": "start_assetid must be a number. Use the last_assetid from the previous page.",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}{
"success": false,
"error": "l must be a Steam language name, such as english.",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}{
"success": false,
"error": "Send your API key in an X-API-KEY header.",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}{
"success": false,
"error": "That API key is not valid.",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}{
"success": false,
"error": "You have no requests left. Add TF2 keys to carry on.",
"where": "https://steamtradex.com/dashboard/api",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}{
"success": false,
"error": "inventory_private",
"message": "That inventory is private.",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}{
"success": false,
"error": "Slow down: 120 requests a minute per account. Try again in 23s.",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}{
"success": false,
"error": "Too many free reads. Private and empty inventories are free, up to 20 a minute, 120 an hour and 600 a day per account, and a read in progress counts until it is paid for. Try again in 23s.",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}{
"success": false,
"error": "We could not get that inventory from Steam. Try again.",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}{
"success": false,
"error": "Steam refused that read. Try again shortly.",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}{
"success": false,
"error": "Every reader is busy. Try again in a minute.",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}{
"success": false,
"error": "paused",
"message": "The inventory API is paused for now while we make inventory reading more reliable. Nothing is sold or charged while it is paused.",
"request_id": "req_5f0c9a1e7b2d4c6a8e3f1b90"
}Headers
| Header | Sent on | Meaning |
|---|---|---|
| x-request-id | Every answer | A reference for this one request. Quote it if you ask us about a request. |
| x-requests-charged | Once your key is accepted, on 200, 403, 502 and 503 | 1 if this answer used one of your requests, 0 if it was free. |
| x-requests-remaining | Same as x-requests-charged | Requests left on your account after this one. |
| x-ratelimit-limit | Once your key is accepted | Requests this key may make in one minute. |
| x-ratelimit-remaining | Once your key is accepted | Requests this key has left before the minute turns. |
| x-ratelimit-reset | Once your key is accepted | Seconds until the minute turns and the count starts again. |
| retry-after | 429, and 503 when Steam refused or every reader was busy | Seconds to wait before trying again. |
400, 401 and 402 answers come before your key’s use is counted, so they carry only x-request-id. A 200 looks like this:
HTTP/1.1 200 OK content-type: application/json x-request-id: req_5f0c9a1e7b2d4c6a8e3f1b90 x-requests-charged: 1 x-requests-remaining: 4999 x-ratelimit-limit: 120 x-ratelimit-remaining: 119 x-ratelimit-reset: 37
Rate limits
Each account may make 120 requests a minute, shared by all of its keys. The count starts again when the clock minute turns, and x-ratelimit-reset says how many seconds that is away.
- Every request with an accepted key counts, free ones included.
- Over the limit you get
429with aretry-afterheader. It is free: wait that many seconds and carry on. - Watch
x-ratelimit-remainingto slow down before you hit it. More keys do not add to the limit; a bot that needs more can ask us. - Private and empty inventories are free, up to 20 a minute, 120 an hour and 600 a day per account. Past that you get
429withretry-afteruntil the count falls. Pages you pay for never count towards it once they are answered, and neither do reads that failed on our side. A read still in progress does count, so keep to 20 or fewer at the same moment.
Billing
You pay in TF2 keys from your Key Bank: deposit keys in one trade offer, then spend them on requests at Dashboard, API, Buy. The more keys you spend at once, the more each key buys:
| Plan | Requests |
|---|---|
| Starter | 7,000 requests per TF2 key, from 1 key |
| Growth | 7,500 requests per TF2 key, when you spend 5 or more keys at once |
| Scale | 8,000 requests per TF2 key, when you spend 10 or more keys at once |
- One request is one answered page: up to 2,000 items, so up to 16,000,000 items read per TF2 key at the best rate.
- Free: private inventories (
403), people who own nothing in that game, refused or failed reads (502,503), and every400,401,402and429. - Requests never expire, and every key on your account shares them.
- Free trial: 100 requests, once per Steam account that has spent at least $5 on Steam. Claim them on Dashboard, API, Buy.
- Your dashboard shows your use by day and by key, and every charge with its request id.
Code examples
Each example reads a whole inventory: it pages until more_items is gone, retries the free errors after retry-after, and joins every item to its description. Set your key in the STEAMTRADEX_API_KEY environment variable rather than in the code.
# First page curl -i "https://steamtradex.com/api/inventory/76561198012345678/753/6?count=2000" \ -H "X-API-KEY: inv_your_key" # Next page: pass the last_assetid you got back curl -i "https://steamtradex.com/api/inventory/76561198012345678/753/6?count=2000&start_assetid=31846251911" \ -H "X-API-KEY: inv_your_key"
// Node 18 or newer: fetch is built in.
const KEY = process.env.STEAMTRADEX_API_KEY; // inv_...
const BASE = "https://steamtradex.com/api/inventory";
async function readPage(steamId, appId, contextId, startAssetId) {
const url = new URL(`${BASE}/${steamId}/${appId}/${contextId}`);
url.searchParams.set("count", "2000");
if (startAssetId) url.searchParams.set("start_assetid", startAssetId);
for (let attempt = 1; attempt <= 3; attempt++) {
const res = await fetch(url, { headers: { "X-API-KEY": KEY } });
if (res.ok) return res.json();
const body = await res.json().catch(() => ({}));
if (res.status === 403) throw new Error("That inventory is private.");
if (res.status === 402) throw new Error("No requests left: buy more at https://steamtradex.com/dashboard/api");
if (res.status === 429 || res.status === 502 || res.status === 503) {
const wait = Number(res.headers.get("retry-after") ?? 5);
await new Promise((r) => setTimeout(r, wait * 1000));
continue; // retries are free
}
throw new Error(`${res.status}: ${body.error} (${body.request_id})`);
}
throw new Error("Steam would not answer. Try again later.");
}
async function readInventory(steamId, appId = "753", contextId = "6") {
const assets = [];
const descriptions = new Map();
let start;
do {
const page = await readPage(steamId, appId, contextId, start);
assets.push(...page.assets);
for (const d of page.descriptions) descriptions.set(`${d.classid}_${d.instanceid}`, d);
start = page.more_items ? page.last_assetid : undefined;
} while (start);
// Join each item to its name, type and tags.
return assets.map((a) => ({ ...a, ...descriptions.get(`${a.classid}_${a.instanceid}`) }));
}
readInventory("76561198012345678").then((items) => {
console.log(items.length, "items");
console.log(items[0]?.market_hash_name);
});# pip install requests
import os, time, requests
KEY = os.environ["STEAMTRADEX_API_KEY"] # inv_...
BASE = "https://steamtradex.com/api/inventory"
def read_page(steam_id, app_id, context_id, start_assetid=None):
params = {"count": 2000}
if start_assetid:
params["start_assetid"] = start_assetid
for attempt in range(3):
r = requests.get(f"{BASE}/{steam_id}/{app_id}/{context_id}",
headers={"X-API-KEY": KEY}, params=params, timeout=60)
if r.ok:
return r.json()
if r.status_code == 403:
raise RuntimeError("That inventory is private.")
if r.status_code == 402:
raise RuntimeError("No requests left: buy more at https://steamtradex.com/dashboard/api")
if r.status_code in (429, 502, 503):
time.sleep(int(r.headers.get("retry-after", 5))) # retries are free
continue
body = r.json()
raise RuntimeError(f"{r.status_code}: {body.get('error')} ({body.get('request_id')})")
raise RuntimeError("Steam would not answer. Try again later.")
def read_inventory(steam_id, app_id="753", context_id="6"):
assets, descriptions, start = [], {}, None
while True:
page = read_page(steam_id, app_id, context_id, start)
assets += page["assets"]
for d in page["descriptions"]:
descriptions[(d["classid"], d["instanceid"])] = d
if not page.get("more_items"):
break
start = page["last_assetid"]
# Join each item to its name, type and tags.
return [{**a, **descriptions.get((a["classid"], a["instanceid"]), {})} for a in assets]
items = read_inventory("76561198012345678")
print(len(items), "items")<?php
$key = getenv('STEAMTRADEX_API_KEY'); // inv_...
$base = 'https://steamtradex.com/api/inventory';
function readPage($key, $base, $steamId, $appId, $contextId, $start = null) {
$query = ['count' => 2000];
if ($start) $query['start_assetid'] = $start;
$url = "$base/$steamId/$appId/$contextId?" . http_build_query($query);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["X-API-KEY: $key"],
CURLOPT_TIMEOUT => 60,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$json = json_decode($body, true);
if ($status !== 200) {
throw new Exception("$status: " . ($json['error'] ?? 'no answer') . ' (' . ($json['request_id'] ?? '') . ')');
}
return $json;
}
$assets = [];
$descriptions = [];
$start = null;
do {
$page = readPage($key, $base, '76561198012345678', 753, 6, $start);
$assets = array_merge($assets, $page['assets']);
foreach ($page['descriptions'] as $d) {
$descriptions[$d['classid'] . '_' . $d['instanceid']] = $d;
}
$start = !empty($page['more_items']) ? $page['last_assetid'] : null;
} while ($start);
echo count($assets) . " items\n";Try it
Send a real request from this page with your own key. It goes straight from your browser to our API, exactly as your bot would send it. Your key is not saved anywhere: close the page and it is gone.
OpenAPI
The whole API as an OpenAPI 3.1 file, built from the same figures as this page so the two always agree. Import it into Postman, Insomnia or Bruno to get every request ready to send, or feed it to a code generator.
https://steamtradex.com/docs/inventory-api/openapi.json
In Postman: Import, then paste the address above. Set the X-API-KEY value under the collection’s Authorization.