API
ReferenceMarket data

Overview

GET
/public/v1

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

EndpointWhat it doesLimit
GET /public/v1/prices/allEvery price in one cached response. Start here.30
GET /public/v1/pricesThe same prices, paginated.60
GET /public/v1/listingsLive listings with full item detail, newest first. Pages of 500 or 1000.1/sec
GET /public/v1/bookSnapshot of every active listing plus seq. For (re)syncing a local book, not polling.6
GET /public/v1/salesRecent sales, filterable by item.60

Buying and selling

EndpointWhat it doesLimit
POST /public/v1/purchaseBuy listings atomically. No cart.30
GET /public/v1/steam-inventoryWhat is in your Steam inventory, with a token per item.5
POST /public/v1/sellList straight from Steam; we send you a trade offer.30
POST /public/v1/listPut backpack items on the market.30
PATCH /public/v1/listChange a listing's price or amount, one or up to 50 at a time.30
POST /public/v1/delistTake listings down, one or up to 50 at a time; items return to your backpack.30
GET /public/v1/my-listingsYour own listings, filterable by status.60
GET /public/v1/my-listings/valueWhat your active listings are worth.30

Items and trades

EndpointWhat it doesLimit
GET /public/v1/backpackYour on-site items, listable and withdrawable by id.30
POST /public/v1/withdrawSend backpack items to Steam as trade offers.not limited
GET /public/v1/tradesYour Steam trades, filterable by status. Pages of 500 or 1000.1/sec

Account

EndpointWhat it doesLimit
GET /public/v1/userYour id, steam_id, name and balance.60
GET /public/v1/ordersYour bought and sold order items.30
GET /public/v1/orders/exportYour whole order history as CSV or JSON, in one download.5
GET /public/v1/transactionsEvery 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:

HeaderMeaning
X-RateLimit-LimitRequests allowed in the window
X-RateLimit-RemainingRequests left in the current window
X-RateLimit-ResetUnix 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.

EventPayloadEmitted 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:

{ "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"  ]}