# Conventions (/conventions)



## Rate limits [#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:

| Header                  | Meaning                                                |
| ----------------------- | ------------------------------------------------------ |
| `X-RateLimit-Limit`     | Requests allowed in the current window, for this route |
| `X-RateLimit-Remaining` | Requests left in the window                            |
| `X-RateLimit-Reset`     | Unix 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: &#x2A;*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]

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

## Identifiers [#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 [#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 [#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 [#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 [#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 [#errors]

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

```json
{
  "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.
