Skip to content
SteamLabs API

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:

JSON
{ "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:

JSON
{ "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:

JSON
{ "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.

GET/api/v1/inventory

Your items across every account, paginated and filterable.

API key required

Requires inventory.read.

Value summary

Use the summary endpoint to compare the filtered inventory at each market's latest reference prices. It accepts the same game, accounts, tags, venue, tradable, marketable, location, rarity, search, price_min, and price_max filters as the listing.

GET/api/v1/inventory/summary

Total item counts and inventory value by market price source.

API key required

Requires inventory.read.

JSON
{
    "data": {
        "total_items": 3542,
        "unique_items": 483,
        "values": {
            "steam_market": { "value_cents": 50255, "currency": "USD" },
            "csfloat": { "value_cents": 31944, "currency": "USD" },
            "marketcsgo": { "value_cents": 32518, "currency": "USD" }
        }
    }
}

An item without a price from one market contributes zero to that market's total. The other market totals still include it when their price exists.

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
groups array Account group ids you own. Items on accounts in any selected group
include_ungrouped boolean Include items held by accounts with no group
venue array steam, csfloat, marketcsgo, skinland, dmarket, skinport, assetpay. 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, protected for items Steam is holding under CS2 trade protection
rarity array CS2 grades by number, 1 to 7 (see below). Any of them matches
search string Matches the item's market hash name. Split on spaces, every word must appear, in any order
price_min number Lowest Steam Community Market price to include, in your preferred display currency. Unpriced items count as 0
price_max number Highest Steam Community Market price to include, in your preferred display currency. Must be at least 0
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.

location is a three-way split, so main means "free to act on right now". An item inside a storage unit is storage until you withdraw it, and an item Steam is holding under CS2 trade protection is protected until the hold ends. Both are yours, both count in total_items and in the summary's values, and neither is part of main.

rarity takes the grade numbers the Game Coordinator uses, which are also what each row returns in its rarity field. The same number covers the matching grade in every item family, so 6 returns Covert weapons and knives, Extraordinary gloves and stickers, and Master agents alike.

rarity Weapons and knives Agents Everything else
1 Consumer Grade Base Grade
2 Industrial Grade Industrial Grade
3 Mil-Spec Grade Distinguished High Grade
4 Restricted Exceptional Remarkable
5 Classified Superior Exotic
6 Covert Master Extraordinary
7 Contraband Contraband

A row's grade is the Game Coordinator's rarity when an inventory refresh has read it, otherwise the grade named in the item's Steam type ("Classified Rifle", "Base Grade Container"). Selecting any grade drops items that have neither: other games, and CS2 items whose type carries no grade.

Bash
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"

A flat row:

JSON
{
    "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 }
}

Trade protection is Steam's seven-day hold on a CS2 item that arrived in a trade. An item you have listed on the Steam Community Market sits in the same place on Steam's side, but it is not an inventory row here: a listing is a listing, and it would otherwise be counted twice. A held item stays in the account and keeps its value, but it cannot be traded, listed, used or moved into storage, so it comes back tradable: false, marketable: false, location: "protected" and context_id: 16. Sell, send, store and trade-up calls skip held items the way they skip stored ones. The end of the hold is on tradable_after, and the same instant is settlement_at on the trade that brought the item in (see Trading).

tradable_after is Steam's own end of the trade hold, for every game (CS2 items take it from the game coordinator, everything else from the web inventory). It is null when the item is unrestricted or when Steam gave no date, which for a lock that survives a refresh means it is permanent. Once that instant passes, tradable flips to true within a minute on its own; you do not need to refresh the inventory to see it.

A grouped row carries no asset id and no account, because a stack spans every account you own:

JSON
{
    "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, skinland, dmarket, skinport, assetpay) 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

GET/api/v1/inventory/storage-units

Your CS2 storage units, fullest first.

API key required

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, rarity, price_min, price_max) mean nothing for a container and are ignored here, exactly as the dashboard hides those controls on its units tab.

Bash
curl "https://dashboard.steamlabs.dev/api/v1/inventory/storage-units?sort=fill&per_page=1" \
  -H "Authorization: Bearer $STEAMLABS_API_KEY"
JSON
{
    "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

GET/api/v1/inventory/storage-units/{id}

One storage unit, with its known contents summary.

API key required

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

GET/api/v1/inventory/storage-units/{id}/contents

The items inside one storage unit.

API key required

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

POST/api/v1/inventory/storage-units/{id}/rename

Queue a rename for one storage unit.

API key required

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
Bash
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"}'
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

POST/api/v1/inventory/storage-units/{id}/withdraw

Pull items back out of one storage unit.

API key required

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.

Bash
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"}'
JSON
{
    "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

POST/api/v1/inventory/sell

List a selection on one selling venue.

API key required

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, marketcsgo, skinland, dmarket, skinport, or assetpay
mode string Pricing mode. Defaults to undercut. Validated per venue, see below. Omit it on skinland: that venue quotes its own price. On assetpay it decides whether the items are sold now or listed
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. On assetpay with instant_markup it is the percentage of AssetPay's instant price to ask, clamped to a minimum of 100
reference_source string assetpay with market_percent only. Which market's price percent is taken of: csfloat (default), steam_market or marketcsgo
instant_sell_after_days integer assetpay listing modes only. Days after which a listing that has not sold comes down and sells to AssetPay at the instant price, 1 to 365. Omit to keep the listing up until it sells. Only acts while the venue's instant sell unsold rule is on
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. Enough for a single catalog item when mode is manual
manual_prices_cents object Exact prices keyed by catalog item id. Required for a mixed selection when mode is manual: one cents value per distinct item. A single shared manual_price_cents cannot cover two different items
manual_currency string Steam only. The wallet currency the exact prices are quoted in, for example PLN
manual_convert boolean Steam only. Re-price the exact prices into each account's own wallet currency instead of skipping the accounts that hold another one. Default false
undercut_cents number Steps below the current lowest ask, in cents. Read by undercut mode on CSFloat, market.csgo, and DMarket. Ignored on Steam. Market.CSGO prices in tenths of a cent and accepts one decimal place, minimum 0.1; the other venues take whole cents, minimum 1
auto_reprice boolean Enroll the listings this call creates into automatic repricing. CSFloat, market.csgo, and DMarket, undercut mode only. Default false
auto_reprice_interval_minutes integer How often those listings are checked: 15, 30, 60, or 240. Defaults to the interval saved for that venue

Steam wallets each carry their own currency, so an exact price 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.

A mixed selection (two or more distinct catalog items) needs manual_prices_cents. One shared manual_price_cents is refused, because that would list different items at the same number. Every distinct item in the selection needs its own cents value. Copies of the same item share that item's price.

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.

AssetPay is the one venue that sells two ways from this endpoint. instant sells the items to AssetPay outright at the price it quotes at that moment, the way skinland does, and the response is a queued accept task per account. The others list the items on AssetPay's store instead: instant_markup asks percent of what AssetPay would pay for each item right now, market_percent asks percent of another market's price for the item (reference_source: csfloat by default, steam_market or marketcsgo; 1 to 500, lifted to the instant price if it lands below it, and items without a price from that market are skipped), and manual takes an exact price per catalog item. Listing is a local write, so those two answer with the listings created rather than with task ids, and the items appear on AssetPay within seconds. That answer is still a 202: task_id is null, task_ids is empty, and listings_created carries the count, beside the usual accounts_affected, items_queued and skipped. There is no undercut mode: SteamLabs is the only supplier of its own AssetPay pool, so there is no competing listing to undercut. A painted skin whose exact finish cannot be determined (a Doppler knife whose phase is unknown, for instance) is skipped and counted under skipped.no_paint; a fresh inventory refresh of the account fills it in.

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. On assetpay the same rule-based task sells now by default (mode: "instant") or lists the matching items on AssetPay's store with instant_markup or manual.

auto_reprice keeps the listings this call creates competitive after they go up. It supports CSFloat, market.csgo and DMarket and requires mode: "undercut"; any other pairing is a 422 on auto_reprice. Each listing captures its own absolute USD floor from floor_percent, so a listing whose venue reference price is missing is still created but is not enrolled. Send auto_reprice on its own and undercut_cents, floor_percent and auto_reprice_interval_minutes all fall back to the repricing defaults saved for that venue, the same ones the dashboard's repricing settings write. For the rule those listings end up with, and for changing it afterwards, see Manage automatic repricing.

Venue Modes
steam undercut, reference_percent, buy_order, manual
csfloat undercut, reference_percent, manual
marketcsgo undercut, reference_percent, top_bid, manual
dmarket undercut, reference_percent, top_bid, manual
skinport undercut, reference_percent, manual
skinland none. Omit mode. Sending one is a 422
assetpay instant (default), instant_markup, market_percent, manual

A mode the venue cannot honour is a 422. Only Steam prices against a buy order. top_bid on market.csgo and DMarket is instant sell into a sitting buy target. On DMarket, Steam-side items are deposited first, then Instant sell uses DMarket's instant-sale call at that price. An item with no instant price after deposit is skipped. It never falls back to a sitting listing.

Bash
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
      }'

One task per account, so 202 carries a list:

JSON
{
    "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, skinland, dmarket, skinport The account is not connected to that marketplace
no_proxy csfloat, marketcsgo, dmarket 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

POST/api/v1/inventory/send

Send a selection as trade offers.

API key required

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. An asset already committed to an outbound trade is also skipped, whether the send task is still queued or the offer is sitting unaccepted on Steam's side, so the same item can never ride in two offers. It becomes eligible again if the offer is declined, canceled, or expires.

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
Bash
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"
      }'
JSON
{
    "task_id": "019fb44d-6b21-7a90-84c2-1f0d9e6c4477",
    "accounts_affected": 14,
    "items_queued": 380,
    "items_skipped": 3
}

Unlike sell, this one has a batch parent: task_id is it, and accounts_affected is the number of sending accounts underneath. items_skipped counts the part of the selection that was not eligible (untradable, stored, listed, or already in a pending trade); when the whole selection is skipped you get 422 nothing_eligible instead.

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_group_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, target_group_ids or target_trade_urls. A rule whose action is route needs a strategy and at least one destination in any target field. fill_value and top_up_value also need a target_value. Every account, tag, and group target must belong to you.

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_empty when the rules route none of the selected items anywhere. Nothing is created, so there is no empty batch to find and cancel.
  • 422 invalid_destination when the destination has no usable trade URL. Refresh that account's trade details and try again.

Store items

POST/api/v1/inventory/store

Deposit a selection into CS2 storage units.

API key required

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
Bash
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" }
      }'
JSON
{
    "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

POST/api/v1/inventory/use

Consume a selection in place.

API key required

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.

Bash
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"}}'
JSON
{
    "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

POST/api/v1/inventory/trade-up

Stage a selection as trade-up contracts.

API key required

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.

Bash
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"]}}'
JSON
{
    "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

GET/api/v1/inventory/{id}

One asset, with everything the details view shows.

API key required

Requires inventory.read.

{id} is the asset row id, not Steam's asset_id. Returns a single flat row, bare, with no envelope.

Bash
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.

Refreshing the data on this page

Items land here after an account completes an inventory sync; nothing on this page reads Steam live. To bring accounts current from code, queue the inventory-only refresh with POST /api/v1/accounts/refresh-inventory (it takes an account selection, not an item selection, which is why it lives with the account actions). Scheduled automatic refreshes, including their per-account exclusions, are configured through PATCH /api/v1/settings/inventory-refresh.

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.