GET /v1/listings/stream — 上架与售出的实时推送
通过 Server-Sent Events 在 CS2 挂单出现和售出的那一刻订阅它们。取代轮询,并告诉您商品何时消失。
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_... 发送。
查询参数
过滤在我们这一侧完成,在发送给您之前生效。它们的作用是节省您的带宽——一个流已经承载整个目录,因此您无需为不同细分开启多个连接。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 的话,您会保留已经售出的挂单。事件
GET /v1/listings 返回的 Listing 对象,包含 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 除外:您持有的挂单即使价格移出您的范围也仍会推送给您,以免您保留旧价格。部分市场会在挂单上架几分钟后才提供磨损值和图案,因此它们可能通过 update 到达。贴纸磨损按市场提供的原样返回:市场未提供时,它在此处和 /v1/listings 中都为 null。$100 及以上的挂单以同一商品的下一个挂单为参照定价,因此当该挂单变化时也会推送 update。请仍在订单中传入 max_price_usd:购买时会重新核对价格,更高的价格会以 price_moved 被拒绝,而不会被扣款。new)。负载为 { "id": "..." }。收到您从未收到过的 id 的 gone 属正常情况:忽略即可。请务必处理该事件。 在商品售出的那一刻将其移除,正是您的购买请求成功与购买请求发现商品已消失之间的区别。这是轮询在任何频率下都无法提供的那一半信息。Last-Event-ID 早于保留的历史记录,因此重放将不完整。负载为 { "reason": "last_event_id_expired", "detail": "..." }。当流所遵循的规则发生变化(如 2026 年 10 月 28 日)且此前构建的视图无法向前修补时,同一事件会以 "reason": "stream_rebased" 在打开的连接上发送一次,随后连接被关闭。请重新读取 GET /v1/listings?sort=newest(或重新加载快照)以重建您的视图,然后在不带 Last-Event-ID 的情况下重连。我们发送该事件,而不是默默地给您一份您会误认为完整的部分重放。无缝重连
每个事件都带有id。重连时把您处理过的最后一个 id 通过 Last-Event-ID 请求头发回,您就会收到恰好遗漏的那部分——包括我们发布新版本期间(发布会按设计断开已打开的连接)。
每 25 秒会收到一个 : heartbeat 注释帧。若静默时间更长,请视为连接已死并重连;浏览器的 EventSource 会自动完成这件事并自动重发 Last-Event-ID。
限制
每个 API 密钥最多 3 个并发流。连接本身不受速率限制——一旦建立,事件将以目录变化的速度推送。挂单进入目录后约 0.3 秒,new 事件就会同时推送给所有密钥,因此轮询 /v1/listings 不可能更早看到它。详见 轮询与流。
第 4 个连接会收到 429 too_many_streams,并带有 Retry-After: 90。未正常关闭就断开的连接最多会继续占用名额 90 秒,因此网络中断后立即重连可能需要等待这么久。
落后超过 8 MB(约为完整流的 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 后的恢复。如果您无法维持长连接,次优方案是带 available_after 参数的 GET /v1/listings,它能把每次轮询变成一次增量读取。授权
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
}