Skip to content

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_KEY

Message Envelope ​

Every message is a JSON object with the same envelope:

json
{
  "subject": "market-status-changed",
  "operation": "custom",
  "data": [ { "...": "..." } ]
}
FieldTypeDescription
subjectstringEvent name — switch on this
operationstringAlways "custom"
dataarrayOne 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:

FieldDescription
purchase_idPurchase UUID — trade_id in GET /api/trades, purchase_id in GET /api/items/holds
custom_idYour idempotency id
asset_id, game_id, market_hash_nameThe purchased item
purchase_type"direct" or "hold"
steam_trade_idSteam 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_idSteam trade id once the offer is accepted — processed_trade.trade_id in GET /api/trades (what its steam_trade_ids filter matches)
steam_trade_statusSteam offer state name (Active, Accepted, Declined, …) — the name of processed_trade.status in GET /api/trades
summarySame as summary in GET /api/trades (InProgress, Accepted, DeclinedByBuyer, …)
unhold_at, auto_withdrawHold purchases only — same as in GET /api/items/holds
tradeThe purchase's full GET /api/trades element
holdThe 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:

FieldDescription
priceWhat you paid
status"processing" for direct, "reserved" for hold
itemSee 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):

statusLayerMeaning
withdrawableholdTrade hold ended — call POST /api/items/withdraw-hold
processingholdWithdraw offer is being sent / was sent
withdraw_failedholdSend failed — retried automatically, or cancel for a refund
item_unavailableholdItem vanished before delivery — fully refunded
canceledholdHold canceled — fully refunded (refunded carries the amount)
acceptedmoneyBuyer accepted the Steam offer
completedmoneySettled — seller paid out; terminal success
cancelled / declinedmoneyDirect purchase failed pre-delivery — fully refunded
rolled_back_by_user / rolled_back_by_botmoneyPost-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 (summary becomes RolledBackBy…);
  • 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.state becomes accepted / 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_idThe asset that changed
itemThe 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.

Need help? Contact our support team