Unofficial CSFloat API

Conventions

Rate limits, money, identifiers, pagination, filtering, mutation bodies, and errors.

Rate limits

Limits are per route, not per account, and they differ by an order of magnitude across the API. There is no published table and no global budget you can reason about up front. Every response carries its own:

HeaderMeaning
X-RateLimit-LimitRequests allowed in the current window, for this route
X-RateLimit-RemainingRequests left in the window
X-RateLimit-ResetUnix timestamp when the window resets

Because the ceiling is route-specific, a client that stays well inside the limit on GET /listings can still be throttled immediately on POST /offers. Read routes are generous; mutation routes — offers, bids, listing changes — are not, often by two orders of magnitude.

The practical consequence: do not hardcode intervals. Drive concurrency from X-RateLimit-Remaining on the route you are actually calling, and back off until X-RateLimit-Reset when it reaches zero. A fixed delay tuned against listing search will be far too aggressive the first time it touches a write route.

Where a bulk route exists, it costs one request instead of N. POST /trades/bulk/accept and PATCH /listings/bulk-modify exist for exactly this reason, and looping their single-item equivalents is the fastest way to exhaust a window.

Money

Money uses integer cents. 500 is $5.00; 50000 is $500.00. Exchange-rate values are decimal multipliers keyed by lowercase currency code.

Identifiers

Listing, order, offer, trade, contract, asset, and Steam IDs are JSON strings. Preserve them as strings; their values exceed JavaScript's safe integer range.

Pagination

  • Cursor: GET /listings, GET /me/watchlist, and GET /users/{steam_id}/stall.
  • Zero-based page: GET /me/buy-orders, GET /me/offers, GET /me/offers-timeline, GET /me/trades, and GET /me/transactions.

Multi-value filters

Trade state filters are comma-separated, for example state=queued,pending. Offer timeline filters repeat the parameter instead, for example states=canceled&states=expired&states=declined.

Mutation bodies

Write operations are not uniform. Three patterns are worth knowing before calling one.

Partial versus absolute updates. PATCH /listings/{listing_id} is a true partial update: send only the fields that changed. PATCH /buy-orders/{order_id} is not — it sends every constraint field on every request, using null to clear one. Omitting a constraint there does not preserve it.

Constraint nesting differs between create and update. POST /buy-orders nests item constraints under hybrid_properties. PATCH /buy-orders/{order_id} sends the same fields flat, at the top level.

Bulk operations key on contract_id. PATCH /listings/bulk-modify identifies each listing by contract_id, not id, even though the single-listing route takes the ID in the path and the listing object calls it id. PATCH /listings/bulk-delist and POST /listings/buy take a contract_ids array; the trade bulk routes take trade_ids.

Mutation responses

Several write operations return a body the CSFloat web client never reads. Where that is the case, the operation says so instead of documenting a shape, because the shape is not established. Treat the status as the result on those.

The bulk trade routes are the exception: they return the updated trade objects, so POST /trades/bulk/accept is more useful than calling POST /trades/{trade_id}/accept in a loop.

Errors

Application errors use a numeric code and a terse message. The code is stable; the message is not, so branch on code.

{
  "code": 4,
  "message": "offers aren't enabled"
}

The code is independent of the HTTP status: a 400 can carry any of several codes, and the same code can appear under different statuses. 27 means no Authorization header was sent.

A 429 means the window for that specific route is exhausted. Wait for X-RateLimit-Reset rather than retrying immediately.

On this page