API
ReferenceMarket data

Listings

GET
/public/v1/listings

Paginated active listings, newest first, each with the full per-game item fields (cs_paint_wear, cs_paint_seed, cs_stickers, cs_inspect_link, and the Rust/Dota/TF2 equivalents); fields for other games are null. Filter by app_id (730 = CS2, 252490 = Rust, 570 = Dota 2, 440 = TF2); anything finer (float ranges, stickers) is meant to be filtered client-side. Prices are integers in cents. Pass cursor (a listing id) to page stably through a churning book: each response's next_cursor is the value for the next request, null when exhausted. page remains available for offset pagination. limit accepts only 500 or 1000, and the route allows one request per second — page in bulk rather than polling small pages.

AuthorizationBearer <token>

API key generated from your account settings on the website. Send as Authorization: Bearer csd_....

In: header

Query Parameters

page?string
Match^[1-9]\d*$
cursor?string
Match^[1-9]\d*$
limit*string

Value in

  • "500"
  • "1000"
app_id?string
Match^[1-9]\d*$

Response Body

application/json

curl -X GET "https://example.com/public/v1/listings?limit=500"
{  "listings": [    {      "id": 1,      "app_id": 1,      "market_hash_name": "string",      "price": -9007199254740991,      "amount": -9007199254740991,      "commodity": true,      "created_at": "2019-08-24T14:15:22Z",      "steam_asset_id": "string",      "icon_url": "string",      "trade_locked_until": "2019-08-24T14:15:22Z",      "cs_weapon": "string",      "cs_type": "string",      "cs_wear": "string",      "cs_rarity": "string",      "cs_collection": "string",      "cs_is_stattrak": true,      "cs_is_souvenir": true,      "cs_is_highlight": true,      "cs_inspect_link": "string",      "cs_paint_wear": 0,      "cs_paint_seed": -9007199254740991,      "cs_paint_index": -9007199254740991,      "cs_fade_percentage": 0,      "cs_blue_percentage": 0,      "cs_stickers": [        {          "slot": -9007199254740991,          "sticker_id": -9007199254740991,          "name": "string",          "image": "string",          "wear": 0,          "scale": 0,          "rotation": 0,          "tint_id": -9007199254740991,          "offset_x": 0,          "offset_y": 0,          "offset_z": 0,          "pattern": -9007199254740991        }      ],      "cs_keychains": [        {          "slot": -9007199254740991,          "sticker_id": -9007199254740991,          "name": "string",          "image": "string",          "wear": 0,          "scale": 0,          "rotation": 0,          "tint_id": -9007199254740991,          "offset_x": 0,          "offset_y": 0,          "offset_z": 0,          "pattern": -9007199254740991        }      ],      "rust_category": "string",      "rust_type": "string",      "rust_collection": "string",      "dota_rarity": "string",      "dota_hero": "string",      "dota_quality": "string",      "dota_type": "string",      "dota_slot": "string",      "dota_collection": "string",      "dota_event": "string",      "tf2_classes": [        "string"      ],      "tf2_quality": "string",      "tf2_effect": "string",      "tf2_wear": "string",      "tf2_spells": [        "string"      ],      "tf2_warpaint": "string",      "tf2_sheen": "string",      "tf2_collection": "string",      "tf2_grade": "string",      "tf2_paint_color": "string",      "tf2_attributes": {        "craftable": true,        "uncraftable": true,        "festivized": true,        "strange_parts": true,        "holiday_restricted": true      },      "tf2_wiki_link": "string",      "tf2_inspect_link": "string",      "tf2_type": "string"    }  ],  "next_cursor": 1,  "metadata": {    "total_pages": -9007199254740991,    "total_items": -9007199254740991,    "current_page": -9007199254740991,    "current_limit": -9007199254740991  }}

Single listing GET

One active listing by its id, with the same per-game item fields as `GET /public/v1/listings`. Use it to re-check a listing you already know about — after a `listing.price_changed` event, or right before a purchase — instead of paging the whole book. Returns `404 LISTING_NOT_FOUND` once the listing is sold or delisted, which is the cheapest way to confirm a listing is gone.

Overview GET

CS Deals offers a public API for marketplace data and trading: REST endpoints for listings, prices, sales, buying, withdrawing and account state, plus a real-time WebSocket feed of listing activity with sequence numbers for building a trustworthy local book. ## Authentication Create a personal API key from **Settings → API** on the website (key creation is browser-only), then authenticate with `Authorization: Bearer csd_...` on every REST request. This info endpoint is the only unauthenticated route. ## Conventions Every field is `snake_case`. Prices, balances and amounts are integers (prices in cents). CS Deals ids are integers; external Steam identifiers are strings and always prefixed `steam_` (`steam_asset_id`, `steam_offer_id`). Timestamps are ISO 8601 strings everywhere, REST and WebSocket alike. ## REST endpoints Every route, what it is for, and its rate limit in requests per minute. Limits are per API key, per endpoint. ### Market data | Endpoint | What it does | Limit | |----------|--------------|-------| | [`GET /public/v1/prices/all`](/docs/reference/market-data/prices-all) | Every price in one cached response. Start here. | 30 | | [`GET /public/v1/prices`](/docs/reference/market-data/prices) | The same prices, paginated. | 60 | | [`GET /public/v1/listings`](/docs/reference/market-data/listings) | Live listings with full item detail, newest first. Pages of 500 or 1000. | 1/sec | | [`GET /public/v1/book`](/docs/reference/market-data/book) | Snapshot of every active listing plus `seq`. For (re)syncing a local book, not polling. | 6 | | [`GET /public/v1/sales`](/docs/reference/market-data/sales) | Recent sales, filterable by item. | 60 | ### Buying and selling | Endpoint | What it does | Limit | |----------|--------------|-------| | [`POST /public/v1/purchase`](/docs/reference/trading/purchase) | Buy listings atomically. No cart. | 30 | | [`GET /public/v1/steam-inventory`](/docs/reference/selling/steam-inventory) | What is in your Steam inventory, with a token per item. | 5 | | [`POST /public/v1/sell`](/docs/reference/selling/sell) | List straight from Steam; we send you a trade offer. | 30 | | [`POST /public/v1/list`](/docs/reference/selling/list) | Put backpack items on the market. | 30 | | [`PATCH /public/v1/list`](/docs/reference/selling/edit-listing) | Change a listing's price or amount, one or up to 50 at a time. | 30 | | [`POST /public/v1/delist`](/docs/reference/selling/delist) | Take listings down, one or up to 50 at a time; items return to your backpack. | 30 | | [`GET /public/v1/my-listings`](/docs/reference/selling/my-listings) | Your own listings, filterable by status. | 60 | | [`GET /public/v1/my-listings/value`](/docs/reference/selling/my-listings-value) | What your active listings are worth. | 30 | ### Items and trades | Endpoint | What it does | Limit | |----------|--------------|-------| | [`GET /public/v1/backpack`](/docs/reference/trading/backpack) | Your on-site items, listable and withdrawable by `id`. | 30 | | [`POST /public/v1/withdraw`](/docs/reference/trading/withdraw) | Send backpack items to Steam as trade offers. | not limited | | [`GET /public/v1/trades`](/docs/reference/trading/trades) | Your Steam trades, filterable by status. Pages of 500 or 1000. | 1/sec | ### Account | Endpoint | What it does | Limit | |----------|--------------|-------| | [`GET /public/v1/user`](/docs/reference/account/user) | Your id, steam_id, name and balance. | 60 | | [`GET /public/v1/orders`](/docs/reference/account/orders) | Your bought and sold order items. | 30 | | [`GET /public/v1/orders/export`](/docs/reference/account/orders-export) | Your whole order history as CSV or JSON, in one download. | 5 | | [`GET /public/v1/transactions`](/docs/reference/account/transactions) | Every movement of your balance, with the running balance. | 30 | `GET /public/v1/listings` and `GET /public/v1/trades` are metered per second rather than per minute: one request a second, each returning up to 1000 rows. Their `limit` accepts only `500` or `1000`; every other paginated route keeps the usual sizes. Rate-limited responses carry their budget: | Header | Meaning | |--------|---------| | `X-RateLimit-Limit` | Requests allowed in the window | | `X-RateLimit-Remaining` | Requests left in the current window | | `X-RateLimit-Reset` | Unix seconds when the window resets | Exceeding a limit returns `429 RATE_LIMITED` with a `Retry-After` header in seconds. Unauthenticated requests are limited by IP instead. ## WebSocket feed Connect to `wss://api.cs.deals/public/v1/ws` with your API key (Bearer header or `?api_key=` query parameter). The scheme is `wss://`, not `https://`. Connections without a valid key are closed with code `4401`. Each user may hold at most 3 concurrent connections; further attempts are closed with code `4429`. ```js // Node, header auth import WebSocket from "ws"; const ws = new WebSocket("wss://api.cs.deals/public/v1/ws", { headers: { Authorization: `Bearer ${process.env.CSDEALS_API_KEY}` }, }); ws.on("message", (data) => console.log(JSON.parse(data.toString()))); ws.on("close", (code, reason) => console.log("closed", code, reason.toString())); ``` ```js // Browser: no headers on a WebSocket, so authenticate by query parameter const ws = new WebSocket("wss://api.cs.deals/public/v1/ws?api_key=csd_..."); ws.onmessage = (event) => console.log(JSON.parse(event.data)); ``` The first frame is `{ "event": "connected", "data": null, "ts": ... }`. Every event is a JSON envelope: `{ "event": string, "data": object, "seq": number, "ts": string }`. `ts` is the server timestamp, ISO 8601. Control messages (`connected`, `subscribed`, `error`) carry no `seq`. `seq` is a **signed 64-bit integer** (`int64`), global across all listing events and shared with the `seq` returned by `GET /public/v1/book`. Decode it into a 64-bit type, not a 32-bit one. It is strictly increasing but **not contiguous**: gaps are normal and never mean you missed an event. Because the API runs on several processes, two events for the *same* listing can reach you slightly out of order. Every payload is absolute state, so the rule is one line: **apply an event only if its `seq` is higher than the last `seq` you applied for that `listing_id`**, and ignore it otherwise. | Event | Payload | Emitted when | |-------|---------|--------------| | `listing.created` | `{ listing_id, app_id, market_hash_name, price, amount, commodity, created_at, ...item fields }` | A new listing goes live | | `listing.price_changed` | `{ listing_id, app_id, price_before, price_after }` | A seller edits a listing's price | | `listing.amount_changed` | `{ listing_id, app_id, new_amount }` | Stock changes after a purchase (`new_amount: 0` means sold out) | | `listing.removed` | `{ listing_id, app_id }` | A seller delists, or the listing otherwise leaves the book | `listing.created` carries the same per-game item fields as `GET /public/v1/listings` rows (float, paint seed, stickers, and so on), so clients can filter interesting listings the moment they appear without a follow-up request. ### Building a local book Every event payload is absolute state (full listing on create, the new price, the new amount), so applying an event twice is harmless. That makes the sync protocol simple: 1. Connect to the WebSocket and start buffering events. 2. `GET /public/v1/book`: you receive `{ seq, listings }`. 3. Load the listings, drop buffered events with `seq <=` the snapshot's `seq`, apply the rest in order, then keep applying live events. A `price_changed` or `amount_changed` for a listing you don't hold can be ignored. If the socket drops, reconnect and repeat from step 1. One request and one socket rebuilds the whole state. ### Filtering By default a connection receives every event. Send a subscribe message to narrow the feed. Each message **replaces** the connection's filter: ```json { "op": "subscribe", "events": ["listing.created"], "app_ids": [730] } ``` Omitting `events` or `app_ids` (or sending an empty array) removes the filter for that dimension. The server confirms with `{ "event": "subscribed", "data": { events, app_ids } }`, where an unfiltered dimension is reported as the string `"all"` instead of an array; malformed messages get `{ "event": "error" }` and leave the filter unchanged. The event set will grow over time; unknown events should be ignored by clients for forward compatibility.