API
ReferenceMarket data

Overview

Open raw markdownFor pasting into AI assistants
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.1/5 sec
GET /public/v1/sales/averages30-day average sale price per item, in one cached response.30

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/depositMove Steam items into your backpack without listing them.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
POST /public/v1/crypto-withdrawSend part of your balance to a crypto address (BTC, ETH, LTC, SOL, USDC).5
GET /public/v1/crypto-withdraw/:idStatus of one crypto withdrawal.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.sold{ listing_id, app_id, market_hash_name, price, amount, listed_at, sold_at }A purchase goes through. Never emitted for delists or cancellations. listed_at is when the listing went live, sold_at when it sold

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.

Webhooks

The WebSocket feed carries marketplace listing activity. For your own account, register a webhook URL under Settings → Developer and we POST every trade status change to it, so trade state does not have to be polled from GET /public/v1/trades.

Deliveries are signed with a secret derived from your API key (X-CSDeals-Signature: sha256=<hex> over <timestamp>.<body>), retried with backoff, and sent from a fixed set of source addresses you can allowlist. The payload's data is a /public/v1/trades row, unchanged.

Full setup, signature verification and retry semantics are in the webhooks guide.

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`. Availability comes with it: `stock` is how many units are listed across every active listing for the item, and `listing_count` how many listings those units are spread over. Both are `0` when nothing is listed, which is when `lowest_listing_price` is `null`. 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.