Endpoints
Inventory
List every item across every account, then sell, send, store, use, or trade up a selection.
Everything the Inventory page does, over HTTP: walk your items across every account, and run any of the page's selection actions against them. CS2 storage units and what is inside them live here too.
Two scopes cover the domain. inventory.read for the listings and lookups, inventory.write for the five bulk writes and the two storage unit writes.
Selecting items to act on
Five writes (sell, send, store, use, trade up) share one selection contract, in one of two shapes.
An explicit list of asset row ids, capped at 1,000:
{ "asset_ids": ["019fb42e-9a61-70d2-818a-f6a56593f3a5", "019fb42e-9a7e-728d-b960-8b4c2162898c"] }Or a description of the selection, using the same filter keys GET /api/v1/inventory accepts:
{ "filters": { "game": ["cs2"], "marketable": "sellable", "search": "case" } }Send exactly one of them. Both, or neither, is a 422: an empty body would otherwise mean "every item I own", which is the most expensive mistake this API could offer. Over the id cap you get 422:
{ "message": "Too many asset_ids in one request. Send at most 1000, or describe the selection with filters.", "code": "bulk_limit_exceeded", "max": 1000 }Ids in asset_ids are SteamLabs row ids (the id field on a listing row), not Steam's asset_id. Ids you do not own are dropped from the selection rather than refused, so they are never confirmed to exist.
Select-all caps
| Selection | Cap |
|---|---|
asset_ids |
1,000 ids per request |
filters |
The first 1,000 matching assets, ordered by id |
| Dashboard Select all in grouped mode | 500 stacks |
The 500 stack cap is the dashboard's own, on its grouped tab. Over the API you always act on assets, so 1,000 is the number that matters.
Both shapes queue worker tasks and both answer 202 Accepted. See Bulk operations for the wider contract.
Every write needs an Idempotency-Key
List inventory
One endpoint, three modes over the same filtered set.
/api/v1/inventoryYour items across every account, paginated and filterable.
Requires inventory.read.
Modes
mode |
One row is | Sorts (first is the default) |
|---|---|---|
flat (default) |
One Steam asset | recent, value, name, float |
grouped |
One catalog item, stacked across every account | recent, value, name, quantity |
units |
One CS2 storage unit | fill, name |
A sort that is not in the list for the mode you asked for is a 422, so a sort meant for one mode cannot silently fall back on another.
Parameters
| Field | Type | Description |
|---|---|---|
mode |
string | flat, grouped, or units. Anything else falls back to flat |
sort |
string | One of the mode's sorts above |
game |
array | cs2, tf2, steam. Any of them matches |
accounts |
array | Steam account ids you own, at most 1,000 |
tags |
array | Account tag ids you own. Items on accounts carrying any of them |
venue |
array | steam, csfloat, marketcsgo. Items sitting on accounts that can sell there right now |
tradable |
boolean | true or false |
marketable |
string | 1 marketable, 0 not marketable, sellable for marketable items on an account that could really list them (authenticator on file, market access confirmed) |
location |
string | main for the plain inventory, storage for items inside a storage unit |
search |
string | Matches the item's market hash name. Split on spaces, every word must appear, in any order |
page |
integer | 1-indexed, default 1 |
per_page |
integer | Default 50, clamped to 200 |
The array filters are real arrays: send game[]=cs2&game[]=tf2. A bare game=cs2 is a 422.
An account id or tag id you do not own is also a 422, not an empty page. "That is not yours" and "that account holds nothing" are different answers and you should not have to guess which you got.
venue is a property of the account, not of the item: it narrows to items held by accounts that are ready to sell on that venue.
Storage units never appear among the items in flat or grouped mode. A unit is a container, not an item. What is inside one does appear, and location=storage isolates it.
curl "https://dashboard.steamlabs.dev/api/v1/inventory?mode=flat&game[]=cs2&marketable=sellable&sort=value&per_page=2" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"items = requests.get(
"https://dashboard.steamlabs.dev/api/v1/inventory",
headers={"Authorization": f"Bearer {api_key}"},
params={
"mode": "flat",
"game[]": "cs2",
"marketable": "sellable",
"sort": "value",
"per_page": 200,
},
).json()const query = new URLSearchParams({
mode: 'flat',
'game[]': 'cs2',
marketable: 'sellable',
sort: 'value',
per_page: '200',
});
const response = await fetch(`https://dashboard.steamlabs.dev/api/v1/inventory?${query}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const { data, meta } = await response.json();A flat row:
{
"data": [
{
"id": "019fb42e-9a61-70d2-818a-f6a56593f3a5",
"asset_id": "38295610447",
"game": "cs2",
"context_id": 2,
"account": {
"id": "019fb42e-9a7e-728d-b960-8b4c2162898c",
"username": "farm_017",
"persona_name": "Ada"
},
"item": {
"id": "019fb430-1c22-73a4-9f0e-2b7c5d1e8a44",
"market_hash_name": "AK-47 | Redline (Field-Tested)",
"name": "AK-47 | Redline",
"image_url": "https://community.fastly.steamstatic.com/economy/image/…",
"type": "Classified Rifle"
},
"price_cents": 1842,
"tradable": true,
"tradable_after": null,
"marketable": true,
"location": "main",
"casket_id": null,
"reserved_marketplace": null,
"reserved_sold": false,
"custom_name": null,
"stattrak": false,
"kill_eater_value": null,
"float": 0.2317884,
"wear": "field_tested",
"paint_seed": 411,
"paint_index": 282,
"def_index": 7,
"origin": 8,
"quality": 4,
"rarity": 5,
"stickers": [
{ "stickerId": 5032, "wear": 0.12 }
],
"keychains": [],
"acquired_at": "2026-07-12T08:41:03+00:00"
}
],
"meta": { "page": 1, "per_page": 2, "total": 4182, "last_page": 2091 }
}A grouped row carries no asset id and no account, because a stack spans every account you own:
{
"data": [
{
"item": {
"id": "019fb430-2d71-71bc-8a0f-4c1e0f9b7a12",
"market_hash_name": "Fever Case",
"name": "Fever Case",
"image_url": "https://community.fastly.steamstatic.com/economy/image/…",
"type": "Base Grade Container",
"game": "cs2"
},
"quantity": 1284,
"tradable_quantity": 1180,
"marketable_quantity": 1284,
"listed_quantity": 12,
"sold_quantity": 3,
"unit_price_cents": 61,
"stack_value_cents": 78324,
"last_acquired_at": "2026-07-30T09:12:44+00:00"
}
],
"meta": { "page": 1, "per_page": 50, "total": 137, "last_page": 3 }
}price_cents and unit_price_cents are the Steam Community Market price, whatever venue you end up selling on. It is the one number every inventory surface in the product values items in, and null for an item nobody has priced yet.
reserved_marketplace is non-null while another venue holds the asset (steam, csfloat, marketcsgo) or while it is staged as a trade-up input (trade_up). Sell, send and store all skip reserved assets.
reserved_sold splits that reservation in two. On CSFloat and market.csgo an item you sell stays in your Steam inventory until the buyer accepts the delivery trade, so a sold asset is still listed by this endpoint: reserved_sold: false means a live listing, reserved_sold: true means the sale already happened and delivery is in flight. Treat both as spoken for. It is always false when reserved_marketplace is null or trade_up.
On a grouped row the same split is counted across the stack: listed_quantity is how many of its assets sit on a live marketplace listing and sold_quantity how many are sold and awaiting delivery. Trade-up staging counts toward neither.
List storage units
/api/v1/inventory/storage-unitsYour CS2 storage units, fullest first.
Requires inventory.read.
| Field | Type | Description |
|---|---|---|
sort |
string | fill (default, fullest first) or name |
accounts |
array | Steam account ids you own |
tags |
array | Account tag ids you own |
search |
string | Matches the unit's own name or its catalog name |
page |
integer | 1-indexed, default 1 |
per_page |
integer | Default 50, clamped to 200 |
The item-level filters (game, tradable, marketable, location, venue) mean nothing for a container and are ignored here, exactly as the dashboard hides those controls on its units tab.
curl "https://dashboard.steamlabs.dev/api/v1/inventory/storage-units?sort=fill&per_page=1" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"{
"data": [
{
"id": "019fb431-2c88-71ea-b0a3-8ce2f5d19b07",
"asset_id": "40118273905",
"name": "Cases 04",
"custom_name": "Cases 04",
"account": {
"id": "019fb42e-9a7e-728d-b960-8b4c2162898c",
"username": "farm_017",
"persona_name": "Ada"
},
"item_count": 940,
"capacity": 1000,
"space_left": 60,
"known_contents": 940,
"known_value_cents": 61420,
"ready_for_deposits": true
}
],
"meta": { "page": 1, "per_page": 1, "total": 12, "last_page": 12 }
}item_count is the count the CS2 Game Coordinator reports for the unit itself. known_contents is how many of those rows we have actually synced, so the two differ until a sync has read inside the unit.
One storage unit
/api/v1/inventory/storage-units/{id}One storage unit, with its known contents summary.
Requires inventory.read.
{id} is the unit's row id, the id field above. Returns the same object, bare, with no envelope. A unit belonging to someone else returns 404.
Storage unit contents
/api/v1/inventory/storage-units/{id}/contentsThe items inside one storage unit.
Requires inventory.read.
| Field | Type | Description |
|---|---|---|
sort |
string | value (default, priciest first), name, or float (lowest first, items without a float last) |
search |
string | Matches the item's market hash name, word by word |
page |
integer | 1-indexed, default 1 |
per_page |
integer | Default 50, clamped to 200 |
Rows are the same shape as a flat inventory row, with location set to storage and casket_id set to the unit's asset_id.
A unit holds at most 1,000 items, so this is the one listing in the domain whose whole result set is bounded by the game. It is still paginated.
Rename a storage unit
/api/v1/inventory/storage-units/{id}/renameQueue a rename for one storage unit.
Requires inventory.write and an Idempotency-Key header.
| Field | Type | Description |
|---|---|---|
name |
string | The new name, 1 to 20 characters. Twenty is the Game Coordinator's own limit, so a longer name is refused here rather than truncated by the game |
curl -X POST "https://dashboard.steamlabs.dev/api/v1/inventory/storage-units/019fb431-2c88-71ea-b0a3-8ce2f5d19b07/rename" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name": "Cases 04"}'$response = Http::withToken($apiKey)
->withHeaders(['Idempotency-Key' => (string) Str::uuid()])
->post('https://dashboard.steamlabs.dev/api/v1/inventory/storage-units/019fb431-2c88-71ea-b0a3-8ce2f5d19b07/rename', [
'name' => 'Cases 04',
])
->json();{
"task_id": "019fb44a-51d0-7238-9c1e-77a1f0c2b9d4",
"accounts_affected": 1
}The name lives in the Game Coordinator, not in our database, so this is a worker task rather than an edit. Poll GET /api/v1/tasks/{id} to follow it.
Withdraw from a storage unit
/api/v1/inventory/storage-units/{id}/withdrawPull items back out of one storage unit.
Requires inventory.write and an Idempotency-Key header.
This is the one write on the page that does not take the shared selection contract. A withdraw is scoped to a single container that holds at most 1,000 rows, so there is no fleet-sized selection to describe.
| Field | Type | Description |
|---|---|---|
asset_ids |
array | Row ids inside this unit. Omit it to withdraw everything matching search |
search |
string | Narrows to items whose market hash name matches, word by word |
An empty body means "empty this unit", which is a reasonable thing to ask a container to do and is bounded by its capacity.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/inventory/storage-units/019fb431-2c88-71ea-b0a3-8ce2f5d19b07/withdraw" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"search": "Fever Case"}'{
"task_id": "019fb44b-8ac3-70f5-b2d1-0a6e3f5c81aa",
"accounts_affected": 1,
"items_queued": 240
}No plan can refuse a withdraw. Getting your own items back out of a container is not a paywall.
Sell items
/api/v1/inventory/sellList a selection on one selling venue.
Requires inventory.write and an Idempotency-Key header.
Takes the selection contract plus a venue and its pricing. Only marketable assets in the main inventory that are not already reserved by another venue's listing are eligible.
| Field | Type | Description |
|---|---|---|
venue |
string | steam, csfloat, or marketcsgo |
mode |
string | Pricing mode. Defaults to undercut. Validated per venue, see below |
percent |
number | Percentage of the reference price, 1 to 500. Required when mode is reference_percent. Above 100 is allowed: listing above the reference is a real strategy, just not the common one |
floor_percent |
number | Never undercut below this share of the reference price, 1 to 100. Dropped when mode is manual, which has nothing to clamp |
manual_price_cents |
integer | The exact price, in cents, at least 1. Required when mode is manual |
manual_currency |
string | Steam only. The wallet currency manual_price_cents is quoted in, for example PLN |
manual_convert |
boolean | Steam only. Re-price manual_price_cents into each account's own wallet currency instead of skipping the accounts that hold another one. Default false |
undercut_cents |
integer | Steps below the current lowest ask, in cents. Only means anything on market.csgo |
Steam wallets each carry their own currency, so manual_price_cents on its own is a bare number: 707 lists as 7.07 zł on a PLN wallet and $7.07 on a USD one. Send manual_currency to say which you meant, and any account whose live Steam wallet is a different currency is skipped with reason wallet_currency_mismatch rather than listed at the wrong number. Omitting it keeps the older behaviour of listing the number as-is against whatever currency the wallet holds.
manual_convert decides what happens to the accounts that hold a different currency. Left false, every listing shows exactly the number you sent and the rest are skipped. Set true, nothing is skipped: the number is treated as a value to match and re-priced into each wallet, so the listed figures are conversions rather than the number you typed.
To put one exact price on many accounts by rule instead of by hand-picked assets, POST /api/v1/tasks with type: "sell_items" and mode: "manual" takes the same fields. See sell_items.
| Venue | Modes |
|---|---|
steam |
undercut, reference_percent, buy_order, manual |
csfloat |
undercut, reference_percent, manual |
marketcsgo |
undercut, reference_percent, top_bid, manual |
A mode the venue cannot honour is a 422. Only Steam prices against a buy order, and only market.csgo takes the top bid.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/inventory/sell" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"filters": { "game": ["cs2"], "marketable": "sellable", "search": "Fever Case" },
"venue": "steam",
"mode": "reference_percent",
"percent": 98,
"floor_percent": 80
}'import uuid
response = requests.post(
"https://dashboard.steamlabs.dev/api/v1/inventory/sell",
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"asset_ids": ["019fb42e-9a61-70d2-818a-f6a56593f3a5"],
"venue": "csfloat",
"mode": "undercut",
"floor_percent": 85,
},
).json()const response = await fetch('https://dashboard.steamlabs.dev/api/v1/inventory/sell', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
asset_ids: assetIds,
venue: 'marketcsgo',
mode: 'top_bid',
}),
});One task per account, so 202 carries a list:
{
"task_id": "019fb44c-1f92-7461-8d3b-9c0a2e771b55",
"accounts_affected": 18,
"task_ids": [
"019fb44c-1f92-7461-8d3b-9c0a2e771b55",
"019fb44c-1f93-7c08-a51e-3e7d4b9f2201"
],
"items_queued": 640,
"skipped": {
"accounts": 2,
"no_secret": 1,
"restricted": 0,
"busy": 1,
"non_cs2": 0
}
}task_id is the first task, so a client written against the common shape has something to follow. task_ids is the real list. There is no batch parent above them: they are separate pieces of work on separate accounts, and the dashboard does not invent one either.
The skipped keys depend on the venue:
| Key | Venue | Meaning |
|---|---|---|
accounts |
all | Total accounts skipped, the sum of the rest |
no_secret |
all | No mobile authenticator on file, so listings cannot be confirmed |
restricted |
all | The account's market access is restricted right now |
busy |
all | That account already has a listing batch queued or running |
non_cs2 |
all | Non-CS2 items in the selection, dropped rather than refused. Always 0 for Steam, which takes both games |
not_connected |
csfloat, marketcsgo |
The account is not connected to that marketplace |
no_proxy |
csfloat, marketcsgo |
The account has no proxy, which that venue requires |
currency |
marketcsgo |
The account's wallet currency is not one market.csgo accepts |
If nothing survived, you get 422 nothing_eligible with the same skipped block rather than an accepted response with zero tasks.
If your plan does not include the venue, you get 403 plan_limit_reached. The entitlement is allowed_marketplaces, which GET /api/v1/me reports up front.
Send items
/api/v1/inventory/sendSend a selection as trade offers.
Requires inventory.write and an Idempotency-Key header.
Takes the selection contract plus a destination. Only tradable assets in the main inventory that are not reserved by a marketplace listing are eligible: a trade offer carrying the rest would be rejected by Steam anyway.
| Field | Type | Description |
|---|---|---|
destination_type |
string | own (another of your accounts), external (a trade URL), or routing (spread over rules) |
destination_account_id |
uuid | The receiving account. Required when destination_type is own, and it must be yours |
trade_url |
string | A Steam trade offer URL carrying partner and token. Required when destination_type is external. Anything else is refused, not merely checked for being a URL |
rules_source |
string | template (default) or custom. Required when destination_type is routing |
routing_template_id |
uuid | A saved routing template of yours. Required when routing from a template |
routing_rules |
array | Inline rules, at least one. Required when routing with rules_source: custom |
auto_accept |
boolean | Accept the offer on the receiving side. Defaults to true, and only applies to your own accounts: an external partner accepts on their own side |
message |
string | The note on the offer, at most 128 characters, which is Steam's own limit |
curl -X POST "https://dashboard.steamlabs.dev/api/v1/inventory/send" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"filters": { "game": ["cs2"], "tradable": true, "location": "main" },
"destination_type": "own",
"destination_account_id": "019fb42e-9a7e-728d-b960-8b4c2162898c",
"auto_accept": true,
"message": "consolidating"
}'response = requests.post(
"https://dashboard.steamlabs.dev/api/v1/inventory/send",
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"asset_ids": asset_ids,
"destination_type": "external",
"trade_url": "https://steamcommunity.com/tradeoffer/new/?partner=39734289&token=AbCd1234",
},
).json(){
"task_id": "019fb44d-6b21-7a90-84c2-1f0d9e6c4477",
"accounts_affected": 14,
"items_queued": 380
}Unlike sell, this one has a batch parent: task_id is it, and accounts_affected is the number of sending accounts underneath.
Inline routing_rules follow the routing builder's own shape, and are validated key by key exactly as a saved template is. Each rule is an object of optional conditions (games, price_min, price_max, item_ids, categories, origins), targets (target_account_ids, target_tag_ids, target_trade_urls), a required action (route or skip), a strategy (round_robin, random, fill_value, top_up_value), and optional target_value and items_limit_per_target. Rules are first-match-wins, and an item matching no rule is left where it is.
At most 50 rules per request, and at most 1000 entries in any one of item_ids, target_account_ids, target_tag_ids or target_trade_urls. A rule whose action is route needs a strategy and at least one destination in any of the three target fields; fill_value and top_up_value additionally need a target_value. Every entry in target_trade_urls has to be a real Steam trade offer URL (https://steamcommunity.com/tradeoffer/new/?partner=...&token=...), and every target_account_ids and target_tag_ids entry has to name an account or account tag you own. Anything else is a 422 naming the rule index that failed.
Tag targets are resolved to their member accounts when the plan runs, so a rule aimed at a tag picks up accounts tagged after the rule was written. See Trading for the full rule reference.
Two failures are specific to this endpoint:
422 routing_plan_emptywhen the rules route none of the selected items anywhere. Nothing is created, so there is no empty batch to find and cancel.422 invalid_destinationwhen the destination has no usable trade URL. Refresh that account's trade details and try again.
Store items
/api/v1/inventory/storeDeposit a selection into CS2 storage units.
Requires inventory.write and an Idempotency-Key header.
Takes the selection contract plus one unit per account. A storage unit only ever holds its own account's items, so a selection spanning five accounts is five deposits and there is no single destination to name.
| Field | Type | Description |
|---|---|---|
units |
object | Steam account id to storage unit row id. At least one entry |
curl -X POST "https://dashboard.steamlabs.dev/api/v1/inventory/store" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"asset_ids": ["019fb42e-9a61-70d2-818a-f6a56593f3a5"],
"units": { "019fb42e-9a7e-728d-b960-8b4c2162898c": "019fb431-2c88-71ea-b0a3-8ce2f5d19b07" }
}'{
"task_id": "019fb44e-2d55-7c11-91a7-6b3e0a4d8812",
"accounts_affected": 3,
"task_ids": [
"019fb44e-2d55-7c11-91a7-6b3e0a4d8812",
"019fb44e-2d56-7f02-b8de-4a2c1e97f5b0",
"019fb44e-2d57-70a4-8e19-2c9b7d3f6641"
],
"items_queued": 812,
"skipped": {
"no_unit": 40,
"non_cs2": 12,
"not_depositable": 5,
"unit_busy": 0,
"clamped": 60
}
}| Key | Meaning |
|---|---|
no_unit |
Items on an account you named no unit for, or whose unit does not belong to that account |
non_cs2 |
Non-CS2 items. Only the CS2 Game Coordinator moves items into a unit |
not_depositable |
Items already inside a unit, and the units themselves |
unit_busy |
Items bound for a unit that already has a move queued or running |
clamped |
Items that did not fit in the room the unit had left |
clamped is not a refusal. Those items were fine, the container was full: a unit holds 1,000 items and the overflow is cut here rather than disappearing silently at the Game Coordinator.
A unit runs one move at a time, in either direction. The Game Coordinator handles a single move per container, and two overlapping ones also clamp against the same contained-item count (that figure only changes when the unit syncs back), so a second deposit could overflow a container the first was already filling. A unit with a move in flight is therefore skipped and reported under unit_busy rather than queued behind it. The rest of the batch still goes: one busy container does not fail the other accounts. Retry those items once the move you can see in task_ids has finished.
Unit ownership is not validated up front. A unit id belonging to another account resolves to nothing and its items come back under no_unit, because the constraint that matters is that the unit belongs to the account whose items are going in, and a validation rule cannot check that.
Use items
/api/v1/inventory/useConsume a selection in place.
Requires inventory.write and an Idempotency-Key header.
Takes the selection contract and nothing else. There is no option to give: what an asset becomes is decided by the item itself.
Two things are usable, and the selection is narrowed to them before anything runs:
- TF2 backpack expanders and the other usable TF2 store items.
- CS2 armory passes.
Items inside a storage unit are excluded. Nothing can be used where it sits in a container.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/inventory/use" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"filters": {"game": ["cs2"], "search": "Armory Pass"}}'{
"task_id": "019fb44f-9e07-7b6d-a3f8-5d1c8b02e4a9",
"accounts_affected": 22,
"task_ids": ["019fb44f-9e07-7b6d-a3f8-5d1c8b02e4a9"],
"items_queued": 22,
"skipped": { "ineligible": 0 },
"plan_limited": []
}items_queued counts only what a task was really created for, and task_ids always names one id per account counted in accounts_affected. An account that became ineligible between being selected and being queued produces no task, and its items are reported under skipped.ineligible instead. If every account falls out that way you get 422 nothing_eligible carrying the same block, never an accepted response naming a task that does not exist.
The task types behind the two halves are use_tf2_items and activate_armory_passes. Both appear in your plan's allowed_task_types.
Trade up items
/api/v1/inventory/trade-upStage a selection as trade-up contracts.
Requires inventory.write and an Idempotency-Key header.
Takes the selection contract and nothing else. The planner groups each account's eligible CS2 inputs by tier and StatTrak and forms as many ten-item contracts as they yield. Leftovers are not consumed, so there is no knob to turn.
curl -X POST "https://dashboard.steamlabs.dev/api/v1/inventory/trade-up" \
-H "Authorization: Bearer $STEAMLABS_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"filters": {"game": ["cs2"], "accounts": ["019fb42e-9a7e-728d-b960-8b4c2162898c"]}}'{
"task_id": "019fb450-3c48-71d9-b077-8e5a2f10c9b3",
"accounts_affected": 6,
"skipped": { "ineligible_accounts": 1 }
}Non-CS2 assets in the selection are ignored. If nothing forms a complete contract you get 422 nothing_eligible. If your plan does not include the trade_up_contract task type you get 403 plan_limit_reached.
Staged inputs come back on the listing with reserved_marketplace: "trade_up", and the sell, send and store planners skip them from then on. For the contracts themselves, their scans and their outcomes, see Trade-ups.
One item
/api/v1/inventory/{id}One asset, with everything the details view shows.
Requires inventory.read.
{id} is the asset row id, not Steam's asset_id. Returns a single flat row, bare, with no envelope.
curl "https://dashboard.steamlabs.dev/api/v1/inventory/019fb42e-9a61-70d2-818a-f6a56593f3a5" \
-H "Authorization: Bearer $STEAMLABS_API_KEY"An id belonging to someone else returns 404, the same as an id that does not exist. Telling the two apart would confirm the row exists.
Errors
Beyond the universal codes, this group returns:
| Code | Status | Meaning |
|---|---|---|
nothing_eligible |
422 |
Nothing in the selection could be acted on. Sell, store and use add a skipped breakdown |
routing_plan_empty |
422 |
Send with routing rules that route none of the selected items anywhere |
invalid_destination |
422 |
Send to a destination with no usable trade URL |
move_already_running |
409 |
A deposit or withdraw is already queued or running for that storage unit |
bulk_limit_exceeded |
422 |
More than 1,000 asset_ids. Adds max |
maintenance_mode |
503 |
Platform maintenance pauses every creating endpoint in this group. Adds reason. Transient, see Errors |
403 plan_limit_reached comes from seven places here, each naming a different entitlement:
| Endpoint | Entitlement |
|---|---|
POST /api/v1/inventory/sell |
allowed_marketplaces does not include the venue |
POST /api/v1/inventory/send |
allows_trade_sending is off (routing additionally needs the distribute_items task type) |
POST /api/v1/inventory/store |
allowed_task_types does not include store_items |
POST /api/v1/inventory/use |
allowed_task_types covers neither use_tf2_items nor activate_armory_passes |
POST /api/v1/inventory/trade-up |
allowed_task_types does not include trade_up_contract |
POST /api/v1/inventory/storage-units/{id}/rename |
allowed_task_types does not include rename_storage_unit |
POST /api/v1/inventory/storage-units/{id}/withdraw |
allowed_task_types does not include withdraw_items |
Depositing and withdrawing spend the same entitlements here as the equivalent bulk tasks, so the surgical endpoints and the task queue can never disagree about what your plan covers.