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 — 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_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.

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.

FieldDescription
purchase_idPurchase UUID (use it with GET /api/items/holds or GET /api/trades)
custom_idYour idempotency id
asset_id, game_id, market_hash_name, priceThe 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_statusDirect purchases, when the offer is already registered
unhold_at, auto_withdrawHold purchases only
itemSee 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:

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)

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_idThe asset that changed
itemThe 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.

Need help? Contact our support team