Appearance
Real-time Updates
Receive purchase and catalog updates over a websocket instead of polling. Events are derived from database change capture (CDC): the API's own committed, validated state is the single source — a socket frame can never disagree with what the REST API returns, because both are produced from the same rows by the same code.
Connecting
Connect to the gateway with your API key as a query parameter:
wss://ws.vpsbot.io/connect/market?x-api-key=YOUR_API_KEYMessage Envelope
Every message is a JSON object with the same envelope:
json
{
"subject": "market-status-changed",
"operation": "custom",
"data": [ { "...": "..." } ]
}| Field | Type | Description |
|---|---|---|
subject | string | Event name — switch on this |
operation | string | Always "custom" |
data | array | One or more event payloads |
WARNING
data is always an array — handle every element, even when you expect a single payload.
The embedded item object
Every purchase event embeds item: the exact response body of GET /api/items/:asset_id computed at that moment — null when that route would return 404 (item reserved, delivered, or gone). You never need a follow-up GET after an event; the frame already carries what the route would say. The same computed body also refreshes the route's server-side cache, so anyone polling the route sees the change instantly.
Events
Three subjects carry the complete story:
market-purchased — sent to the buyer
Fired once when a purchase is created.
| Field | Description |
|---|---|
purchase_id | Purchase UUID (use it with GET /api/items/holds or GET /api/trades) |
custom_id | Your idempotency id |
asset_id, game_id, market_hash_name, price | The item and what you paid |
purchase_type | "direct" (plain buy — trade offer already created) or "hold" (trade-locked item reserved) |
status | "processing" for direct, "reserved" for hold |
steam_trade_id, steam_trade_status | Direct purchases, when the offer is already registered |
unhold_at, auto_withdraw | Hold purchases only |
item | See the embedded item object |
market-status-changed — sent to the buyer
Fired on every subsequent transition, for all purchase types. status carries the verbatim value the REST API stores — hold-lifecycle words are the state you see in GET /api/items/holds, money-lifecycle words are the transaction_status:
status | Layer | Meaning |
|---|---|---|
withdrawable | hold | Trade hold ended — call POST /api/items/withdraw-hold |
processing | hold | Withdraw offer is being sent / was sent |
withdraw_failed | hold | Send failed — retried automatically, or cancel for a refund |
item_unavailable | hold | Item vanished before delivery — fully refunded |
canceled | hold | Hold canceled — fully refunded (refunded carries the amount) |
accepted | money | Buyer accepted the Steam offer |
completed | money | Settled — seller paid out; terminal success |
cancelled / declined | money | Direct purchase failed pre-delivery — fully refunded |
rolled_back_by_user / rolled_back_by_bot | money | Post-delivery Steam reversal (see trades for the refund policy) |
Additional fields when available: refunded (amount returned to your balance), amount, reason, steam_trade_id, steam_trade_status, market_hash_name, and always item.
One frame per transition
Layers never double-fire: a hold cancellation arrives only as the hold-layer canceled frame (its money-layer twin — declined when the buyer canceled, cancelled when the system invalidated — is suppressed), while delivery and settlement arrive only as the money-layer accepted and completed frames (the hold state mirrors both words 1 = 1 in REST, but those mirror flips are silent on the socket). A purchase's frames in order tell the full story with no duplicates.
One deliberate gap: a post-delivery reversal initiated by the buyer's own side changes no stored status (per the refund policy the purchase stays accepted), so no frame fires — consistent with REST.
market-items-changed — broadcast to every connected client
Fired exclusively on listing changes — the single source of this feed is the market's listing table: a seller repricing a hold, live price-follow ticks, an item listed at unlock, delisted, and the listing retire/restore a purchase or cancellation causes. Every catalog-visible transition lands here exactly once; a purchase shows up as its item's listing being retired (item: null), a cancellation as it coming back. Changes are batched: updates are collected for a short window (~500 ms) and shipped as ONE frame whose data array carries every changed asset (one entry per asset, latest state wins):
json
{
"subject": "market-items-changed",
"operation": "custom",
"data": [
{ "asset_id": "…", "game_id": 730, "item": null },
{ "asset_id": "…", "game_id": 730, "item": { "price": 3200, "hold": true, "...": "…" } }
]
}| Field (per entry) | Description |
|---|---|
asset_id, game_id | The asset that changed |
item | The fresh GET /api/items/:asset_id body, or null if it is no longer purchasable |
Typical flow: another buyer reserves a hold item → an entry with item: null arrives; they cancel → a later frame carries the full item body — it is back on sale. Iterate every element of data; a busy moment delivers many assets in one frame.
TIP
Sensitive fields (inspect, bot_id, …) are stripped from all embedded/broadcast item bodies, exactly like an unauthenticated-inspect REST call.
Reconnect and reconcile
Frames are fire-and-forget: after a disconnect, reconcile with GET /api/items/holds and GET /api/trades — the socket statuses are the same words those routes return, so your state machine needs no translation table.