# WOK API integration guide

This is the shortest complete path for connecting a server or an agent to WOK. Base URL: `https://woksteamapi.com`. The [OpenAPI contract](https://woksteamapi.com/api/openapi.json) defines all request and response fields; the [Postman collection](https://woksteamapi.com/api/postman.json) contains runnable requests.

## 1. Use one WOK key

Create or reveal a key in your [WOK account](https://woksteamapi.com/account). The same active WOK key authenticates inventory, prices, profiles, friends, and Rust stats. WOK sends requests to Steam using its server configuration. A WOK key is **not** a Valve Steam Web API key, and your client does not need a Steam token.

Keep the key on your server. Set `WOK_API_KEY` and a 17-digit `STEAM_ID` in its environment. Never put the key in a URL, browser bundle, prompt, or log. Use this header on every data request:

```http
Authorization: Bearer <WOK_API_KEY>
```

First, check the key and its remaining quota. This read does not consume a quota unit:

```bash
curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer ${WOK_API_KEY}" \
  https://woksteamapi.com/v1/key
```

An active key returns `active: true`, `plan`, `limits`, `remaining`, and `resets_at`. `401` means the key is missing, invalid, disabled, or expired; `402` means a daily/monthly quota or required add-on is exhausted. A `429` includes `Retry-After` when the minute limit is reached.

Use the **WOK domain**, the **WOK key in the Bearer header**, and the exact WOK paths below. Do not call `api.steampowered.com` with a WOK key. New `/v1` requests should not contain `?key=`; compatibility routes support it only for older clients.

## 2. Choose the game

The `game` parameter selects the game for inventory, item prices, catalog, and history. All eight named games use the same endpoint; only the parameter changes.

| Game | `game` | Steam AppID | Inventory | Price sources | Extra player data |
| --- | --- | ---: | --- | --- | --- |
| Counter-Strike 2 | `cs2` | 730 | `/v1/inventory` | Steam Market and configured third-party markets | CS2 inspect requires the Inspect add-on |
| Rust | `rust` | 252490 | same | Steam Market and configured third-party markets | `/v1/rust/stats`, `/v1/rust/achievements`, `/v1/rust/playtime` |
| Dota 2 | `dota2` | 570 | same | Steam Market and configured third-party markets | — |
| Team Fortress 2 | `tf2` | 440 | same | Steam Market and configured third-party markets | — |
| PUBG | `pubg` | 578080 | same | Steam Market only | — |
| s&box | `sandbox` | 590830 | same | Steam Market only | — |
| Unturned | `unturned` | 304930 | same | Steam Market only | — |
| PAYDAY 2 | `payday2` | 218620 | same | Steam Market only | — |

Every named game uses `GET /v1/price`, `GET /v1/items`, and `GET /v1/price-history` with its `game` key. The endpoints are shared, but source coverage differs by game and item. Inspect `source`, `sources`, and `updated_at` in a price response instead of assuming a particular marketplace is available.

Numeric AppIDs are also accepted for **inventory only**. For example, `game=753` reads Steam Community items and returns `game: "app753"`; it does not enable catalog prices or Rust player stats. The game key is `sandbox` for s&box and `dota2` for Dota 2. Do not send a game name to the Rust player-data paths: they are fixed to AppID 252490.

### Inventory for any named game

```bash
curl --fail-with-body --silent --show-error --get \
  -H "Authorization: Bearer ${WOK_API_KEY}" \
  --data-urlencode "steam_id=${STEAM_ID}" \
  --data-urlencode 'game=rust' \
  --data-urlencode 'top=10' \
  https://woksteamapi.com/v1/inventory
```

Change `game=rust` to any key in the table. `steam_id` may be a SteamID64, profile URL, or vanity name **on the inventory and profile routes**. For friends and Rust player data, send only a 17-digit SteamID64. `top=0` returns all item rows; `items_total` still counts the full inventory when `top` limits rows. The native inventory response is an object containing `status`, `items`, `total`, and `meta`. Check `status`: `private`, `empty`, `notfound`, `limited`, and `error` can be returned with HTTP 200. The compatibility endpoint `/steam/api/inventory` returns an array on success and different HTTP status codes for failures.

For CS2, the default response covers the items Steam exposed through public inventory context 2. Steam may temporarily hide recently acquired items from other viewers; the public count and value cannot include those omitted assets. See [CS2 trade protection and inventory completeness](https://woksteamapi.com/api/cs2-trade-protection.md) before using an inventory as proof of ownership or as a complete portfolio value.

For CS2, opt in with `try_first_seven_days_blocked_items=1` to attempt bounded authenticated partner trade-window context-2 and context-16 reads after the public inventory read. WOK compares asset IDs and may append candidates absent from the public snapshot. The same asset ID is counted once. Candidate rows identify their exact origin with `source: "authenticated_steam_trade_view", context_id: "2"` or `source: "authenticated_steam_context16", context_id: "16"`. Both use `visibility: "absent_from_public_snapshot"`, `protection_status: "unverified"`, and `tradeprotected: null`. They stay separate from public rows and from candidates in the other source with the same name, even with `group=1`; grouped candidate rows include `trade_view_assetids[]`, and compatibility `group=0` rows have one `assetid`. Candidates precede public rows before `search` and native `top`, so default `top=10` can show them. A candidate row may have a saved generic market-hash-name quote with `price_basis: "local_catalog_market_hash_name"`; without one, price fields are null or empty and `price_basis: null`. The request's `price_source` and `strict` selection apply to this local lookup. No extra marketplace or proxy request is made, and top-level valuation excludes candidates. Their presence in either authenticated view does **not** establish seven-day Trade Protected status. No trade offer is sent, and you do not provide a Steam login or trade URL. Mode `2` still returns HTTP `501` because verified protected-item coverage is unavailable.

Check `meta.coverage`: `public_plus_trade_view_unverified` means at least one authenticated view was usable; `public_only` with `meta.protected_items_fallback: "public_only"` means mode `1` fell back to public context 2. `meta.trade_view_partial: true` marks an unavailable, truncated, or invalid response from either authenticated source; `false` still does not prove complete ownership coverage. The compatibility route exposes coverage in `X-Wok-Inventory-Coverage` and fallback in `X-Wok-Protected-Items-Fallback`. In native mode `1`, `meta.public_items_total` is the public count, `meta.trade_view_extra_count` counts added item units after asset-ID deduplication, and top-level `items_total` is the combined observed count. `meta.public_snapshot_age_seconds` reports the age of the public snapshot. `meta.protection_status` is `unverified`. `meta.valuation_scope` and `meta.counts_scope` are both `public_only`: monetary totals and the `sellable`, `untradable`, `tradelocked`, and `unpriced` counters exclude candidates. Compatibility `X-Wok-Total` is public-only value; `X-Wok-Trade-View-Extra-Count` gives added candidates before search. A `steam_count_mismatch` diagnostic indicates a discrepancy with Steam's public count, not the number or identity of protected items.

Each returned inventory item has `image` (the raw Steam icon value) and `image_url` (a renderable HTTPS Steam CDN URL when the icon value has a safe known format). CS2 applied `stickers[]` and `charms[]` use the same two fields when Steam supplied an attachment image. `image_url: null` means there was no safe URL to derive; it does not imply the item lacks an image on Steam. URL derivation is local, makes no extra Steam or proxy request, and does not verify that the image is currently available from the CDN.

For CS2 float, paint seed, paint index and inspect links, send `include_inspect=1` (Inspect add-on required). A trade-view candidate receives those values, plus applied sticker or charm IDs, only when its own valid inspect certificate supplies them; no extra Steam call is made and attachment images or prices can be missing. With `group=0`, available `float_value` and `paint_seed` belong to that row's `assetid`. In grouped output, use `inspect_items[]`; each entry carries its own `assetid` and only the inspect fields Steam supplied for that asset. Missing fields remain absent. Different assets with the same market name never inherit each other's float or seed.

The protected-item option is CS2-only. Other games reject mode `1` or `2` with HTTP `400`; omit it or send `0` for them.

### Preview a CS2 item class

When you already have a Steam `classid` (and, if applicable, `instanceid`) for a CS2 item, use WOK's class preview to fetch Steam's item definition. This is useful for resolving a newly encountered class name, market name, type, tags, descriptions and icon when Steam has published them. It accepts the same Bearer key as other WOK routes; no paid Inspect add-on is needed. Supply decimal ID strings so large Steam IDs are not rounded by JavaScript:

```bash
curl --fail-with-body --silent --show-error --get \
  -H "Authorization: Bearer ${WOK_API_KEY}" \
  --data-urlencode "classid=${CS2_CLASS_ID}" \
  --data-urlencode 'instanceid=0' \
  https://woksteamapi.com/v1/cs2/item-preview
```

The response has `scope: "class"`, `source: "steam_get_asset_class_info"`, `appid: 730`, the class and instance IDs, and `observed_at` (Unix seconds when metadata was fetched). Read `item.name`, `item.markethashname`, `item.image_url`, `item.tags`, and `item.descriptions` when present. `image` is Steam's raw icon value; `image_url` is a derived, renderable URL when the icon has a safe known format. The item definition has no individual asset ID, float, paint seed, owner, price, or trade-protection state. `observed_at` is not the item's acquisition time. Steam may take time to index a new class, so `404 class_not_found` means this class was unavailable from Steam at that moment. Invalid IDs return `400`; missing or excessively long query parameters return `422`. Unavailable Steam metadata or its daily upstream budget returns `503`. One call consumes one WOK quota unit, uses a bounded direct Steam Web API lookup, and can reuse a cached class description without residential proxy traffic. See the [class preview guide](/api/cs2-item-preview.md) for the field and error contract.

### Opt-in inventory change snapshots

To record visible quantity changes, first obtain a complete regular inventory
response, then explicitly capture its **current valid cache**. Snapshot routes
only read local data; they never fetch Steam or spend residential proxy traffic.
They require a canonical SteamID64 rather than a vanity name.

```bash
curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer ${WOK_API_KEY}" \
  -H 'Content-Type: application/json' \
  -d "{\"steam_id\":\"${STEAM_ID}\",\"game\":\"cs2\"}" \
  https://woksteamapi.com/v1/inventory-snapshots
```

The first capture is a `baseline`: `diff.added` and `diff.removed` are empty
and `diff.unknown.quantity_change` says `no_previous_snapshot`. A later
*new inventory observation* yields an `observed_delta` by exact
`market_hash_name`; repeating the same cache observation returns
`created: false`. Follow `diff.next_cursor` with
`GET /v1/inventory-snapshots/{snapshot_id}/diff?cursor=...` when a change list
exceeds one page. `GET /v1/inventory-snapshots?steam_id=...&game=cs2` lists
this key's saved observation metadata, and `DELETE` on that same path removes
its history for the target. Check `from_observed_at` and `to_observed_at` for
the observation window. Asset identity, trade cause, exact event time, and
nonpublic items remain unknown. A missing, private, or incomplete cache never
becomes a false empty snapshot. The [OpenAPI reference](https://woksteamapi.com/api/openapi.json)
defines every request and response field.

### Prices and item catalog

Use the exact Steam market hash name from an inventory item's `markethashname`:

```bash
curl --fail-with-body --silent --show-error --get \
  -H "Authorization: Bearer ${WOK_API_KEY}" \
  --data-urlencode 'game=rust' \
  --data-urlencode 'name=Item name from markethashname' \
  https://woksteamapi.com/v1/price
```

`GET /v1/items?game=rust&q=...` searches saved catalog rows. `GET /v1/price-history?game=rust&name=...&days=30` returns recorded observations, when available. History starts when WOK first observed or was asked to track that item; it does **not** backfill earlier days, so a newly tracked item may have one point even with `days=30`. Prices are USD and can be missing for a valid inventory item. These reads use WOK's saved catalog; they do not force a Steam or marketplace refresh. `POST /v1/prices` accepts up to 200 names for one game. `POST /v1/inventories` accepts up to 20 SteamIDs, four games, and 80 player-game pairs; each pair consumes one quota unit.

### Check coverage before choosing a source

`GET /v1/prices/coverage?game=cs2&source=steam_market` reports how many names in **WOK's saved catalog** and how many names actually seen in requested inventories have a valid saved quote from that source. It also returns quote-age p50/p95 and how many names/days have retained real history. Neither denominator means every Steam item. The report is cached for five minutes and uses no marketplace or residential proxy request.

```bash
curl --fail-with-body --silent --show-error --get \
  -H "Authorization: Bearer ${WOK_API_KEY}" \
  --data-urlencode 'game=cs2' \
  --data-urlencode 'source=steam_market' \
  https://woksteamapi.com/v1/prices/coverage
```

`GET /v1/cs2/items` adds cursor-paginated CS2 filters to the saved catalog: `base_name`, `exterior`, `variant` (`standard`, `stattrak`, or `souvenir`), `source`, `min_price`, and `max_price`. Both USD bounds must match the same source quote; inspect each returned price's `updated_at`. Name-derived fields have `metadata.provenance: "market_hash_name"`. `rarity` and `collection` are explicitly `null`, because the current WOK catalog has no verified feed for them. This endpoint does not scan current individual Steam Market listings; use `/v1/cs2/market-listings` below for their floats.

```bash
curl --fail-with-body --silent --show-error --get \
  -H "Authorization: Bearer ${WOK_API_KEY}" \
  --data-urlencode 'base_name=AK-47' \
  --data-urlencode 'exterior=Field-Tested' \
  --data-urlencode 'source=steam_market' \
  --data-urlencode 'limit=50' \
  https://woksteamapi.com/v1/cs2/items
```

Read `items[]` and follow `next_cursor` while `has_more` is true, sending the **same filters** with each page. A changed filter invalidates that cursor. A name can appear without a saved quote if neither `source` nor a price bound is set; `source_count=0` and `prices=[]` then mean no matching saved quote. For exact field types and error responses, use the [OpenAPI contract](https://woksteamapi.com/api/openapi.json).

### A basket's recorded daily price history

`POST /v1/portfolio-history` combines **observed** daily closing prices for 1–20 exact item names in one game and one saved source. Supply the quantities yourself; this request does not inspect a player's inventory or prove ownership. It reads local history and enrolls known names for future observations within WOK's bounded tracking capacity, without a Steam or market request. It uses one WOK quota unit.

```bash
curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer ${WOK_API_KEY}" \
  -H 'Content-Type: application/json' \
  -d '{"game":"cs2","source":"steam_market","days":30,"items":[{"name":"AK-47 | Redline (Field-Tested)","quantity":2},{"name":"AWP | Asiimov (Field-Tested)","quantity":1}]}' \
  https://woksteamapi.com/v1/portfolio-history
```

The response has `items[].points` for each name and `series[]` for each UTC day. `tracking` reports `tracking`, `not_in_catalog`, or `capacity` per name; tracking affects **future** saved observations only. A day's `total_usd` is `null` unless **every** supplied item has a real observation on that day; `missing_items` names the gaps. `status: "incomplete"` is normal for sparse history. These closes were observed at different times, so even a complete day is not a simultaneous or executable basket quote. No value is carried forward into missing days. `source` must be one saved source, such as `steam_market` or `skinport`; combining sources requires an explicit decision in your application.

### Individual CS2 Market listings and floats

For current **individual** Steam Community Market lots, use the CS2 listing endpoint with the same WOK Bearer key and the Inspect add-on. Pass the exact Steam `market_hash_name`, including exterior and any StatTrak or Souvenir prefix:

```bash
curl --fail-with-body --silent --show-error --get \
  -H "Authorization: Bearer ${WOK_API_KEY}" \
  --data-urlencode 'name=AK-47 | Redline (Field-Tested)' \
  --data-urlencode 'start=0' \
  https://woksteamapi.com/v1/cs2/market-listings
```

Read `listings[]`: each row has `listing_id`, `asset_id`, exact `market_hash_name`, `float_value`, `paint_seed`, `paint_index`, `inspect_link`, and the observed price. `float_status` and `inspect_status` tell you whether those values were available; a missing float is `null`, never an invented zero. `price_minor`, `fee_minor`, and `total_minor` are **in Steam's returned currency**, identified by `currency_id`, `currency`, and `price_display`. They are not the USD catalog prices from `/v1/price`. Use the formatted price or convert with a separate exchange rate before comparing currencies.

Each call reads one bounded Steam page of up to 20 source listings and consumes one WOK quota unit. If `has_more` is true, request the returned `next_start` (normally `20`, `40`, and so on). `source_count` is the number Steam returned before WOK's exact name check; `listings` may be empty on one page while other pages still contain matches. `total_count` is the Steam filtered-query count and is not a guaranteed count for the exact market hash name. `coverage: "partial"` means this one response does not cover the whole query; even a final page may be partial because earlier pages are separate. The optional `bucket_id` returned in the first response avoids repeating the Steam name-to-bucket lookup on subsequent pages. Results are briefly cached; `observed_at` is a Unix timestamp in seconds, and `cached` indicates reuse. Check both before treating a listing as still for sale. This endpoint does not scan the full market or all floats in one request. `404` means Steam did not find the item, `429` means a WOK request limit, and `502`/`503` mean Steam or the market read is temporarily unavailable (with `Retry-After` where supplied).

For a CS2 self-encoded inspect link, `POST /v1/cs2/inspect` decodes float, paint seed, stickers and keychains locally. Set `INSPECT_LINK` to the complete Steam link returned for an asset or listing:

```bash
curl --fail-with-body --silent --show-error \
  -H "Authorization: Bearer ${WOK_API_KEY}" \
  -H 'Content-Type: application/json' \
  -d "{\"inspect_link\":\"${INSPECT_LINK}\"}" \
  https://woksteamapi.com/v1/cs2/inspect
```

Known sticker, charm and sticker-slab IDs include `name` and `markethashname` from WOK's bundled licensed name index. A known definition without a standalone market listing has `markethashname: null`. An unknown ID remains in `stickers[]` or `keychains[]` with its numeric fields and no guessed name, market name or image. The inspect link itself contains no image or market price; this endpoint makes no Steam or marketplace request. Legacy inspect links return `status: "unavailable"` when they require a separate Steam GC inspector.

## 3. Read player information

Profile and friends are for the Steam account, independent of the selected game. For a one-call profile with Steam level, use `GET /v1/profile?steam_id=...&include_level=1`; the response adds `level`, `steam_level`, and `level_status`. The compatibility form `GET /steam/api/profile?id=...&state=full` accepts the SteamWebAPI-style `id` parameter and returns the same level fields. `level_status=unavailable` means Steam has no visible level, while `upstream_error` means the level lookup failed and can be retried. The rest of the profile remains available in either case.

| Need | WOK endpoint | Read from response |
| --- | --- | --- |
| Name, avatar, profile URL, visibility, account creation time | `GET /v1/profile?steam_id=...` | `personaname`, `avatarfull`, `profileurl`, `visibility`, `timecreated` |
| Steam account level (for every game) | `GET /v1/steam-level?steam_id=...` | `steam_level` |
| Public Steam name history | `GET /v1/aliases?steam_id=...` | `aliases[]` with `name` and `time_changed` |
| All visible games and playtime | `GET /v1/games?steam_id=...` | `games[]` with `playtime_forever`, `playtime_2weeks`, `playtime_hours`, `playtime_2weeks_hours`; visible-library total hours |
| Up to 64 profiles | `POST /v1/profiles` | Object keyed by SteamID64 |
| Friends | `GET /v1/friends?steam_id=...&limit=50` | `friends`, `has_more`, `next_cursor` |
| Public ban signals | `GET /v1/player-security?steam_id=...` | `bans`, `trade_eligibility` |

`timecreated` is a Unix timestamp in seconds when Steam supplies it, otherwise `null`. `visibility=3` means public; `0` means WOK could not determine visibility. Steam level is separate from the Rust stats endpoint: `GET /v1/steam-level?steam_id=...` returns `{"steam_id":"...","steam_level":25}` even when Rust statistics for that SteamID are unavailable, if Steam exposes its level. `/v1/games` returns all games visible through Steam's owned-games API, sorted by lifetime playtime. It retains Steam's minutes and adds decimal hours; `total_playtime_hours` sums visible games only. Empty `games` with `visibility="unknown"` may mean private game details or an empty library. Friends are paginated: send the returned `next_cursor` until `has_more` is false. A private friends list returns HTTP 403. A private profile or game details can also make some Rust fields unavailable; never turn missing fields into a fabricated zero.

`/v1/aliases` reads Steam Community's public persona alias history, which is not part of the official Steam Web API. It returns up to ten entries and may include the current name. Steam can clear or limit this history, so an empty array means Steam did not expose aliases; it does not prove the account never used other names. `time_changed` is the timestamp text supplied by Steam Community, not a normalized Unix time.

## 4. Rust stats, achievements, and playtime

These Rust routes take only `steam_id`; WOK fixes `appid=252490` and uses its server-side Steam key. Each call consumes one WOK quota unit and results are cached briefly. Steam's own visibility settings still apply. Use `/v1/steam-level` from section 3 for the account level; level is not a Rust game statistic.

```bash
curl --fail-with-body --silent --show-error --get \
  -H "Authorization: Bearer ${WOK_API_KEY}" \
  --data-urlencode "steam_id=${STEAM_ID}" \
  https://woksteamapi.com/v1/rust/achievements
```

The response keeps Steam's `playerstats` envelope. Read `playerstats.achievements` as an array, for example:

```json
{
  "playerstats": {
    "steamID": "76561198000000001",
    "gameName": "Rust",
    "achievements": [
      {"name": "CRAFT_HAMMER", "achieved": 1},
      {"name": "CONSTRUCT_BASE", "achieved": 0}
    ],
    "success": true
  }
}
```

`achieved` is Steam's numeric flag: `1` unlocked, `0` locked. Valve's JSON calls the achievement identifier `apiname`; WOK also returns the identical `name` alias for simple clients while keeping `apiname`. The list comes from Steam for the requested player; WOK does not invent or hard-code Rust achievement names.

| Need | WOK endpoint | Steam source | Read from response |
| --- | --- | --- | --- |
| Game counters | `GET /v1/rust/stats?steam_id=...` | `GetUserStatsForGame` | `playerstats.stats[]`; Steam may also include `playerstats.achievements[]` |
| Named kills and resources | `GET /v1/rust/summary?steam_id=...` | Same cached Rust stats | `kills.players`, `kills.scientists`, `resources.wood`, `resources.stones`, `resources.metal_ore`, `resources.scrap`, `stats_by_name` |
| Achievements | `GET /v1/rust/achievements?steam_id=...` | `GetPlayerAchievements` | `playerstats.achievements[]` with `name`, `achieved`, and optional `unlocktime` |
| Total and recent playtime | `GET /v1/rust/playtime?steam_id=...` | `GetOwnedGames` filtered to 252490 | `response.games[0].playtime_forever` and optional `playtime_2weeks` in minutes; `playtime_hours` and `playtime_2weeks_hours` in decimal hours |

Steam supplies raw Rust counters as `{ "name": "kill_player", "value": 3370 }` rows. For direct access, find a row by its exact `name`. The summary route converts those published names to stable fields and also returns **all** numeric counters in `stats_by_name`:

```json
{
  "steam_id": "76561198000000001",
  "appid": 252490,
  "scope": "steam_account_lifetime",
  "kills": {"players": 3370, "scientists": 1269, "bears": 158},
  "resources": {"wood": 1974977, "stones": 2816427, "metal_ore": 3158257, "scrap": 50119},
  "combat": {"deaths": 100, "headshots": 1000},
  "stats_by_name": {"kill_player": 3370, "harvested_wood": 1974977, "acquired_metal.ore": 3158257}
}
```

The example is shortened; real responses contain the other mapped keys and every numeric Steam stat. `kills.players` is Rust's published `kill_player` counter, and `kills.scientists` is `kill_scientist` (the Steam-published scientist/bot kill counter). `resources.wood` comes from `harvested_wood`, `resources.stones` from `harvested_stones`, `resources.metal_ore` from `acquired_metal.ore`, and `resources.scrap` from `acquired_scrap`. `stats_by_name` also includes other Steam-published counters such as animal kills and headshots. These are **lifetime Steam account counters across Rust**, not per-server events or live wipe statistics. A counter absent in Steam is `null` in the mapped fields. Steam did not publish a sulfur counter in the public accounts checked; WOK cannot produce an accurate sulfur total from those responses. The five Valve methods in this guide accept SteamID and/or AppID and do not expose a Rust server identifier or a per-server resource ledger. If per-server kills, farming or wipe data are required, the individual Rust server must expose its own plugin/API or logs; public Steam profiles alone cannot reconstruct them. Facepunch documents the admin-only [`sv stats` server command](https://wiki.facepunch.com/rust/useful_commands) for connected players' kills, deaths and suicides.

Steam's `playtime_forever` and `playtime_2weeks` values are **minutes**; WOK calculates the matching decimal `playtime_hours` and `playtime_2weeks_hours`. `response.games` can be empty when Rust is not visible or is not owned; the response alone cannot distinguish those cases. `playtime_2weeks` may be absent when Steam has no recent value; its hours field is then `null`. Steam can also report `playtime_forever: 0` for a player with nonzero Rust stats. WOK preserves that upstream value; do not infer that the account never played from this field alone. Private or absent Rust statistics return HTTP 403 with `error.code=rust_data_unavailable`; do not retry that SteamID until its visibility changes. A transient Steam failure returns 502.

## 5. Other features and limits

| Task | Endpoint | Scope |
| --- | --- | --- |
| Refresh prices on an existing inventory snapshot | `POST /v1/inventory/refresh-prices` | All named games; no new Steam inventory scan; 404 on cache miss |
| CS2 float, paint, stickers | `POST /v1/cs2/inspect` | CS2 only; Inspect add-on |
| CS2 class name, icon and tags by class ID | `GET /v1/cs2/item-preview` | CS2 class metadata; any active key; no individual asset values |
| CS2 inspect PNG | `POST /v1/cs2/screenshot` | CS2 only; Inspect add-on |
| FACEIT player data | `/faceit/data/v4/players...` | FACEIT CS2, separate from Steam Rust stats |
| Catalog export or queued crawl | `/v1/catalog/exports`, `/v1/crawl` | Bulk add-on |

For `429`, wait for `Retry-After` and retry with backoff. For `502`, Steam or another upstream failed; retry later. For `503`, a required server-side upstream capability is unavailable. For `400` or `422`, fix the input. Inspect the JSON error body and HTTP status before retrying. One WOK key can access every included endpoint; paid add-ons extend the **same** key and do not create another credential.

Valve references: [user stats and achievements](https://partner.steamgames.com/doc/webapi/isteamuserstats), [owned games, playtime and level](https://partner.steamgames.com/doc/webapi/iplayerservice), [profiles and friends](https://partner.steamgames.com/doc/webapi/isteamuser).
