Streaming API

WebSocket stream

WebSocket endpoint - upgrade an HTTP connection to receive real-time prices. This is a separate service from the REST API.

Connection

Authenticate with an API key:

ws://<host>/v2/stream?api_key=<api_key>

Query parameters:

ParamRequiredDescription
api_keyyesAPI key (the X-Api-Key value)
langnoLanguage code: en (default), ko, zh-hans

The server does not restrict the Origin header, so browser-based clients - including pages opened from file:// - can connect directly.

Wire format

Every frame sent by the server is a batch envelope:

{"ts": 1586042815.269000, "data": [ <message>, <message>, ... ]}
  • ts - Unix timestamp in seconds with microsecond precision, stamped by the server when the frame is written.
  • data - one or more messages. The ["offer", ...], ["response", ...], ["event", ...], ["remove_event", ...], ["sync", ...], live-event-state and account-update arrays shown below are the individual data[] entries; they are never sent as bare top-level frames.

Multiple messages may be batched into a single envelope - e.g. a register snapshot and its ok ["response", ...] together, or offers alongside account updates. Batching boundaries are not semantically meaningful - iterate data[] and dispatch on each entry's leading type tag (entry[0]); never rely on ordering, grouping, or a type appearing exactly once per frame.

Initial sync

After connecting, the server sends a snapshot of the currently-priced events followed by a ["sync", {...}] marker whose payload carries the session_id of this stream (useful when correlating with REST errors or contacting support). Each event is an ["event", {...}] entry - a flat object carrying the identifiers and metadata - delivered inside the envelope:

{"ts": 1586042815.269000, "data": [
  ["event", {"event_type": "normal", "sport": "fb", "event_id": "2026-06-15,1001,2002", "competition_id": 1, "competition_name": "England Premier League", "competition_country": "XE", "home": "Arsenal", "away": "Chelsea", "event_name": "Arsenal vs. Chelsea", "ir_status": "pre_event", "start_time": "2026-06-15T15:00:00Z"}],
  ["event", {"event_type": "normal", "sport": "tn", "event_id": "2026-06-16,501,502", ...}],
  ["event", {"event_type": "multirunner", "sport": "af", "event_id": "2026-02-23,multirunner,100364405", "competition_id": 545, "competition_name": "USA NFL", "competition_country": "US", "teams": [{"team_id": 21614, "name": "Arizona Cardinals"}, {"team_id": 21615, "name": "Atlanta Falcons"}], "event_name": "NFL Super Bowl Winner", "start_time": "2026-02-23T21:00:00Z", "end_time": "2027-02-14T21:00:00Z"}],
  ["sync", {"session_id": "..."}]
]}

Two event shapes appear. A normal (match) event carries home and away. A multirunner (outright / futures) event has no home/away; instead it carries a teams array ([{"team_id", "name"}, ...], one per runner) and an end_time. Dispatch on event_type; read the runner list from teams for multirunners.

The snapshot is not the full fixture list: it contains only events that currently have live prices. The dump may span several envelopes; ["sync", ...] is the last data[] entry of the final one. From then on, changed events are re-sent as ["event", {...}] entries, and an event whose prices disappear is delivered as ["remove_event", {"sport": ..., "event_id": ..., ...}].

Live event state

Events that are in play may additionally produce state updates as data[] entries. Payloads are sport-specific; the football shapes:

["event_time", {"sport": "fb", "event_id": "...", "time": ["1h", 23]}]
["event_score", {"sport": "fb", "event_id": "...", "score": [1, 0]}]
["event_red_cards", {"sport": "fb", "event_id": "...", "score": [0, 1]}]
  • time - [period, minutes], where the football periods are "1h", "2h" and "ht"; null when no clock is available.
  • score - [home, away] (the event_red_cards payload reuses the score key for the red-card counts).
  • ["ir_info", {...}] carries a full in-running state snapshot for an event (fields vary by sport), and ["remove_ir_info", {"sport": ..., "event_id": ...}] signals the state is gone - treat both as informational.
  • ["event_exchange_dark_liquidity", {"sport": ..., "event_id": ..., "lines": {...}}] - a rough estimate of additional liquidity available per line on the event, beyond the published offers. Informational.

Commands

Register for offers on an event:

["register_event", "<sport>", "<event_id>"]

On success the server immediately sends one ["offer", ...] per active bet type on the event (the snapshot), then an ok response - typically batched in one envelope: {"ts": ..., "data": [["offer", {...}], ["offer", {...}], ["response", {"status": "ok", "data": null}]]}. From then on, whenever the offers on the event change, the full current set is re-broadcast (with the affected bet types updated) and any bet type that has lost all liquidity is delivered as ["remove_offer", ...].

Unregister:

["unregister_event", "<sport>", "<event_id>"]

Server responds with ["response", {"status": "ok", "data": null}] - also when the event was not registered (unregistering is idempotent). No further offer / remove_offer messages are sent for that event.

List currently registered events:

["list_registered_events"]

Server responds with the full set of (sport, event_id) pairs the session is registered for:

["response", {"status": "ok", "data": {
  "registered_events": [
    ["fb", "2026-06-15,1001,2002"],
    ["tennis", "2026-06-16,501,502"]
  ]
}}]

Keepalive (echo):

["echo", "any-payload"]
-> ["response", {"status": "ok", "data": ["any-payload"]}]

Arguments are optional, may be any JSON values, and are echoed back verbatim in data. The server also sends an ["info", ...] entry every few seconds, so an idle connection still receives regular traffic.

offer / remove_offer messages

Each offer describes the available stake at every price for one (sport, event_id, bet_type) triple. The bet_type string fully identifies the market side (it encodes the market, handicap, outcome and for/against direction), so each triple is a distinct, independently-updated offer - for and against on the same selection arrive as two separate offer messages with different bet_type values.

["offer", {
  "sport": "fb",
  "event_id": "2026-06-15,1001,2002",
  "bet_type": "for,ah,h,1",
  "market_type": "ah",
  "in_running": false,
  "price_list": [
    {"effective": {"price": 2.0, "min": ["USDT", 5.0], "max": ["USDT", 150.0]}},
    {"effective": {"price": 1.99, "min": null, "max": ["USDT", 80.0]}}
  ]
}]

Each price_list entry is {"effective": {"price": <decimal>, "min": <stake|null>, "max": <stake>}}; stakes are ["USDT", amount] arrays. The min and max keys are always present:

  • min - the minimum stake accepted at that price; null when there is no minimum.
  • max - the total stake available at that price. Always a ["USDT", amount] pair (a price with no available stake is not published).

Entries are ordered by price (the decimal odds) descending, with at most one entry per price.

remove_offer carries only the (sport, event_id, bet_type) triple - that bet type has no remaining liquidity for the event:

["remove_offer", {
  "sport": "fb",
  "event_id": "2026-06-15,1001,2002",
  "bet_type": "for,ah,h,1"
}]

Account update messages

Account-level updates arrive on the same WebSocket, as plain entries inside the same {"ts": ..., "data": [...]} envelope that carries offer / remove_offer - siblings of the market-data messages.

{"ts": 1586042815.269000, "data": [
  ["balance", {"balance": ["EUR", 10000.1], "open_stake": ["EUR", 152.55], "smart_credit": ["EUR", 300.0]}],
  ["xrate", {"ccy": "EUR", "rate": 1.1347}],
  ["order", {...}],
  ["bet", {...}],
  ["pmm", {...}],
  ["betslip", {...}],
  ["info", {...}]
]}

data[] may contain these account entry types:

  • balance, xrate - amounts are in your account's native currency.
  • order - want_stake, stake and profit_loss are in USDT; each entry of nested bets[] follows the bet format.
  • bet - want_stake, got_stake, profit_loss and status.response_pmm.effective.min/max are in USDT.
  • pmm, betslip - the live quote and state of an open betslip (see the Quickstart). price_list entries follow the same {"effective": {"price", "min", "max"}} format as offer messages; price_list and total are in USDT, prices sorted descending.
  • betslip_closed - {"betslip_id": ..., "close_reason": ...}. The betslip expired (betslips are short-lived) or was closed; no further pmm quotes will arrive for it. Create a new betslip to re-quote the selection.
  • info - feed status; registered_events is the number of events currently registered on this connection.
  • clear_events - the server lost its upstream market data feed: discard all event, offer and live-state data you hold. A fresh snapshot (events, then ["sync", ...]) follows when the feed recovers.

The order, presence, and count of entry types within data[] are not contractual - the example above shows one possible ordering only. Dispatch on each entry's type tag.

balance message fields:

  • balance: [currency, amount] - current account balance in the customer's native currency.
  • open_stake: [currency, amount] - total stake across all unsettled bets, in the same currency.
  • smart_credit: [currency, amount] - extra funds available for betting on top of the balance, equal to the amount the account is expected to win from the bets already placed. Absent when the account has no smart credit.

Errors

Recoverable command-level errors arrive in-band on the open connection. The socket stays up; you can keep sending commands. Like every other message, the error element is a data[] entry inside the envelope (possibly batched with other messages):

{"ts": 1586042815.269000, "data": [["response", {"status": "error", "code": "<code>"}]]}

Codes emitted directly by the stream:

CodeWhen
bad_jsonFrame is not valid JSON, not a JSON array, or an empty array.
invalid_inputThe command name is not a string or not recognised, or its argument shape is wrong for the command.
already_registeredregister_event for an event already registered on this session.
customer_event_limit_exceededregister_event would exceed your registered-events limit (counted across all your connections). Unregister something first.
invalid_customerregister_event while the feed does not recognise your customer record (e.g. not yet propagated after a server restart). Retry after a short backoff; contact support if it persists.
system_errorTransient server-side failure - retry after a short backoff.

Note there is no "unknown event" error: registering an event the feed has no prices for succeeds with an empty snapshot, and unregister_event of an unregistered event returns ok. Treat any other code string as opaque - log it and retry after a short backoff.

Authentication and admission are enforced at the HTTP handshake. A refused upgrade returns a non-101 status with a short text/plain body naming the reason. Clients see a handshake error.

StatusBodyWhat to do
400missing_credentials, invalid_langapi_key is missing, or lang is not an allowed value. Fix the request.
401auth_rejectedThe key was not accepted. Do not retry with the same key; check it against a REST endpoint.
503unavailableThe server cannot accept connections right now. Retry with backoff.

Treat all other 4xx as 401 and all other 5xx as 503. The websockets library raises InvalidStatus with the body on exc.response.body.

Backpressure - if you read too slowly, the server's outbound buffer overflows and the server closes with code 1008. Read faster or register fewer events. An immediate reconnect overflows again. A client that stopped reading never receives the frame and sees only the raw TCP close.

Two classes of failure drop an established connection silently (raw TCP close, no WebSocket close frame, no in-band error):

  • I/O error - any read or write failure on the socket.
  • Internal error - a rare server-side failure; not client-triggerable and observably identical to an I/O error. Reconnect with backoff.
get/v2/stream

Request

  • Base URL: https://magicmarkets.com (the document declares no server; this is the origin it was published from)
  • URL: https://magicmarkets.com/v2/stream
  • Auth: API key in header X-Api-Key

Query parameters

api_keystring required

API key (the X-Api-Key value)

lang'en' | 'ko' | 'zh-hans'

Language for event/competition names

Response

Failures are delivered in-band as ["response", {"status": "error", "code": ...}] entries inside the {"ts": ..., "data": [...]} envelope on the open WebSocket, or by silent TCP close - see the Errors section above.

Changes

No recorded changes to this operation across all 1 revision of this API.