# Steam badges, game statistics and app data

These methods use your WOK API key in `Authorization: Bearer YOUR_WOK_KEY`.
They are available on all-purpose and Players plans, subject to their existing
quotas and Free-key Discord membership rules. Each single read and local catalog
page uses one quota unit; an achievement batch uses one unit per unique
account/AppID pair.

## Account badges and XP

`GET /v1/badges?steam_id=YOUR_STEAMID64`

Returns published `badges`, their `count`, and Steam's nullable `player_xp`,
`player_level`, `player_xp_needed_to_level_up` and
`player_xp_needed_current_level`. Missing fields remain `null`.
`status: unknown` and `complete: false` distinguish unavailable badge data
from an explicitly published empty list.

## Game statistics

`GET /v1/game-stats?steam_id=YOUR_STEAMID64&appid=252490`

Reads one account and one game. The response includes `stats` as name/value
pairs, `stats_by_name`, `count`, `game_name`, and `complete`. Only finite
published numeric values are included; missing counters are not filled with
zeros. Private or unsupported game statistics return an availability error.
The Rust statistics reader shares this source cache.

## Achievement and statistic definitions

`GET /v1/apps/schema?appid=252490&language=english`

Returns localized `game_name`, `game_version`, `stats` and `achievements`.
Achievement definitions include `name`, `display_name`, `description`,
`hidden`, `icon`, `icon_gray` and `default_value`. Statistic definitions include
`name`, `display_name` and `default_value`.

`language` accepts a bounded list of Steam language names and aliases such as
`en`, `ru`, `de`, `fr`, `es`, `pt`, `tr`, `pl`, `uk` and `zh`. It does not affect
player progress or create separate account caches.

`status` is `available`, `partial` or `unknown`. Absent definition lists stay
`null`; a missing schema is not a claim that the game has no achievements.

## Global achievement percentages

`GET /v1/apps/achievements/global?appid=252490`

Returns `achievements: [{name, percent}]`, `count` and `status`. Percentages
are Steam's published global unlock percentages from 0 to 100. Missing or
invalid percentages remain `null`; they are not estimates of a particular
player's progress.

## App news

`GET /v1/apps/news?appid=730&count=20&maxlength=1000`

Optional `enddate` is a Unix timestamp: return posts published before it.
`count` is 1–50 and `maxlength` is 1–5,000. Unbounded full-text downloads are
not accepted. The response contains `news`, returned `count`, source
`total_count`, `maxlength` and `enddate`.

News entries contain the source title, URL, author, content, feed names and
publication timestamp. `contents` may contain Steam's HTML or markup; treat
it as untrusted content if displaying it on a website.

## Selected achievements in a batch

`POST /v1/games/achievements/batch`

```json
{
  "pairs": [
    {"steam_id": "YOUR_STEAMID64", "appid": 252490},
    {"steam_id": "ANOTHER_STEAMID64", "appid": 730}
  ],
  "include_schema": true,
  "include_global": true,
  "language": "english"
}
```

Provide 1–20 account/AppID pairs. `steam_id` must be a user SteamID64 string;
`appid` must be a positive integer. Duplicates are removed in first-seen order
before quota reservation. `include_schema` and `include_global` default to
`false`. `language` is accepted only with `include_schema: true`.

The response contains `pairs`, `count`, `requested_count`, `cached_count`,
`upstream_attempts` and `pending_count`. Each pair contains `steam_id`, `appid`,
numeric HTTP-style `status`, nullable `data` and nullable `error`.

Requested enrichment is nested under `data.schema` and
`data.global_achievements`, each with `status`, nullable `data` and nullable
`error`. A failed optional source preserves the available player achievements.
An optional read skipped at the deadline reports `batch_deadline_exceeded`.

The batch has four workers, at most 24 outstanding physical jobs and a
30-second caller deadline. Queued jobs stop before starting a source read
after that deadline. Already started reads may finish later;
`upstream_attempts` counts the rows completed when the response was assembled,
and `pending_count` indicates running deadline jobs. Source caches are shared
by AppID and schema language, so many players of the same game reuse them.

No method automatically scans achievements across a player's whole library.

## Local Steam app catalog

`GET /v1/apps/search?q=counter&limit=50`

Searches cached app names by word prefix, or an exact numeric AppID. Leave
`q` empty to page through apps in ascending AppID order. `q` is at most
80 characters, `limit` is 1–100. Use the returned `next_cursor` with the same
query for the next page. A cursor is not a frozen catalog snapshot.

The response contains `apps`, `count`, `query`, `search_mode`, `next_cursor`
and `catalog`. Each app exposes `appid`, `name`, nullable `last_modified` and
nullable `price_change_number`. `catalog` contains `status`, `complete`,
cached `count`, `last_synced_at`, `last_complete_at`, `capacity_limited` and
`source`.

Catalog reads never trigger Steam requests. A background worker bootstraps
official Store app names in pages of at most 5,000, with 30 seconds between
pages, and refreshes changed entries every six hours. Its continuation and
incremental watermark survive restarts. It stores at most 400,000 apps;
reaching that bound is explicitly reported as partial coverage.

`status` is `available`, `partial`, `stale` or `offline`. `complete` describes
the last completed enumeration, not the availability of every app's private
or region-specific data. Steam does not publish deleted-app tombstones through
this reader, so the local catalog retains previously observed names. On SQLite
builds without FTS5, `search_mode: name_prefix` uses an indexed whole-name
prefix lookup.

## Cache and upstream costs

Badge and player-stat reads cache for five minutes, definitions and global
achievement percentages for one hour, and news for five minutes. All reads
reuse Steam Web API admission, singleflight and the server's daily budget.
No residential proxy, browser cookie, publisher key or new subscription is
required. A cold optional achievement enrichment can add one schema call and
one global-percentage call per distinct app; shared app caches avoid repeating
them for each player.

## Steam references

- [Badges: IPlayerService](https://partner.steamgames.com/doc/webapi/IPlayerService)
- [Stats, schemas and percentages: ISteamUserStats](https://partner.steamgames.com/doc/webapi/ISteamUserStats)
- [Public app news: ISteamNews](https://partner.steamgames.com/doc/webapi/ISteamNews)
- [Store app enumeration: IStoreService](https://partner.steamgames.com/doc/webapi/IStoreService)


Batch clients can opt in to explicit missing IDs and per-ID retry diagnostics with `include_meta: true`; discover support through `/v1/key.capabilities.profile_batch_details.version`. See [the exact profile batch contract](#profile-batch-details). Existing admitted quota accounting is unchanged.


## Profile batch details


`POST /v1/profiles` retains its default SteamID-to-profile map. New optional strict boolean `include_meta: true` returns `{profiles, missing, meta}`; an entirely failed accepted batch may also include `error` and retains the source/local HTTP error status. Auth/quota/input errors keep their existing JSON format and accounting. Detect support using `/v1/key.capabilities.profile_batch_details.version == 1` before sending the new option. This capability must not be assumed available until the deployment marker is updated.

`profiles` contains only proven results, including successful peers when others fail. Every missing requested unique ID appears once in `missing`: `{steamid, retryable, error:{code,status,retry_after?,phase?,source_scope?,upstream_status?,category?}}`. A successful official response omitting an ID produces `steam_profile_not_returned`, status404, retryablefalse; absence proves neither a clean account, country, empty inventory nor private visibility. Retry only explicit retryable IDs after the maximum HTTP `Retry-After`; never silently replay the full batch. Partial requests return HTTP200 with proven peers. All-failed local capacity returns503 with source-specific classification and Retry-After2seconds. Daily budget/cooldown use their authoritative source delay.

`meta` contains version1, safe32hex request_id, requested/returned/complete, charged_units/quota_admitted, upstream_attempts/upstream_bytes/attributed_paid_bytes, cached/shared and timings_ms. Physical accounting belongs only to the source owner; shared peers do not duplicate bytes/attempts. Timings are monotonic wall milliseconds for measured stages; absent stages were not entered. Response timings are a snapshot before response JSON serialization; the structured log additionally includes serialization. The worker queue clock starts on endpoint entry after body parsing; it excludes earlier middleware/proxy/body-ingress time. Measurements do not impose a hard wall deadline on SQLite/OS blocking.

Every new profile carries source `steam_webapi`, `steam_public_profile_xml`, or `unknown`. source_observed_at is local positive finite Unix seconds or null when historical origin/observation is unproven. It is not an upstream historical snapshot. A cached value retains its original observation. XML fields cannot overwrite official SQLite profiles/activity/private clocks. XML is only reusable in the separate bounded legacy namespace; official batches/fresh inventory visibility cannot join it. Known official empty country remains distinct from missing XML country.

The current accounting is explicitly retained by user decision: after the existing atomic quota admission each unique ID is charged once, including local503/zero-attempt outcomes. There are no refunds, new quota exemptions, hidden retries or changed customer limits. charged_units reports that policy and is not physical upstream cost.

Headers after quota admission: X-Wok-Request-ID; X-Wok-Missing-Profiles; X-Wok-Missing-Steamids (at most64 validated IDs); existing X-Wok-Partial-Error for classified failures; Retry-After when retryable missing IDs exist. Native client_aborted/499 is recorded when disconnect is observed before worker accounting. A disconnect after that point can still appear as native200 and proxy499; native status alone is not proof of delivered response.

Resource policy: whole cold remainders enter a bounded FIFO registry queue; existing cache/shared peers remain available without joining the cold queue. Wait<=2seconds, max16queued callers; canceled/expired cold work is not newly admitted. Shared existing owners keep their lifecycle. This FIFO does not establish a per-key production SLA. Shared Steam8network slots, existing global pacing/day budgets and all other clients remain unchanged.
