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.
curl --request GET \
--url https://csboard.com/v1/listings/stream \
--header 'Authorization: Bearer <token>'import requests
url = "https://csboard.com/v1/listings/stream"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://csboard.com/v1/listings/stream', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://csboard.com/v1/listings/stream",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://csboard.com/v1/listings/stream"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://csboard.com/v1/listings/stream")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://csboard.com/v1/listings/stream")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body<string>{
"code": "<string>",
"detail": "<string>"
}{
"code": "too_many_streams",
"detail": "At most 3 concurrent streams per key. Close an existing connection first — one stream carries the whole catalogue, so you do not need several. A connection that dropped without closing frees its slot within 90 s.",
"max_connections": 3,
"retry_after_sec": 90
}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.Rifle, Knife, Gloves.Classified, Covert.Factory New, Minimal Wear, Field-Tested, Well-Worn, Battle-Scarred.market_hash_name (case-insensitive) — stream one specific item.only or exclude.only or exclude.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.Last-Event-ID header — the header is the standard mechanism and is preferred.new, update, gone. Default: all three.update is about 80% of the feed, almost all of it price changes. If you only buy new listings and drop sold ones, events=new,gone gives you the same picture of what is for sale at about a fifth of the bytes. Leave out gone and you keep listings that have already sold.Events
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.One listing per physical skin, the same rule as GET /v1/listings and the snapshot. When the same skin is on sale through more than one supplier, only the cheapest copy is announced; the others are not. If a cheaper copy appears later, the copy you hold is retired with gone and the cheaper one arrives as new. If the copy you hold sells and another remains, that one arrives as new. Your view therefore never carries two prices for one skin, and it matches the snapshot at any moment.price_usd moved, or float_value / paint_seed arrived after its new event. The payload is the full Listing object, same shape as new, with the current values. Replace your copy by id; if you have no listing with that id yet, add it.With new, update and gone you can keep the whole catalogue yourself — load it once from the snapshot, then apply the stream — instead of polling GET /v1/listings. Filters apply to update like to new, except min_price / max_price: a listing you hold that reprices out of your range still reaches you, so you never keep it at an old price. Some markets send float and pattern a few minutes after the lot goes on sale, which is why they can come in an update. Sticker wear comes as the market reports it: when the market does not know it, it is null both here and in /v1/listings.Lots from $100 are priced against the next listing of the same item, so an update also comes when that other listing changes. Still send max_price_usd with your order: the price is checked again at purchase, and a higher one is refused with price_moved, never charged.new). The payload is { "id": "..." }. A gone for an id you never received is normal: ignore it.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.Last-Event-ID is older than the retained history, so a replay would be incomplete. Payload is { "reason": "last_event_id_expired", "detail": "..." }. The same event with "reason": "stream_rebased" arrives on an open connection, once, when the rules the feed follows change (as on October 28, 2026) and a view built before cannot be patched forward; the connection is then closed.Re-read GET /v1/listings?sort=newest (or reload the snapshot) 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.Reconnecting without gaps
Every event carries anid. 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. Anew event goes out about 0.3 seconds after a listing enters the catalogue, to every key at the same moment, so polling /v1/listings cannot see a listing sooner. See polling versus the stream.
A fourth connection answers 429 too_many_streams with Retry-After: 90. A connection that dropped without closing keeps its slot for up to 90 seconds, so a reconnect right after a network blip may have to wait that long.
A connection that falls more than 8 MB behind (about 40 seconds of the full feed) is closed. Reconnect with Last-Event-ID and you continue from the last event you processed. If it keeps happening, your client reads slower than the feed arrives: narrow it with events and the filters above.
Example request
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
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("update", (e) => {
const listing = JSON.parse(e.data);
live.set(listing.id, listing);
// new price, or float_value / paint_seed arrived later
});
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 |
|---|---|---|
| 400 | invalid_param | A filter has an invalid value, for example an unknown type in events. |
| 401 | missing_api_key, invalid_api_key | Missing or invalid API key. |
| 429 | too_many_streams | More than 3 concurrent streams for this key. Close an existing connection first, or wait Retry-After (90) seconds for a dropped one to free its slot. The body carries max_connections and retry_after_sec. |
/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.Authorizations
Send your key as a Bearer token on every request: Authorization: Bearer csb_pub_.... Generate keys in your CSBoard profile. Calls made with a key are metered only by that key's own limits — there is no per-IP limit on top — and every answer carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and X-RateLimit-Bucket for the bucket the call spent. See Rate limits.
Query Parameters
Only stream listings at or above this USD price.
Only stream listings at or below this USD price.
e.g. Rifle, Knife, Gloves.
e.g. Classified, Covert.
Exact wear name.
Factory New, Minimal Wear, Field-Tested, Well-Worn, Battle-Scarred Exact market_hash_name (case-insensitive).
Filter StatTrak™ items.
only, exclude Filter Souvenir items.
only, exclude 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.
instant, up_to_12h, hold Resume after this event id. Use only if your client cannot send the Last-Event-ID header.
Narrow new events to listings whose refund_percent is at least this (0-100). gone events are never filtered.
0 <= x <= 100Event types to receive, comma-separated: new, update, gone. Default: all three. update is about 80% of the feed, almost all price changes; events=new,gone keeps what is for sale at about a fifth of the bytes.
"new,gone"
Response
An open SSE stream. Stays open until you disconnect.
The response is of type string.
curl --request GET \
--url https://csboard.com/v1/listings/stream \
--header 'Authorization: Bearer <token>'import requests
url = "https://csboard.com/v1/listings/stream"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://csboard.com/v1/listings/stream', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://csboard.com/v1/listings/stream",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://csboard.com/v1/listings/stream"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://csboard.com/v1/listings/stream")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://csboard.com/v1/listings/stream")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body<string>{
"code": "<string>",
"detail": "<string>"
}{
"code": "too_many_streams",
"detail": "At most 3 concurrent streams per key. Close an existing connection first — one stream carries the whole catalogue, so you do not need several. A connection that dropped without closing frees its slot within 90 s.",
"max_connections": 3,
"retry_after_sec": 90
}