> ## Documentation Index
> Fetch the complete documentation index at: https://api.csboard.com/llms.txt
> Use this file to discover all available pages before exploring further.

# GET /v1/listings/stream — Live Feed of New and Sold Listings

> Subscribe to CS2 listings the moment they appear and the moment they sell, over Server-Sent Events. Replaces polling, and tells you when items disappear.

A Server-Sent Events feed of the catalogue changing. One connection replaces any polling loop.

Polling cannot answer *"what is new?"* cheaply — every client that tries ends up re-reading the same first page on a timer, and the answer is almost always "nothing". It also cannot answer *"what is gone?"* at all: a poller only discovers an item sold by trying to buy it and failing. This endpoint pushes both.

**Authentication required.** Send your key as `Authorization: Bearer csb_pub_...`.

## Query parameters

Filters are applied on our side, before anything is sent to you. They exist to save your bandwidth — one stream already carries the whole catalogue, so you never need several connections to cover different segments.

<ParamField query="min_price" type="number">
  Only stream listings at or above this USD price.
</ParamField>

<ParamField query="max_price" type="number">
  Only stream listings at or below this USD price.
</ParamField>

<ParamField query="category" type="string">
  Only stream this category, e.g. `Rifle`, `Knife`, `Gloves`.
</ParamField>

<ParamField query="rarity" type="string">
  Only stream this rarity tier, e.g. `Classified`, `Covert`.
</ParamField>

<ParamField query="wear" type="string">
  Only stream this wear bucket: `Factory New`, `Minimal Wear`, `Field-Tested`, `Well-Worn`, `Battle-Scarred`.
</ParamField>

<ParamField query="name" type="string">
  Exact `market_hash_name` (case-insensitive) — stream one specific item.
</ParamField>

<ParamField query="stat_trak" type="string">
  StatTrak™ filter. `only` or `exclude`.
</ParamField>

<ParamField query="souvenir" type="string">
  Souvenir filter. `only` or `exclude`.
</ParamField>

<ParamField query="delivery" type="string">
  `instant` or `hold`. Narrows `new` events to one delivery bucket — a withdrawal bot subscribing with `delivery=instant` never sees an item it cannot ship today.

  `gone` events are never filtered: you may be holding the item from before your filter existed, and a missed removal is the ghost listing this feed exists to prevent.
</ParamField>

<ParamField query="last_event_id" type="string">
  Resume after this event id. Use only if your client cannot send the `Last-Event-ID` header — the header is the standard mechanism and is preferred.
</ParamField>

## Events

<ResponseField name="new" type="Listing">
  A listing entered the catalogue. The payload is exactly the `Listing` object `GET /v1/listings` returns, including `listed_at`.

  This fires for genuinely new inventory **and** for re-listings — items returning from cancelled orders, expired trade offers, or released holds. A re-listed item is new to you even though it existed before.
</ResponseField>

<ResponseField name="gone" type="object">
  A listing left the catalogue — sold, reserved, or withdrawn. The payload is `{ "id": "..." }`.

  **Act on this one.** Dropping items as they sell is the difference between your buy calls succeeding and your buy calls discovering the item was already gone. This is the half of the picture polling cannot give you at any frequency.
</ResponseField>

<ResponseField name="resync" type="object">
  Your `Last-Event-ID` is older than the retained history, so a replay would be incomplete. Payload is `{ "reason": "last_event_id_expired", "detail": "..." }`.

  Re-read `GET /v1/listings?sort=newest` to rebuild your view, then reconnect without `Last-Event-ID`. We send this rather than silently handing you a partial replay you would mistake for a complete one.
</ResponseField>

## Reconnecting without gaps

Every event carries an `id`. Send the last id you processed back as the `Last-Event-ID` header on reconnect and you receive exactly what you missed — including across our deploys, which drop open connections by design.

A `: heartbeat` comment arrives every 25 seconds. Treat a longer silence as a dead connection and reconnect; browsers' `EventSource` does this and resends `Last-Event-ID` for you.

## Limits

Three concurrent streams per API key. The connection itself is not rate-limited — once open, events flow as fast as the catalogue changes.

## Example request

```bash theme={null}
curl -N "https://csboard.com/v1/listings/stream?max_price=250" \
  -H "Authorization: Bearer csb_pub_..." \
  -H "Accept: text/event-stream"
```

## Example stream

```
: connected

id: 1785312840123-0
event: new
data: {"id":"000012e8-1f4a-4c2b-9d3e-7a1b2c3d4e5f","market_hash_name":"AK-47 | Redline (Field-Tested)","wear":"Field-Tested","float_value":0.2418,"price_usd":31.2,"category":"Rifle","rarity":"Classified","tradable":true,"delivery":"instant","listed_at":"2026-07-27T09:14:02.481Z"}

id: 1785312851904-0
event: gone
data: {"id":"000012e8-1f4a-4c2b-9d3e-7a1b2c3d4e5f"}

: heartbeat
```

## Example client

```javascript theme={null}
const es = new EventSource("https://csboard.com/v1/listings/stream?max_price=250");
const live = new Map();

es.addEventListener("new", (e) => {
  const listing = JSON.parse(e.data);
  live.set(listing.id, listing);
});

es.addEventListener("gone", (e) => {
  live.delete(JSON.parse(e.data).id);
});

es.addEventListener("resync", () => {
  live.clear();
  // re-read /v1/listings?sort=newest, then let EventSource reconnect
});
```

`EventSource` cannot set an `Authorization` header — in a server-side runtime use an SSE client that can (or pass the key however your HTTP client allows). The snippet above shows the event handling, not the auth.

## Error codes

| HTTP status | Code               | Meaning                                                                          |
| ----------- | ------------------ | -------------------------------------------------------------------------------- |
| 401         | `unauthorized`     | Missing or invalid API key.                                                      |
| 429         | `too_many_streams` | More than 3 concurrent streams for this key. Close an existing connection first. |

<Tip>
  If you are streaming, you do not need to poll `/v1/listings` on a timer at all — keep it for your initial catalogue load and for recovering after a `resync`. If you cannot hold a long-lived connection, the next best thing is `GET /v1/listings` with `available_after`, which turns each poll into a delta read.
</Tip>


## OpenAPI

````yaml GET /listings/stream
openapi: 3.1.0
info:
  title: CSBoard API
  version: 1.0.0
  description: >-
    Market data over the CSBoard marketplace — live listings, floats, stickers,
    minAsk prices, FX rates — plus opt-in buying straight from your balance.
    Free to read, key-gated, built for automation.
  contact:
    name: CSBoard
    url: https://csboard.com/docs
servers:
  - url: https://csboard.com/v1
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Status
    description: Liveness and freshness probes.
  - name: Market data
    description: Read the live catalog, prices, and FX rates.
  - name: Trading
    description: Buy listings from your CSBoard balance. Opt-in, key-gated.
  - name: Account
    description: Your balance, settled funds, and trading status.
  - name: Webhooks
    description: Register an endpoint and receive signed order updates instead of polling.
paths:
  /listings/stream:
    get:
      tags:
        - Market data
      summary: Stream listings as they appear and disappear
      description: >-
        Server-Sent Events feed of the catalogue changing. One connection
        replaces any polling loop.


        Polling cannot answer "what is new?" cheaply — every client that tries
        ends up re-reading the same first page on a timer. It also cannot answer
        "what is gone?" at all: a poller only learns an item sold by trying to
        buy it and failing. This endpoint pushes both edges.


        ### Events


        - `new` — data is exactly the `Listing` object `/v1/listings` returns.

        - `gone` — data is `{ "id": "…" }`. The item left the catalogue. **Act
        on this.** Dropping items as they sell is the difference between your
        buy calls succeeding and your buy calls discovering the item was already
        gone.

        - `resync` — your `Last-Event-ID` predates the retained history, so a
        replay would be incomplete. Re-read `/v1/listings?sort=newest` before
        trusting the stream again.


        ### Reconnecting without gaps


        Every event carries an id. Send the last one you processed back as the
        `Last-Event-ID` header and you receive exactly what you missed —
        including across our deploys. A `: heartbeat` comment arrives every 25
        seconds; treat a longer silence as a dead connection and reconnect.


        ### Limits


        Three concurrent streams per key. One stream already carries the whole
        catalogue, so the filters below are for your bandwidth, not for working
        around that.


        ```

        : connected


        id: 1785312840123-0

        event: new

        data: {"id":"000012e8-…","market_hash_name":"AK-47 | Redline
        (Field-Tested)","price_usd":31.2,"delivery":"instant","listed_at":"2026-07-27T09:14:02.481Z"}


        id: 1785312851904-0

        event: gone

        data: {"id":"000012e8-…"}


        : heartbeat

        ```
      operationId: streamListings
      parameters:
        - name: min_price
          in: query
          required: false
          description: Only stream listings at or above this USD price.
          schema:
            type: number
        - name: max_price
          in: query
          required: false
          description: Only stream listings at or below this USD price.
          schema:
            type: number
        - name: category
          in: query
          required: false
          description: e.g. Rifle, Knife, Gloves.
          schema:
            type: string
        - name: rarity
          in: query
          required: false
          description: e.g. Classified, Covert.
          schema:
            type: string
        - name: wear
          in: query
          required: false
          description: Exact wear name.
          schema:
            type: string
            enum:
              - Factory New
              - Minimal Wear
              - Field-Tested
              - Well-Worn
              - Battle-Scarred
        - name: name
          in: query
          required: false
          description: Exact market_hash_name (case-insensitive).
          schema:
            type: string
        - name: stat_trak
          in: query
          required: false
          description: Filter StatTrak™ items.
          schema:
            type: string
            enum:
              - only
              - exclude
        - name: souvenir
          in: query
          required: false
          description: Filter Souvenir items.
          schema:
            type: string
            enum:
              - only
              - exclude
        - name: delivery
          in: query
          description: >-
            Narrow `new` events to one delivery bucket (`instant` or `hold`).
            `gone` events are never filtered — a missed removal is the ghost
            listing this feed exists to prevent.
          schema:
            type: string
            enum:
              - instant
              - up_to_12h
              - hold
        - name: last_event_id
          in: query
          required: false
          description: >-
            Resume after this event id. Use only if your client cannot send the
            `Last-Event-ID` header.
          schema:
            type: string
          example: 1785312840123-0
        - name: min_refund_percent
          in: query
          description: >-
            Narrow `new` events to listings whose refund_percent is at least
            this (0-100). `gone` events are never filtered.
          schema:
            type: number
            minimum: 0
            maximum: 100
      responses:
        '200':
          description: An open SSE stream. Stays open until you disconnect.
          content:
            text/event-stream:
              schema:
                type: string
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Too many concurrent streams for this key (max 3).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    Error:
      type: object
      description: >-
        All errors return { code, detail }. Some carry extra fields (e.g.
        price_moved adds current_total_usd, insufficient_balance adds
        required_usd/current_usd).
      properties:
        code:
          type: string
          description: >-
            Machine-readable error code, e.g. rate_limit_exceeded,
            trading_not_enabled, price_moved.
        detail:
          type: string
          description: Human-readable explanation.
      required:
        - code
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        Send your key as a Bearer token on every request: `Authorization: Bearer
        csb_pub_...`. Generate keys in your CSBoard profile.

````