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. Every purchase frame embeds the purchase exactly as the REST API returns it — the GET /api/trades element and, for holds, the GET /api/items/holds element — built by the same functions that serve those routes, and a frame is pushed whenever either body changes. What you hold from the socket is what a REST call would return at that moment.
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.
Purchase fields (every purchase frame)
market-purchased, market-status-changed and market-purchase-updated all carry these fields, read off the REST bodies at the moment the frame was built:
| Field | Description |
|---|---|
purchase_id | Purchase UUID — trade_id in GET /api/trades, purchase_id in GET /api/items/holds |
custom_id | Your idempotency id |
asset_id, game_id, market_hash_name | The purchased item |
purchase_type | "direct" or "hold" |
steam_trade_id | Steam trade offer id — processed_trade.tradeoffer_id in GET /api/trades, steam_trade_id in GET /api/items/holds; null until the offer exists |
steam_full_trade_id | Steam trade id once the offer is accepted — processed_trade.trade_id in GET /api/trades (what its steam_trade_ids filter matches) |
steam_trade_status | Steam offer state name (Active, Accepted, Declined, …) — the name of processed_trade.status in GET /api/trades |
summary | Same as summary in GET /api/trades (InProgress, Accepted, DeclinedByBuyer, …) |
unhold_at, auto_withdraw | Hold purchases only — same as in GET /api/items/holds |
trade | The purchase's full GET /api/trades element |
hold | The purchase's full GET /api/items/holds element for hold purchases, null for direct ones |
The embedded item object
market-purchased and market-status-changed also embed 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). The same computed body also refreshes the route's server-side cache, so anyone polling the route sees the change instantly.
Events
market-purchased — sent to the buyer
Fired once when a purchase is created. Adds to the purchase fields:
| Field | Description |
|---|---|
price | What you paid |
status | "processing" for direct, "reserved" for hold |
item | See the embedded item object |
market-status-changed — sent to the buyer
Fired on every status 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 status of GET /api/trades (transaction_status in holds):
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) |
Adds to the purchase fields: status, amount (money layer), refunded (amount returned to your balance, on refunds), reason (the error text of this transition, when there is one) and item.
One status frame per transition
Layers never double-fire a status: 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; that mirror flip shows up as a market-purchase-updated carrying the new hold.state).
market-purchase-updated — sent to the buyer
Fired whenever a purchase's REST body (trade or hold) changes without a status transition — for example:
- the Steam offer is created or changes state (
steam_trade_id,steam_trade_status,summary,processed_trade.status); - the offer is accepted and gets its trade id (
steam_full_trade_id), or settles (processed_trade.settlement); - a post-delivery reversal (
summarybecomesRolledBackBy…); - an error is recorded (
trade.error,hold.last_error,hold.transaction_error) or a withdraw retry is scheduled (hold.retry_count,hold.next_retry_at); - a hold-state mirror flip (
hold.statebecomesaccepted/completed).
It carries exactly the purchase fields — no status, price or item; read trade.status / hold.state for the current state. A frame is sent only when the body really differs from the last one sent, so replace your stored copy of the purchase with trade / hold from each frame.
Frames are snapshots
Each purchase frame carries the full current REST state, so a later frame always supersedes an earlier one. Transitions can reach you in either order — e.g. a market-purchase-updated that already shows trade.status: "completed" may arrive just before the market-status-changed for completed. Drive your state machine from market-status-changed, and keep your copy of the purchase from the newest frame of any kind.
market-items-changed — broadcast to every connected client
Fired on catalog changes for any item GET /api/items lists: an item listed or delisted, a price change, a change to its delivery fields or instant_delivery, and the retire/restore a purchase or cancellation causes. A purchase shows up as the item being retired (item: null), a cancellation as it coming back.
Every catalog-visible change lands here exactly once. Changes are batched: updates are collected for a short window (~500 ms) and shipped with one entry per asset (latest state wins). Up to 500 entries go in one frame, and a larger batch is split across several frames:
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. Includes average_delivery_time (seconds) and delivery_rate (percent 0–100) |
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 trade and hold objects in frames are those routes' elements verbatim, so the reconciled records drop straight into the same store.