GET /v1/listings/stream — живая лента появления и продажи листингов
Подписка на листинги CS2 в момент их появления и в момент продажи, через Server-Sent Events. Заменяет опрос и сообщает, когда предметы исчезают.
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-параметры
Фильтры применяются на нашей стороне, до отправки вам. Они нужны для экономии вашего трафика — одно соединение уже несёт весь каталог, поэтому несколько подключений под разные сегменты не нужны.Rifle, Knife, Gloves.Classified, Covert.Factory New, Minimal Wear, Field-Tested, Well-Worn, Battle-Scarred.market_hash_name (регистр не учитывается) — поток по одному конкретному предмету.only или exclude.only или exclude.Last-Event-ID — заголовок является стандартным механизмом и предпочтителен.new, update, gone. По умолчанию все три.update составляет около 80% потока, почти всё это изменения цен. Если вы только покупаете новые листинги и убираете проданные, events=new,gone даёт ту же картину того, что сейчас в продаже, примерно в пять раз меньшим объёмом. Без gone у вас останутся уже проданные листинги.События
Listing, который возвращает GET /v1/listings, включая listed_at.Срабатывает как для действительно нового инвентаря, так и для перевыставлений — предметов, вернувшихся из отменённых заказов, истёкших трейд-офферов или снятых холдов. Перевыставленный предмет для вас новый, даже если существовал раньше.Один листинг на один физический предмет — то же правило, что у GET /v1/listings и снимка. Когда один и тот же скин продаётся через нескольких поставщиков, объявляется только самая дешёвая копия, остальные — нет. Если позже появится копия дешевле, та, что у вас, снимается событием gone, а дешёвая приходит как new. Если ваша копия продана, а другая осталась, она приходит как new. Поэтому у вас никогда нет двух цен на один предмет, и ваша картина в любой момент совпадает со снимком.price_usd или пришли float_value / paint_seed после его события new. Полезная нагрузка — полный объект Listing, как у new, с текущими значениями. Замените свою копию по id; если листинга с таким id у вас ещё нет — добавьте его.С new, update и gone можно держать весь каталог у себя — один раз загрузить снапшот, дальше применять стрим — вместо опроса GET /v1/listings. Фильтры применяются к update так же, как к new, кроме min_price / max_price: листинг, который у вас есть и чья цена ушла за ваш диапазон, всё равно придёт, чтобы он не остался у вас по старой цене. Часть маркетов присылает float и паттерн через несколько минут после выставления лота, поэтому они могут прийти в update. Износ наклеек приходит таким, каким его отдаёт маркет: если маркет его не знает, он null и здесь, и в /v1/listings.Лоты от $100 оцениваются относительно следующего листинга того же предмета, поэтому update приходит и тогда, когда меняется тот листинг. Всё равно передавайте max_price_usd в заказе: цена проверяется заново при покупке, и более высокая отклоняется с price_moved, а не списывается.new). Полезная нагрузка — { "id": "..." }. gone для id, который вы не получали, — норма: игнорируйте его.Именно на это событие нужно реагировать. Убирать предметы в момент продажи — это разница между тем, что ваши запросы на покупку проходят, и тем, что они узнают о пропаже предмета постфактум. Эту половину картины опрос не даёт ни на какой частоте.Last-Event-ID старше сохранённой истории, поэтому повтор был бы неполным. Полезная нагрузка — { "reason": "last_event_id_expired", "detail": "..." }. То же событие с "reason": "stream_rebased" приходит один раз в открытое соединение, когда меняются правила потока (как 28 октября 2026) и собранный раньше вид нельзя дополнить вперёд; соединение после этого закрывается.Перечитайте GET /v1/listings?sort=newest (или перезагрузите снимок), чтобы восстановить своё состояние, затем переподключитесь без Last-Event-ID. Мы присылаем это событие вместо того, чтобы молча отдать вам частичный повтор, который вы приняли бы за полный.Переподключение без пропусков
У каждого события естьid. Отправьте последний обработанный id обратно в заголовке Last-Event-ID при переподключении — и получите ровно то, что пропустили, включая наши деплои, которые по своей природе рвут открытые соединения.
Каждые 25 секунд приходит комментарий : heartbeat. Более долгую тишину считайте мёртвым соединением и переподключайтесь; браузерный EventSource делает это сам и сам же отправляет Last-Event-ID.
Лимиты
Три одновременных потока на один API-ключ. Само соединение не ограничено по частоте — события идут с той скоростью, с какой меняется каталог. Событиеnew уходит примерно через 0,3 секунды после появления листинга в каталоге, всем ключам одновременно, так что опрос /v1/listings не увидит листинг раньше. Подробнее — в разделе «Опрос или стрим».
Четвёртое подключение получает 429 too_many_streams с Retry-After: 90. Соединение, оборванное без закрытия, держит свой слот до 90 секунд, поэтому переподключению сразу после сбоя сети, возможно, придётся подождать столько же.
Соединение, отставшее больше чем на 8 МБ (около 40 секунд полного потока), закрывается. Переподключитесь с Last-Event-ID, и поток продолжится с последнего обработанного события. Если это повторяется, ваш клиент читает медленнее, чем идёт поток: сузьте его через events и фильтры выше.
Пример запроса
curl -N "https://csboard.com/v1/listings/stream?max_price=250" \
-H "Authorization: Bearer csb_pub_..." \
-H "Accept: text/event-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
Пример клиента
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);
// новая цена или пришли float_value / paint_seed
});
es.addEventListener("gone", (e) => {
live.delete(JSON.parse(e.data).id);
});
es.addEventListener("resync", () => {
live.clear();
// перечитайте /v1/listings?sort=newest, затем дайте EventSource переподключиться
});
EventSource не умеет выставлять заголовок Authorization — в серверной среде используйте SSE-клиент, который это умеет (или передайте ключ так, как позволяет ваш HTTP-клиент). Сниппет выше показывает обработку событий, а не аутентификацию.
Коды ошибок
| HTTP-статус | Код | Значение |
|---|---|---|
| 400 | invalid_param | У фильтра недопустимое значение, например неизвестный тип в events. |
| 401 | missing_api_key, invalid_api_key | Отсутствует или некорректный API-ключ. |
| 429 | too_many_streams | Больше 3 одновременных потоков на ключ. Закройте существующее соединение или подождите Retry-After (90) секунд, пока оборванное освободит слот. В теле — max_connections и retry_after_sec. |
/v1/listings по таймеру не нужно вообще — оставьте его для первичной загрузки каталога и для восстановления после resync. Если удерживать долгоживущее соединение вы не можете, следующий по эффективности вариант — GET /v1/listings с параметром available_after, который превращает каждый опрос в чтение дельты.Авторизации
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.
Параметры запроса
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"
Ответ
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
}