Message Format
Every message on the feed, both server replies and data updates, is a flat JSON object with a type field at the top level. There is no nested envelope: the payload fields sit alongside type.
{ "type": "market_updates", "published_at": "2026-05-30T14:00:05.123Z", "markets": [ /* ... */ ] }
Data updates arrive in batches: a single message carries an array of changed entities under a channel-specific key (events or markets). The one exception is stake_limit_update, which arrives as a single flat object per market. Field names are snake_case.
published_at
Every data update carries a root-level published_at: the server-set time the update was published. It is an ISO-8601 UTC timestamp with fixed-width millisecond precision — always exactly three fractional digits, so whole seconds read 2026-05-30T14:00:05.000Z. The fixed width means comparing the raw strings orders updates chronologically; no parsing needed.
Use it to order updates and to measure how fresh the feed is. The value is always set by the server, once per update, and is identical for every client receiving that update.
Server replies (subscribe, pong, error, …) do not carry published_at.
The type tells you how to interpret the rest of the object. Reply types (subscribe, unsubscribe, pong, error, …) are covered in Subscriptions, Heartbeat, and Errors. This page covers the data update types per channel.
A channel may emit message types beyond those documented here. Branch on type and ignore any type you don't recognise.
market channel
Market definitions and their selections, including line and price.
market_updates
{
"type": "market_updates",
"published_at": "2026-05-30T14:00:05.123Z",
"markets": [
{
"id": 8842,
"event_id": 12345,
"definition": {
"id": 70,
"description": "Match Result",
"market_type": { "code": "1x2", "name": "Match Result" },
"segment": { "code": "ft", "name": "Full Time", "scope": "match" }
},
"specifier": null,
"status": "active",
"live": false,
"selections": [
{
"id": 35928,
"market_id": 8842,
"name": "Home",
"participant_id": 5567,
"line_value": null,
"price": {
"id": 991,
"pricing_provider_id": 1,
"price": 2.6747,
"status": "active",
"updated_at": "2026-05-30T14:00:00Z"
}
}
],
"stake_limits": [
{
"pricing_provider_id": 1,
"participant_id": null,
"min_stake": 10,
"max_stake": 1000
}
]
}
]
}
Market fields
| Field | Type | Description |
|---|---|---|
id | int | Market identifier |
event_id | int | Event the market belongs to |
definition | object | Market definition (see below) |
specifier | string | null | Market specifier (e.g. handicap/total line key), where applicable |
status | string | Market status |
live | bool | Whether the market is in-play |
selections | array | The market's selections (see below) |
stake_limits | array | The market's current stake limits (see below) |
definition
| Field | Type | Description |
|---|---|---|
id | int | Definition identifier |
description | string | Human-readable market description |
market_type | object | { code, name } |
segment | object | { code, name, scope } |
selections[]
| Field | Type | Description |
|---|---|---|
id | int | Selection identifier |
market_id | int | Parent market |
name | string | Selection name (e.g. Home, Over) |
participant_id | int | null | Participant the selection refers to, where applicable |
line_value | decimal | null | Line for handicap/total selections; null for moneyline |
price | object | Current price (see below) |
price
| Field | Type | Description |
|---|---|---|
id | int | Price identifier |
pricing_provider_id | int | Always 1 (Vertex). |
price | decimal | Current decimal price |
status | string | active on market_updates — prices that are not tradable are filtered out of that stream. On suspended_market_updates it is suspended, which is how a withdrawal reaches you. See below. |
updated_at | string | ISO 8601 UTC timestamp of when this price was last written. Matches the value the REST endpoints return for the same price. |
stake_limits[]
Each market_updates frame carries the market's limits at the moment it was sent, so you don't need a REST call to read them when a market opens. The shape and values match what GET /events/{eventId}/markets?expand=stake_limits returns to your API key. If the market has no limits set, the array is empty.
| Field | Type | Description |
|---|---|---|
pricing_provider_id | int | Always 1 (Vertex). |
participant_id | int | null | Participant the limit is scoped to on participant-scoped markets (e.g. player props); null otherwise |
min_stake | int | Minimum stake accepted on the market |
max_stake | int | The market's maximum-stake limit. Price-dependent (see Stake Limits) |
suspended_market_updates
Emitted when prices are withdrawn. Same markets array shape as market_updates, without stake_limits. The status on each market reflects the suspension.
This is the message that tells you a price is gone, so do not filter it out. Unlike market_updates, it is deliberately not filtered: the prices inside it carry status: "suspended", and that is the point of the message. On receiving it, drop the prices you are holding for those selections and stop quoting them. You will not receive a market_updates frame saying the same thing, because a non-tradable price never appears on that stream at all.
Quoting resumes the ordinary way: the next market_updates frame carrying those selections means they are tradable again.
A suspension can arrive with identifiers only. If the market's event, sport or competition is no longer available to your key (for example, because the event has just gone inactive), the market is cut down to its ids and statuses:
{
"type": "suspended_market_updates",
"published_at": "2026-05-30T14:00:05.123Z",
"markets": [
{
"id": 8842,
"event_id": 12345,
"status": "suspended",
"selections": [
{
"id": 35928,
"market_id": 8842,
"price": { "id": 991, "pricing_provider_id": 1, "status": "suspended", "updated_at": "2026-05-30T14:00:05Z" }
}
]
}
]
}
definition, specifier, live, the selection name, participant_id, line_value and the price value are null or missing. The instruction is the same: drop the prices you hold for those selection ids. Make sure your client accepts this shape and does not reject the message.
stake_limit_update
Emitted when a market's stake limits change. One message per affected market. Unlike the batched updates above, the fields sit at the top level with no array wrapper.
{
"type": "stake_limit_update",
"published_at": "2026-05-30T14:00:05.123Z",
"event_id": 12345,
"market_id": 8842,
"participant_id": null,
"pricing_provider_id": 1,
"min_stake": 10,
"max_stake": 1000
}
| Field | Type | Description |
|---|---|---|
event_id | int | Event the market belongs to |
market_id | int | Market the limits apply to |
participant_id | int | null | Participant the limit is scoped to on participant-scoped markets (e.g. player props); null otherwise |
pricing_provider_id | int | Always 1 (Vertex). |
min_stake | int | Minimum stake accepted on the market |
max_stake | int | The market's maximum-stake limit. Price-dependent (see Stake Limits) |
Treat this message as a change signal: on receipt, the REST read (GET /events/{eventId}/markets?expand=stake_limits) is the authoritative source for the limits in force.
event channel
Fixture lifecycle and metadata.
event_updates
Each item is a patch, not a full event. Fields that did not change are left out. A missing field means "keep the value you already hold". It never means "clear it". Apply each item to your copy of the event, matched by id.
When an event becomes available to your key, the update carries enough to add it to your copy without a REST call:
{
"type": "event_updates",
"published_at": "2026-05-30T14:00:05.123Z",
"events": [
{
"id": 12345,
"sport_id": 1,
"sport": { "id": 1, "name": "Soccer", "code": "soc" },
"competition_id": 101,
"competition": { "id": 101, "name": "Premier League", "country": "England" },
"participants": [
{ "id": 5567, "name": "Arsenal", "type": "team", "role": "home" },
{ "id": 5568, "name": "Chelsea", "type": "team", "role": "away" }
],
"start_time": "2026-05-30T14:00:00",
"status": "scheduled",
"active": true
}
]
}
When an event stops trading, the update carries only its id, status and active: false:
{
"type": "event_updates",
"published_at": "2026-05-30T14:00:05.123Z",
"events": [
{ "id": 12345, "status": "live", "active": false }
]
}
| Field | Type | Description |
|---|---|---|
id | int | Event identifier. Always present |
sport_id | int | Sport the event belongs to |
sport | object | { id, name, code } |
competition_id | int | Competition the event belongs to |
competition | object | { id, name, country } |
participants | array | { id, name, type, role } per participant. type is team or player. Sent when the event becomes available; otherwise left out |
start_time | string | Scheduled start, in UTC. ISO-8601 with no zone designator — see the note below |
status | string | Event status: scheduled, live, finished or cancelled |
active | bool | Whether the event is open for trading. false means stop trading it |
:::warning active: false means stop trading the event now
When you receive active: false, stop quoting every price you hold for that event straight away. Do not wait for anything else.
statusis not the signal. An event usually goes inactive at kickoff, sostatusmay already readlive. Branch onactive, notstatus.- Suspensions can arrive later. The event update is sent first, and the markets are suspended after it.
suspended_market_updatesfor the event may follow a moment later, possibly with identifiers only. Do not wait for them. - Updates keep coming after deactivation. You will still receive the event's final status, such as
finished, as an update carryingid,statusandactive: false. :::
:::caution start_time carries no Z
On this channel start_time is sent without a zone designator — 2026-05-30T14:00:00, not 2026-05-30T14:00:00Z. The value is UTC. It is the only timestamp on the Vertex API that omits the designator: published_at, updated_at, and start_time on GET /events all carry a trailing Z.
This matters because most date libraries read a zone-less string as the reader's local time, which shifts the kickoff by your own offset and can land it on the wrong date. Parse it explicitly as UTC:
new Date(start_time + "Z") // JavaScript
datetime.fromisoformat(start_time).replace(tzinfo=timezone.utc) # Python
LocalDateTime.parse(startTime).toInstant(ZoneOffset.UTC) // Java
GET /events returns the same instant with the designator attached, so a REST read is a reliable cross-check.
:::
event_sync
A lightweight signal that event-level data has changed and the client should reconcile from REST. It carries no event payload:
{ "type": "event_sync", "published_at": "2026-05-30T14:00:05.123Z", "message": "event_change" }
When it arrives, reload your event list from GET /events and reconcile it with what you hold. It is not a state update in itself.
event_sync is sent to every client subscribed to the event channel, including subscriptions filtered by event_ids. It is the only message that ignores the filter.