Overview
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 | Every price in one cached response. Start here. | 30 |
GET /public/v1/prices | The same prices, paginated. | 60 |
GET /public/v1/listings | Live listings with full item detail, newest first. Pages of 500 or 1000. | 1/sec |
GET /public/v1/book | Snapshot of every active listing plus seq. For (re)syncing a local book, not polling. | 6 |
GET /public/v1/sales | Recent sales, filterable by item. | 60 |
Buying and selling
| Endpoint | What it does | Limit |
|---|---|---|
POST /public/v1/purchase | Buy listings atomically. No cart. | 30 |
GET /public/v1/steam-inventory | What is in your Steam inventory, with a token per item. | 5 |
POST /public/v1/sell | List straight from Steam; we send you a trade offer. | 30 |
POST /public/v1/list | Put backpack items on the market. | 30 |
PATCH /public/v1/list | Change a listing's price or amount, one or up to 50 at a time. | 30 |
POST /public/v1/delist | Take listings down, one or up to 50 at a time; items return to your backpack. | 30 |
GET /public/v1/my-listings | Your own listings, filterable by status. | 60 |
GET /public/v1/my-listings/value | What your active listings are worth. | 30 |
Items and trades
| Endpoint | What it does | Limit |
|---|---|---|
GET /public/v1/backpack | Your on-site items, listable and withdrawable by id. | 30 |
POST /public/v1/withdraw | Send backpack items to Steam as trade offers. | not limited |
GET /public/v1/trades | Your Steam trades, filterable by status. Pages of 500 or 1000. | 1/sec |
Account
| Endpoint | What it does | Limit |
|---|---|---|
GET /public/v1/user | Your id, steam_id, name and balance. | 60 |
GET /public/v1/orders | Your bought and sold order items. | 30 |
GET /public/v1/orders/export | Your whole order history as CSV or JSON, in one download. | 5 |
GET /public/v1/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.
// Node, header authimport 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()));// Browser: no headers on a WebSocket, so authenticate by query parameterconst 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:
- Connect to the WebSocket and start buffering events.
GET /public/v1/book: you receive{ seq, listings }.- Load the listings, drop buffered events with
seq <=the snapshot'sseq, 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:
{ "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.
Response Body
application/json
curl -X GET "https://example.com/public/v1"{ "version": "string", "websocket": { "path": "string", "auth": "string" }, "events": [ "string" ], "rest": [ "string" ]}Listings GET
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.
Every price at once GET
Every item price in one response, no pagination. The whole set is built once and cached for 60 seconds, so this is the cheap way to keep a price table current: prefer it to paging through `GET /public/v1/prices`, and prefer it to `GET /public/v1/book` when you want item prices rather than individual live listings. Each row carries `lowest_listing_price`, the cheapest live CS Deals listing for that item, alongside the Steam-derived `market_price`. Optional `app_id` filter, which is worth passing since it cuts the payload to the game you trade. Responses carry an `ETag`; send it back as `If-None-Match` and you get a `304` when nothing has changed. `generated_at` is when the cached copy was built.