Skip to content
API

Inventory prices

Steam inventory price API with source-level values

Each inventory item includes the selected USD price, all known source prices and an update timestamp. The response total uses item count and sellable state; market volume is never treated as inventory quantity.

GET /v1/inventory?price_source=steamBearer authentication

Updated

curl
curl --fail-with-body \
  -H "Authorization: Bearer $WOK_API_KEY" \
  "https://woksteamapi.com/v1/inventory?steam_id=76561198090744629&game=cs2&top=10&price_source=steam"
Source controlper-request mode and strict fallback
USDper-item and inventory totals
Timestampssource update metadata
Repriceno repeated inventory scan

Choose what your inventory value means

A public inventory tells you what a player owns. The chosen pricing mode determines which catalog observations value those items.

Request choiceSelection ruleWhat to check
price_source=autoPrefer backpacktf, steam_avg, steam, steam_market, skinport, then tm when available after price validation.Explicit auto overrides a key's saved source. It is not the average or lowest quote; BUFF and instant-sell buy orders are excluded.
price_source=steam&strict=1Use the Steam family: steam_avg, steam, then steam_market.Strict disables other-family fallback; it does not force one exact Steam feed.
price_source=buff&strict=1Use a matched BUFF163 CS2 price only.Unmatched items stay null and contribute to unpriced. With strict=0, other market sources may fill gaps.
price_source=skins&strict=1Use a matched Skins.com CS2 lowest listing only.Unmatched items stay null and contribute to unpriced. Check priceupdatedat on every priced item.
price_source=waxpeer&strict=1Use a matched Waxpeer lowest listing for CS2, Dota 2, Rust or TF2.Missing listings stay unpriced. WOK saves its own USD observation time.
price_source=49skins&strict=1Use a matched 49Skins CS2 listing converted from EUR to USD.Only currently listed items appear; marketplace checkout fees are not included.
price_source=csdeals&strict=1Use a matched CS.DEALS CS2 lowest listing when its feed is configured.Missing or unavailable provider observations stay unpriced.
price_source=haloskins&strict=1Use a matched HaloSkins CS2 lowest in-stock listing when its feed is configured.Only P2P or bot listings with stock are considered.
price_source=mixUse the lowest accepted market observation, including fresh third-party listing feeds.Stale third-party observations and grossly underpriced outliers are excluded; steam_buy remains separate. A listing floor is not a realized sale price.
price_stat=min|max|avg|medianWithout an explicit source, compare recent lowest asks across independent marketplaces. With price_source, use only a reported metric from that provider.price_statistics.source_count gives coverage. Per-provider maximum and average are null unless the provider supplies them. This is not a range over every active lot.

Always read status, unpriced, items[].pricesource and items[].priceupdatedat. For a priced item, pricemedian × count is its grouped value; volume is market metadata, not inventory quantity. Keep total, untradable_value and tradelocked_value separate rather than adding them into an unlabeled estimate.

Normal OK/EMPTY cache hits reapply current local prices while keeping the existing inventory snapshot and TTL. Changing price_source does not request a provider refresh. A recent inventory scan can still have an older price observation; show those two ages separately.

Quote items without a SteamIDRefresh only cached inventory pricesCompare WOK with SteamWebAPIValidate your existing integration

Why listing floors and medians give different inventory values

The same quantities can produce different totals when the chosen statistic changes. Choose the measure your application needs before comparing two inventory estimates.

Set WOK_API_KEY and STEAM_ID in your server environment. These requests use the same player, game and explicit provider; only price_stat changes. strict=1 prevents another source from filling gaps, and top=0 returns all grouped rows for reconciliation.

Skinport listing-floor inventory value
curl --fail-with-body --max-time 70 \
  -H "Authorization: Bearer $WOK_API_KEY" \
  --get "https://woksteamapi.com/v1/inventory" \
  --data-urlencode "steam_id=$STEAM_ID" \
  --data-urlencode "game=cs2" \
  --data-urlencode "price_source=skinport" \
  --data-urlencode "price_stat=min" \
  --data-urlencode "strict=1" \
  --data-urlencode "top=0" \
  --data-urlencode "currency=USD"
Skinport listing-median inventory value
curl --fail-with-body --max-time 70 \
  -H "Authorization: Bearer $WOK_API_KEY" \
  --get "https://woksteamapi.com/v1/inventory" \
  --data-urlencode "steam_id=$STEAM_ID" \
  --data-urlencode "game=cs2" \
  --data-urlencode "price_source=skinport" \
  --data-urlencode "price_stat=median" \
  --data-urlencode "strict=1" \
  --data-urlencode "top=0" \
  --data-urlencode "currency=USD"

min requests Skinport's reported listing floor. For OpenSkin-derived Skinport observations, a source_statistics.skinport.basis value of active_listing_statistics means median describes sellers' listing prices. It can be higher than the lowest offer; it is not a completed-sale median or expected seller proceeds. An unavailable provider statistic remains a null item price.

Illustrative arithmetic only: fictional items and USD prices, not live quotes
Illustrative rowCountFloor per itemMedian per item
Example item A2$10$16
Example item B1$5$9
Total for three eligible units$25$41

For priced, tradable and marketable rows, multiply pricemedian by count, then sum. In the floor response this is 2 × 10 + 1 × 5 = 25; in the median response it is 2 × 16 + 1 × 9 = 41. The field name pricemedian holds the chosen unit statistic; volume is not an owned quantity. Attached-sticker values and separate estimated_total craft estimates are not added to total.

Check status before valuing a response. Private, missing or failed inventories are unavailable, not zero-valued. Check unpriced and null row prices before comparing coverage; a partial total is not a complete inventory estimate. Compare the exact names, counts and eligibility flags in both responses.

Keep meta.price_source and meta.price_stat with the value. meta.cache_age describes inventory membership; items[].source_statistics.skinport.updated_at describes the saved price observation. Two separate calls consume separate API quota and can observe changed data. A source timestamp does not prove current ownership, and a listing quote does not guarantee availability or sale proceeds.

Use the inventory cache and refresh workflow when you need to revalue saved membership. For individual items rather than an owned inventory, use the item price API. The price-statistic integration reference covers cross-market statistics and provider fields.

Audit floor and listing median from one saved response

Compare the same player's quantities, eligibility flags and saved price observations without making two inventory requests.

Set WOK_API_KEY and a numeric STEAM_ID on your server, then capture this one native response. Normal quota rules apply; a cache miss can require several Steam pages or transport attempts. The Python step reads only your saved JSON and makes no additional API or provider requests.

Capture one complete native inventory (curl)
umask 077
curl --fail-with-body --max-time 70 --max-filesize 16777216 \
  -H "Authorization: Bearer $WOK_API_KEY" \
  --get "https://woksteamapi.com/v1/inventory" \
  --data-urlencode "steam_id=$STEAM_ID" \
  --data-urlencode "game=cs2" \
  --data-urlencode "price_source=skinport" \
  --data-urlencode "strict=1" \
  --data-urlencode "top=0" \
  --data-urlencode "currency=USD" \
  --data-urlencode "format=json" \
  --output inventory.json

Run the audit only after curl exits successfully. Do not add search, select, limit, a cursor or redirect/retry options. HTTP success alone is not a usable inventory: the script checks the expected SteamID64, game, explicit inventory status, USD, item flags and items_total = sum(items[].count) before reporting a comparison. A top-limited or inconsistent response fails instead of becoming a smaller inventory.

Python audit of the saved response
Offline same-response audit (Python standard library)
# Save as audit_snapshot.py; read one saved response, never make an HTTP request.
import json, math, sys, time
from collections import Counter
from decimal import Decimal

def need(condition, message):
    if not condition:
        raise SystemExit("Audit unavailable: " + message)

def finite(value):
    try:
        return type(value) in (int, float) and math.isfinite(value)
    except OverflowError:
        return False

need(len(sys.argv) == 4, "use audit_snapshot.py inventory.json STEAM_ID cs2")
filename, expected_id, game = sys.argv[1:]
need(len(expected_id) == 17 and expected_id.isascii() and expected_id.isdigit(), "expected SteamID64")
with open(filename, "rb") as saved:
    raw = saved.read(16 * 1024 * 1024 + 1)
need(len(raw) <= 16 * 1024 * 1024, "saved response exceeds this example's 16 MiB limit")
try:
    data = json.loads(raw)
except (ValueError, UnicodeError):
    raise SystemExit("Audit unavailable: invalid saved JSON") from None
need(isinstance(data, dict), "expected a native inventory object")
need(data.get("steamid") == expected_id and data.get("game") == game, "player or game mismatch")
need(data.get("status") in ("ok", "empty") and data.get("currency") == "USD", "unavailable status or currency")
rows, meta = data.get("items"), data.get("meta")
need(isinstance(rows, list) and isinstance(meta, dict), "missing items or metadata")
need(meta.get("price_source") == "skinport" and meta.get("strict", True) is True, "source or strict mismatch")
age, ttl, now = meta.get("cache_age"), meta.get("cache_ttl"), time.time()
need(finite(age) and finite(ttl) and 0 <= age <= 300 and age < ttl, "inventory age at response exceeds policy")
page = data.get("pagination")
need(page is None or isinstance(page, dict) and page.get("has_more") is False
     and page.get("offset") == 0 and page.get("next_cursor") is None, "partial page")
quantity = eligible = compared = 0
floor = median = Decimal("0")
excluded = Counter()
for row in rows:
    need(isinstance(row, dict) and row.get("game") == game, "item game or shape mismatch")
    count, tradable, marketable = row.get("count"), row.get("tradable"), row.get("marketable")
    need(type(count) is int and count > 0, "invalid owned count")
    need(all(type(flag) in (bool, int) and flag in (0, 1) for flag in (tradable, marketable)), "invalid eligibility flags")
    quantity += count
    if not (tradable and marketable):
        continue
    eligible += count
    sources = row.get("source_statistics", {})
    need(isinstance(sources, dict), "invalid source statistics")
    metric = sources.get("skinport", {})
    need(isinstance(metric, dict), "invalid Skinport statistics")
    low, mid, stamp = metric.get("min"), metric.get("median"), metric.get("updated_at")
    reason = ("missing_pair" if low is None or mid is None else
              "invalid_or_zero_pair" if not all(finite(v) and 0 < v < 100000 for v in (low, mid)) else
              "unsupported_basis_or_currency" if metric.get("basis") != "active_listing_statistics" or metric.get("currency") != "USD" else
              "stale_or_unknown_price_age" if not finite(stamp) or not 0 < stamp <= now or now - stamp > 21600 else None)
    if reason:
        excluded[reason] += count
        continue
    compared += count
    floor += Decimal(str(low)) * count
    median += Decimal(str(mid)) * count
need(type(data.get("items_total")) is int and data["items_total"] == quantity, "truncated or inconsistent item counts")
need(data["status"] != "empty" or quantity == 0, "empty status has items")
print(json.dumps({
    "scope": "same_response_common_priced_subset", "status": data["status"],
    "comparison": "available_subset" if compared else "no_comparable_prices",
    "inventory_quantity": quantity, "eligible_quantity": eligible, "compared_quantity": compared,
    "excluded_ineligible_quantity": quantity - eligible, "excluded_eligible_quantity": dict(excluded),
    "unpriced_for_comparison_quantity": excluded["missing_pair"] + excluded["invalid_or_zero_pair"],
    "common_floor_subtotal_usd": format(floor, ".2f") if compared else None,
    "common_listing_median_subtotal_usd": format(median, ".2f") if compared else None,
    "inventory_age_at_response_seconds": age, "price_age_limit_seconds": 21600,
}, sort_keys=True))

Save the Python code as audit_snapshot.py, then run python3 audit_snapshot.py inventory.json "$STEAM_ID" cs2. It reads at most 16 MiB and never reads your API key. Treat validation errors as unavailable data; this focused exercise is not a full JSON Schema validator or proof of response authenticity.

Both subtotals use the same tradable-and-marketable rows with a usable positive floor and median. They multiply each unit price by owned count, never volume. compared_quantity, excluded quantities and unpriced_for_comparison_quantity expose coverage; the latter counts missing, malformed and rounded-zero price pairs, rather than copying the API's selected-price unpriced field. No comparable rows means null subtotals, not a zero-value inventory. Even available subtotals describe only that common priced subset.

This listing comparison requires source_statistics.skinport.currency="USD" and basis="active_listing_statistics". A generic provider_reported basis does not establish the listing-versus-sale meaning needed here and is excluded. This conservative client check does not change WOK's price-source selection. Missing fields and rejected rows are never filled from another source.

The example's age policy requires inventory cache_age at most 300 seconds and below cache_ttl when the response was captured, plus a nonfuture price updated_at no older than six hours at audit time. These are client example limits, not a global source TTL promise. An old saved file does not prove current ownership; running it again cannot refresh membership. Keep your system clock accurate.

These are listing-price estimates, not completed sales, an instant cash-out quote or guaranteed seller proceeds. Eligibility flags do not establish current trade availability. Attached-sticker or charm prices, craft premiums, estimated_total and marketplace fees are not added. For other workflows, use the two explicit statistic requests or the cache-only price refresh; each separate API call follows normal quota rules.

Update a batch's prices without another Steam scan

First fetch the inventory normally. While that snapshot remains fresh, set refresh_prices:true to revalue its existing items from WOK's local catalog.

JSON bodyPOST /v1/inventories
{"steamids":["76561198090744629"],"games":["cs2","dota2"],"refresh_prices":true,"top":0}

Send the body with Bearer authentication and Content-Type: application/json. This mode makes no Steam or proxy requests, but each unique player-game pair still uses one request-quota unit. Do not combine it with no_cache:true.

Inspect each result's ok and error.code. Missing, expired or invalidated snapshots return per-pair 404 inventory_cache_miss; other pairs still succeed. The endpoint never silently rescans on a miss. Request a separate normal inventory scan only for the pairs you need.

Successful results expose meta.price_refreshed, meta.inventory_created_at and meta.prices_updated_at. Repricing does not extend inventory TTL or prove that items have not changed on Steam. Price time refers to the local repricing operation; each item's priceupdatedat describes its catalog observation.

Full cache and refresh workflowFull batch contractPostman price-only examplePython and JavaScript SDK downloads

Quickstart

Use WOK from a trusted server. Keep the API key outside browser bundles and public repositories.

curl
curl --fail-with-body \
  -H "Authorization: Bearer $WOK_API_KEY" \
  "https://woksteamapi.com/v1/inventory?steam_id=76561198090744629&game=cs2&top=10&price_source=steam"
JavaScript (Node.js)
// Node.js: keep the API key on your server.
const url = new URL("https://woksteamapi.com/v1/inventory");
url.search = new URLSearchParams({
  steam_id: "76561198090744629",
  game: "cs2",
  top: "10",
  price_source: "steam"
});
const response = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.WOK_API_KEY}` }
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
const inventory = await response.json();
Python
import os
import requests

response = requests.get(
    "https://woksteamapi.com/v1/inventory",
    headers={"Authorization": f"Bearer {os.environ['WOK_API_KEY']}"},
    params={
        "steam_id": "76561198090744629",
        "game": "cs2",
        "top": 10,
        "price_source": "steam",
    },
    timeout=30,
)
response.raise_for_status()
inventory = response.json()

Response example

The values below illustrate the response shape. Field names and types match the public contract.

JSON

application/json
{
  "steamid": "76561198090744629",
  "game": "cs2",
  "status": "ok",
  "total": 24.68,
  "items_total": 2,
  "sellable": 2,
  "untradable": 0,
  "untradable_value": 0.0,
  "tradelocked": 0,
  "tradelocked_value": 0.0,
  "unpriced": 0,
  "items": [
    {
      "markethashname": "Example market item",
      "count": 2,
      "game": "cs2",
      "tradable": 1,
      "marketable": 1,
      "pricemedian": 12.34,
      "prices": {
        "steam": 12.34
      },
      "pricesource": "steam",
      "priceupdatedat": 1787572800.0,
      "volume": 3
    }
  ],
  "meta": {
    "cached": true,
    "shared": false,
    "lane": "cache",
    "saved": false,
    "ms": 0,
    "bytes": 0,
    "paid_bytes": 0,
    "upstream_attempts": 0,
    "detail": "",
    "cache_age": 42,
    "cache_ttl": 1800,
    "price_source": "steam"
  }
}

Response schema

Stable fields for typed clients, storage and error handling.

Steam inventory price API with source-level values response schema
FieldTypeMeaning
steamidstringCanonical 17-digit SteamID64.
gamestringDocumented key: cs2, rust, tf2, dota2, pubg, sandbox, unturned or payday2. Numeric Steam appids are also accepted and returned as app<appid> without catalog pricing.
statusstringok, empty, private, notfound, limited or error.
totalnumberPriced sellable inventory total in USD.
items_totalintegerNumber of item instances before top truncation.
sellableintegerTradable and marketable item instances, including unpriced ones.
untradableintegerItem instances excluded from sellable value.
untradable_valuenumberValue of excluded untradable items in USD.
tradelockedintegerItem instances currently trade locked.
tradelocked_valuenumberValue of trade-locked items in USD.
unpricedintegerInstances without a usable price.
items[]arrayItems with market names, count, trade state, prices and optional inspect links.
items[].markethashnamestringCanonical price-catalog market hash name.
items[].marketnamestringSteam market display name when supplied.
items[].typestringSteam-supplied item type text; not a WOK-normalized category.
items[].countintegerGrouped item instance count.
items[].tradableintegerSteam tradability flag, 0 or 1.
items[].marketableintegerSteam marketability flag, 0 or 1.
items[].tradelockedbooleanWhether the grouped item is trade locked.
items[].tradelockuntilnumber|nullUnix expiry when a trade lock is known.
items[].pricemediannumber|nullSelected source median price in USD.
items[].pricesourcestringSelected catalog source; empty when the item is unpriced.
items[].priceupdatedatnumber|nullSelected source observation time, separate from inventory cache age.
items[].pricesobjectPer-source USD prices.
items[].inspect_linksarrayOptional CS2 inspect actions.
metaobjectCache age/TTL, cached/shared flags, timing, bytes, lane and upstream-attempt metadata.
meta.cachedbooleanWhether the inventory snapshot was reused.
meta.sharedbooleanWhether this request joined an in-flight singleflight fetch.
meta.cache_ageintegerSnapshot age in seconds.
meta.cache_ttlintegerApplied freshness window in seconds.
items[].volumeintegerExternal market activity metadata; never inventory quantity.

Limits and error behavior

Rules clients should handle explicitly in production.

The Steam inventory must be public. Private, missing and empty inventories are distinct statuses, not successful empty data.

top accepts 0 through 100. top=0 returns every item row; items_total still describes the complete inventory when a top limit is used.

Prices are USD. An item can be valid while pricemedian is null; use unpriced instead of treating missing prices as zero-value evidence.

A partial or malformed Steam inventory is not saved as a successful snapshot. On GET /v1/inventory, validation/auth failures use HTTP 400/401/402/404/422/429; Steam outcomes private, notfound, limited and error are returned with HTTP 200 in response.status. The compatibility GET /steam/api/inventory maps them to 403/404/429/502 (empty to 410).

Use response total for inventory value. If you aggregate item rows yourself, multiply pricemedian by count, never by volume.

Prices are estimates, not guaranteed sale proceeds. Rare items and applied stickers can require instance-level appraisal.

Cache and freshness semantics

How to tell whether data was reused, shared or refreshed.

Successful inventory snapshots use a 1,800-second default TTL. Negative defaults are 21,600 seconds for empty, 3,600 seconds for private and 600 seconds for not found.

For authenticated inventory responses that reach validation/result handling, read X-Wok-Cache-Age and X-Wok-Cache-TTL when present; authentication and quota errors may omit them.

Identical concurrent misses are coalesced. A waiter receives X-Wok-Singleflight: 1 and does not start another Steam inventory fetch.

Use refresh_prices=1 or POST /v1/inventory/refresh-prices only with an active compatible OK/EMPTY snapshot. Repricing never fetches Steam or resets inventory age/TTL; 404 inventory_cache_miss means run a normal inventory scan. refresh_prices=1 cannot be combined with no_cache=1; prices_updated_at identifies the repricing operation.

A normal cache hit for an OK/EMPTY inventory also reapplies the current local price catalog before returning its value. Inventory snapshot age and item price age are separate: repricing does not fetch Steam, force a provider refresh or extend the inventory TTL.

Set price_source=auto explicitly to override a key's saved source. Auto uses an ordered source preference, not an average or the lowest price. Read items[].pricesource and items[].priceupdatedat for the selected observation; source availability varies by game and item.

Frequently asked questions

Direct answers about access, freshness and result semantics.

How is the inventory total calculated?

WOK multiplies each selected item price by its grouped inventory count. External market volume is metadata and is never used as an item quantity.

Can prices be refreshed without scanning Steam again?

Yes. Normal OK/EMPTY cache hits already apply current local catalog prices. Use refresh-prices when you require cache-only behavior: it returns inventory_cache_miss rather than fetching Steam if no compatible active snapshot exists.

What happens when an item has no price?

The item remains in the response with a null price and contributes to unpriced. It is not silently removed.