Skip to main content

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.

note

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

FieldTypeDescription
idintMarket identifier
event_idintEvent the market belongs to
definitionobjectMarket definition (see below)
specifierstring | nullMarket specifier (e.g. handicap/total line key), where applicable
statusstringMarket status
liveboolWhether the market is in-play
selectionsarrayThe market's selections (see below)
stake_limitsarrayThe market's current stake limits (see below)

definition

FieldTypeDescription
idintDefinition identifier
descriptionstringHuman-readable market description
market_typeobject{ code, name }
segmentobject{ code, name, scope }

selections[]

FieldTypeDescription
idintSelection identifier
market_idintParent market
namestringSelection name (e.g. Home, Over)
participant_idint | nullParticipant the selection refers to, where applicable
line_valuedecimal | nullLine for handicap/total selections; null for moneyline
priceobjectCurrent price (see below)

price

FieldTypeDescription
idintPrice identifier
pricing_provider_idintAlways 1 (Vertex).
pricedecimalCurrent decimal price
statusstringactive 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_atstringISO 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.

FieldTypeDescription
pricing_provider_idintAlways 1 (Vertex).
participant_idint | nullParticipant the limit is scoped to on participant-scoped markets (e.g. player props); null otherwise
min_stakeintMinimum stake accepted on the market
max_stakeintThe 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
}
FieldTypeDescription
event_idintEvent the market belongs to
market_idintMarket the limits apply to
participant_idint | nullParticipant the limit is scoped to on participant-scoped markets (e.g. player props); null otherwise
pricing_provider_idintAlways 1 (Vertex).
min_stakeintMinimum stake accepted on the market
max_stakeintThe 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 }
]
}
FieldTypeDescription
idintEvent identifier. Always present
sport_idintSport the event belongs to
sportobject{ id, name, code }
competition_idintCompetition the event belongs to
competitionobject{ id, name, country }
participantsarray{ id, name, type, role } per participant. type is team or player. Sent when the event becomes available; otherwise left out
start_timestringScheduled start, in UTC. ISO-8601 with no zone designator — see the note below
statusstringEvent status: scheduled, live, finished or cancelled
activeboolWhether 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.

  • status is not the signal. An event usually goes inactive at kickoff, so status may already read live. Branch on active, not status.
  • Suspensions can arrive later. The event update is sent first, and the markets are suspended after it. suspended_market_updates for 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 carrying id, status and active: 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.