# Vertex API Documentation > API and WebSocket reference for Vertex partners REST request and response schemas: [OpenAPI 3.1 spec](/openapi/vertex.json). ## Introduction ## Vertex: B2B Sports Betting Odds API A B2B low-latency real-time sports odds and trading API. :::note[B2B access] Vertex is not self-serve. API access is provisioned per partner. Contact your integration manager to be onboarded. ::: ### What is Vertex? Vertex is a B2B sports betting odds and trading API providing low-latency real-time data including: - Pre-match and live odds across major sports - Bet placement and account state - Event catalog: sports, competitions, participants - Live price updates over WebSocket - Reference prices and freshness signals ### Get started - **[Quickstart](./quickstart)**: Connect to the API and WebSocket in minutes - **[REST API](./rest-api)**: Endpoints for catalog, prices, accounts, and bets - **[WebSocket Feed](./websocket)**: Subscribe to live odds and event updates - **[Changelog](./changelog)**: Track updates and breaking changes ### Machine-readable spec The REST API's OpenAPI 3.1 spec is published at [`/openapi/vertex.json`](pathname:///openapi/vertex.json). Load it into a client generator, an API tool such as Postman, or an AI coding assistant. For AI tools, [`/llms.txt`](pathname:///llms.txt) indexes every docs page and [`/llms-full.txt`](pathname:///llms-full.txt) contains all of them in one file. --- ## Quickstart Confirm your Vertex credentials work in two checks: one REST call and one WebSocket connection. ### Prerequisites - A **bearer token** issued during onboarding. If you don't have one, contact your integration manager. - `curl` (for the shell example) or **Node.js 18+** / **Python 3.9+** (for the language examples). Set the token in your environment so it never lands in your shell history: ```bash export VERTEX_TOKEN="" ``` All examples below reference `$VERTEX_TOKEN`. Never paste the literal value into source code. ### 1. Test the REST API A single authenticated `GET /sports` proves your token is valid and the API is reachable. **curl** ```bash curl -s -H "Authorization: Bearer $VERTEX_TOKEN" \ "https://trading.vertex-api.com/api/v2/sports" ``` **JavaScript** ```js const res = await fetch('https://trading.vertex-api.com/api/v2/sports', { headers: { Authorization: `Bearer ${process.env.VERTEX_TOKEN}` }, }); console.log(res.status, (await res.json()).data.length, 'sports'); ``` **Python** ```python import os, requests res = requests.get( "https://trading.vertex-api.com/api/v2/sports", headers={"Authorization": f"Bearer {os.environ['VERTEX_TOKEN']}"}, ) print(res.status_code, len(res.json()["data"]), "sports") ``` Expected response: `HTTP 200` with a wrapped array of sports. ```json { "data": [ { "id": 1, "name": "Soccer", "code": "soc" }, { "id": 6, "name": "Ice Hockey", "code": "ih" } ] } ``` If you get `401`, your token is missing or invalid. Re-check the `VERTEX_TOKEN` env var. ### 2. Test the WebSocket The same `$VERTEX_TOKEN` from step 1 is used to authenticate against the WebSocket feed. Pass it as a `token` query parameter on the connection upgrade. ``` wss://trading-ws.vertex-api.com/ws?token= ``` #### 2.1 Connect Open the socket and confirm the handshake succeeds. **JavaScript** ```js import WebSocket from 'ws'; // npm install ws const ws = new WebSocket( `wss://trading-ws.vertex-api.com/ws?token=${process.env.VERTEX_TOKEN}` ); ws.on('open', () => console.log('Connected')); ws.on('error', (e) => console.error('Connection error:', e.message)); ``` **Python** ```python # pip install websocket-client import os, websocket URL = f"wss://trading-ws.vertex-api.com/ws?token={os.environ['VERTEX_TOKEN']}" def on_open(ws): print("Connected") def on_error(ws, err): print(f"Connection error: {err}") ws = websocket.WebSocketApp(URL, on_open=on_open, on_error=on_error) ws.run_forever() ``` A successful handshake fires the `open` event. If you see `401`, the token is missing or invalid. Re-check `$VERTEX_TOKEN`. #### 2.2 Subscribe to price updates A connection alone receives nothing. You must subscribe. Send a `subscribe` frame to register interest in a channel. The `market` channel emits `market_updates` (markets and their selections, with current prices), which is the most useful single stream to verify against. Omit `event_ids` to receive every update on the channel. Pass an array of event IDs to filter (see below). **JavaScript** ```js ws.on('open', () => { ws.send(JSON.stringify({ type: 'subscribe', channel: 'market' })); // To filter: // ws.send(JSON.stringify({ type: 'subscribe', channel: 'market', event_ids: [12345] })); }); ws.on('message', (raw) => { const msg = JSON.parse(raw.toString()); if (msg.type === 'subscribe') { console.log('Subscribed:', msg); } else if (msg.type === 'market_updates') { console.log('Market update:', msg.markets); } }); ``` **Python** ```python import json def on_open(ws): ws.send(json.dumps({"type": "subscribe", "channel": "market"})) # To filter: # ws.send(json.dumps({"type": "subscribe", "channel": "market", "event_ids": [12345]})) def on_message(ws, raw): msg = json.loads(raw) if msg.get("type") == "subscribe": print("Subscribed:", msg) elif msg.get("type") == "market_updates": print("Market update:", msg["markets"]) ws = websocket.WebSocketApp(URL, on_open=on_open, on_message=on_message, on_error=on_error) ws.run_forever() ``` **Expected output:** a `subscribe` ack with `status: "ok"`, followed by a stream of `market_updates` payloads as prices move. ```json { "type": "subscribe", "status": "ok", "channel": "market", "effective": { "event_ids": [] } } { "type": "market_updates", "published_at": "2026-05-30T14:00:05.123Z", "markets": [{ "id": 8842, "event_id": 12345, "selections": [{ "id": 35928, "price": { "price": 2.6747, "status": "active" } }], ... }] } ``` :::tip[Resubscribing replaces, not adds] Sending a second `subscribe` on the same channel **replaces** the previous filter, so always pass the full desired filter set. Use `unsubscribe` to stop a single channel or `unsubscribe_all` to clear everything. ::: ### Next steps - **[Place a Bet walkthrough](./rest-api/walkthroughs/place-a-bet)**: full bet-placement flow, end to end. - **[REST API reference](./rest-api)**: every endpoint with request/response schemas and try-it-now. - **[WebSocket Feed](./websocket)**: message envelopes, subscriptions, heartbeats, and reconnect. - **[Concepts](./concepts)**: pricing, reference prices, stages. --- ## πŸ—‚οΈ Data Hierarchy ## Data Hierarchy Every price in the Vertex feed is anchored to a five-level hierarchy: **Sport β†’ Competition β†’ Event β†’ Market β†’ Selection**. Understanding this structure is essential for routing messages, building catalogs, and storing data on your side. ### The five levels #### Sport The top-level category of competition, for example soccer, tennis, basketball, ice hockey, or MMA. Each sport has an `id`, a `name`, and a short `code` (e.g. `soc`, `ten`, `mma`). The full set is returned by [`/sports`](../rest-api/get-sports). Sport is the coarsest filter you can apply when discovering competitions and events over REST. #### Competition A named competition within a sport, for example the English Premier League or ATP Masters 1000 Madrid. A competition carries `id`, `sport_id`, `name`, and `country`. Competitions are discovered via [`/competitions`](../rest-api/get-competitions) (filterable by sport). #### Event A single scheduled match or contest, for example Arsenal vs Chelsea on a given date, or a specific MMA bout. An event carries `id`, `sport_id`, `competition_id`, `start_time`, `status` (`scheduled`, `live`, `finished`, `cancelled`), an `active` flag (whether it is open for trading), and its `participants` (each with `id`, `name`, a `type` of `team` or `player`, and `role`). Events are listed via [`/events`](../rest-api/get-events) and fetched individually via [`/events/{eventId}`](../rest-api/get-event). #### Market A bettable proposition on an event, for example match result, total goals over/under 2.5, or first-set winner. A market carries `id`, `event_id`, a `segment` (its classification, with `code`, `name`, and `scope`), an optional `specifier` (the line key for handicap/total variants, e.g. `2.5`), a `status` (`open`, `suspended`, `closed`), and a `live` flag. A market's selections are returned with it via [`/events/{eventId}/markets`](../rest-api/get-markets). #### Selection A single outcome within a market, for example "Arsenal to win", "Over 2.5 goals", or a player-prop outcome. A selection carries `id`, `market_id`, `name`, an optional `participant_id`, a `line_value` (the numeric line for handicap/total selections; `null` for moneyline), an optional `result`, and its Vertex price. The price is carried over REST in a `prices` array that holds the single Vertex entry (`pricing_provider_id`, `price`, `status`). Selections are the leaf nodes of the hierarchy and the unit at which prices are carried. Price updates stream over the WebSocket `market` channel as `market_updates`, where each selection's current price is nested inside its parent market. ### Participants Teams and individual players are **participants** β€” a cross-cutting catalogue rather than a sixth level in the hierarchy. A participant carries `id`, `sport_id`, `name`, a `type` of `team` (e.g. `Carolina Hurricanes`) or `player` (e.g. an individual NHL player, tennis player, or fighter). The catalogue runs to tens of thousands of records, and [`/participants`](../rest-api/get-participants) is paginated (filterable by sport; 100 records per page by default) β€” a single call returns one page, not the full set. Individual participants are fetched via [`/participants/{id}`](../rest-api/get-participant). The same participant `id` is referenced elsewhere by foreign key: as the `role`-tagged competitors in an event's `participants` array, and as the `participant_id` on participant-scoped markets and selections β€” for example, a player-prop market carries the `participant_id` of the player the prop is about. Resolve that `participant_id` against `GET /participants/{id}` to recover the team or player name β€” or, if you are building a local lookup table, page through `/participants` with `page`/`page_size`. ### Visual hierarchy ```mermaid graph TD S[Sport: Soccer] --> C[Competition: Premier League] C --> E[Event: Arsenal vs Chelsea] E --> M1[Market: Match Result] E --> M2[Market: Total Goals 2.5] M1 --> SEL1[Selection: Arsenal] M1 --> SEL2[Selection: Draw] M1 --> SEL3[Selection: Chelsea] M2 --> SEL4[Selection: Over] M2 --> SEL5[Selection: Under] ``` ### Identifiers All identifiers in the feed are **stable integers**. There are no composite, prefixed, or slug-based IDs. Each entity has a plain integer `id`, and each level references its parent by a foreign-key field: | Level | ID field | References parent via | |-------|----------|----------------------| | Sport | `id` | n/a (top level) | | Competition | `id` | `sport_id` | | Event | `id` | `competition_id` | | Market | `id` | `event_id` | | Selection | `id` | `market_id` | For example, lineage is recoverable purely from these integer foreign keys: a selection delivered inside a `market_updates` message carries its `market_id`, and the enclosing market carries its `event_id`, so a single selection resolves up to its event without any composite identifier: ```json { "id": 35928, "market_id": 8842, "price": { "pricing_provider_id": 1, "price": 2.6747, "status": "active" } } ``` Line values are carried as discrete fields (`line_value` on the selection and `specifier` on the market) and are **never** encoded into an identifier. Alternate lines on the same market are simply distinct selections, distinguished by their `line_value`. ### Implications for clients - **Subscriptions** are made per channel (`event` or `market`), optionally filtered by `event_ids`. There is no "subscribe to a whole sport or competition" option; to follow a competition, list the relevant `event_ids` (discovered via REST). Omitting `event_ids` is a wildcard for everything your key is entitled to. See [Subscriptions](../websocket/subscriptions.md). - **Storage**: persist by `selection_id`, but index by `event_id` for fast lookup when an event lifecycle update arrives. - **Reconciliation**: if you receive a selection or market update and don't recognise its parent event, fetch that event's markets via REST before applying the update. --- ## πŸ’‘ Concepts Overview ## Concepts Overview Vertex is the B2B odds delivery API of a quantitative sports betting syndicate and odds provider. It gives external clients programmatic access to proprietary pricing across a broad range of sports (soccer, tennis, basketball, baseball, ice hockey, American football, combat sports such as MMA and boxing, esports, and many more), delivered as both REST catalog endpoints and a real-time WebSocket feed. The full, current set of sports is returned by the [`/sports`](../rest-api/get-sports) endpoint. This section explains the foundational concepts you need to consume the feed effectively. We recommend reading these pages in order before integrating. ### The Vertex price The API exposes prices from a single pricing provider: **Vertex** (`pricing_provider_id: 1`). Every selection carries one Vertex price, and `pricing_provider_id` is always `1` wherever it appears in the API. ### Data hierarchy at a glance All odds in the feed are organised under a strict five-level hierarchy: ```mermaid graph TD Sport --> Competition Competition --> Event Event --> Market Market --> Selection ``` Every entity has a stable integer identifier, and each level references its parent by id (`sport_id`, `competition_id`, `event_id`, `market_id`). See [Data Hierarchy](./data-hierarchy.md) for the full model. ### How REST and WebSocket relate The API is split into two transports with complementary roles: | Transport | Role | Use for | |-----------|------|---------| | REST | Catalog and current state | Discovering sports, competitions, events, and markets; fetching current prices for an event; re-establishing state after a connection loss | | WebSocket | Real-time updates | Receiving live price changes, market status transitions, and event lifecycle updates as they happen | The standard integration pattern is: use REST to bootstrap your catalog and load current prices, then open a WebSocket subscription to receive updates. On reconnect, re-subscribe and re-baseline from REST. The feed does not replay messages missed while you were disconnected (see [Reconnecting](../websocket/reconnect.md)). ### Where to go next - [Data Hierarchy](./data-hierarchy.md): the Sport β†’ Selection model and how identifiers work - [Stake Limits](./stake-limits.md): how `min_stake` / `max_stake` work and how the maximum stake depends on price --- ## βš–οΈ Stake Limits ## Stake Limits Every market carries a **minimum** and **maximum** stake. Read them over REST with [`GET /events/{eventId}/markets?expand=stake_limits`](../rest-api/get-markets). On the WebSocket `market` channel, they arrive with every [`market_updates`](../websocket/messages.md#market_updates) frame, and [`stake_limit_update`](../websocket/messages.md#stake_limit_update) tells you when they change. ```json "stake_limits": [ { "pricing_provider_id": 1, "participant_id": null, "min_stake": 10, "max_stake": 1000 } ] ``` ### Minimum stake `min_stake` is exactly what it sounds like: the smallest stake the market accepts. Bets below it are rejected with `400`. ### Maximum stake depends on the price `max_stake` is a single limit that caps **your stake** at high prices and **your potential profit** at low prices, so it is not always the maximum you can stake. How it translates into a stake cap depends on the decimal price you are betting at: | Price | What `max_stake` caps | Maximum accepted stake | | --- | --- | --- | | `>= 2.00` | Your stake | `max_stake` | | `< 2.00` | Your potential profit | `max_stake / (price βˆ’ 1)` | At prices of `2.00` and above, a winning bet pays out at least as much profit as the stake itself, so the limit applies to the stake directly. Below `2.00`, each unit staked wins *less* than a unit, so the limit shifts to your potential profit, and you can stake more than `max_stake`. #### Worked example With `max_stake: 1000`: | Price | Maximum stake | Potential profit at max | | --- | --- | --- | | `3.00` | 1,000 | 2,000 | | `2.00` | 1,000 | 1,000 | | `1.50` | 1,000 / 0.50 = **2,000** | 1,000 | | `1.10` | 1,000 / 0.10 = **10,000** | 1,000 | Bets above the effective maximum are rejected with `400`. :::tip[Compute the cap before placing] Work out the maximum accepted stake client-side before calling [`POST /bets`](../rest-api/place-bet): ```text If price >= 2.00: max_accepted_stake = max_stake If price < 2.00: max_accepted_stake = max_stake / (price - 1) ``` ::: ### Scope and shape - Limits are **per market**. On participant-scoped markets (e.g. player props), the entry carries the `participant_id` it applies to; on event-scoped markets `participant_id` is `null`. - `pricing_provider_id` is always `1` (Vertex), consistent with the rest of the API. - Limits are specific to your API key. Different partners may see different values on the same market. ### Keeping limits current Limits change as an event approaches and during play. The integration pattern mirrors prices: 1. **Baseline over REST**: fetch `expand=stake_limits` alongside `selections` when you load an event's markets. 2. **Read them from `market_updates`**: each frame carries the market's current `stake_limits`, including the first frame after a market opens. `suspended_market_updates` never carry them. 3. **Listen for `stake_limit_update`**: it arrives on the `market` channel whenever a market's limits change. 4. **Re-baseline when in doubt**: the REST read is the authoritative source for the limits in force; after a reconnect, re-fetch rather than trusting cached values. --- ## πŸ”‘ Authentication ## Authentication All REST endpoints require a Bearer token. The same token also authenticates the WebSocket feed. Tokens are issued per partner during onboarding. To rotate or revoke a token, contact your integration manager. ### Sending the token Include the token in the `Authorization` header on every request: ```bash curl -H "Authorization: Bearer $VERTEX_TOKEN" \ https://trading.vertex-api.com/api/v2/sports ``` ### Unauthorized responses Requests without a valid token return `401 Unauthorized` with an `ErrorResponse` body. The most common cases: | Condition | `message` | |---|---| | `Authorization` header missing, or not prefixed with `Bearer ` | `Missing or invalid Authorization header` | | Token is empty | `Token is required` | | Token is not recognised by any verifier | `No verifier could authenticate the token` | | Token is recognised but rejected (expired, revoked, signature invalid) | Verifier-specific message | Example response: ```json { "status": 401, "message": "Missing or invalid Authorization header", "data": null } ``` --- ## ⚠️ Errors ## Errors Every failed request returns a JSON body and a matching HTTP status code in the response headers. ### Error format There are two response shapes depending on which layer rejected the request. **Standard envelope**: returned by authentication and by endpoints that validate explicitly (for example, `POST /bets`): ```json { "status": 400, "message": "stake must be greater than 0", "data": null } ``` | Field | Type | Description | |---|---|---| | `status` | integer | HTTP status code, mirrored from the response headers. | | `message` | string | Human-readable explanation. For multi-field validation, messages are joined with `; `. | | `data` | string | Reserved for additional context. Currently always `null`. | **Framework fallback**: returned when an exception is not handled explicitly (malformed JSON, type mismatch, unknown route, wrong method, wrong content type): ```json { "timestamp": "2026-05-26T14:37:46.202+00:00", "status": 400, "error": "Bad Request", "path": "/api/v2/bets" } ``` Always branch on the HTTP status code rather than the body shape. --- ### Common error responses #### 400 Bad Request Returned for validation failures, malformed bodies, and badly-typed parameters. Validation on `POST /bets` returns the standard envelope; framework-level errors return the fallback. Multi-field validation on `POST /bets`: ```json { "status": 400, "message": "selectionId is required; pricingProviderId is required; stake is required; price is required", "data": null } ``` Stake outside the market's [stake limits](../concepts/stake-limits.md). The message reports the effective maximum for the price you sent: ```json { "status": 400, "message": "Stake exceeds maximum of 1000", "data": null } ``` Betting a price that has since been withdrawn. Every price you are served is tradable at the moment it is sent, so you cannot pre-empt this by inspecting `status`; a price can be withdrawn between your read and your bet. Re-read the market and bet the new price: ```json { "status": 400, "message": "Selection price is not active", "data": null } ``` Betting a market or event that the provider has stopped trading. Unlike a withdrawn price, this does not clear on a re-read β€” the whole market type is closed until it reopens: ```json { "status": 400, "message": "Event market type is not tradable for this provider", "data": null } ``` Framework fallback (malformed JSON, wrong parameter type, invalid ISO-8601 timestamp, `page_size` outside `1-1000` on paginated list endpoints (`/events`, `/competitions`, `/participants`, `/bets`), non-numeric values in CSV filters like `event_ids=abc`): ```json { "timestamp": "2026-05-26T14:37:46.202+00:00", "status": 400, "error": "Bad Request", "path": "/api/v2/bets" } ``` #### 401 Unauthorized Bearer token missing, malformed, or rejected. See [Authentication](./authentication) for the full message list. ```json { "status": 401, "message": "Missing or invalid Authorization header", "data": null } ``` #### 404 Not Found Unknown route, or an entity ID does not exist (for example, `GET /events/9999999999`). Some 404 responses on this path return an empty body, so match on the status code. ```json { "timestamp": "2026-05-26T14:36:30.235+00:00", "status": 404, "error": "Not Found", "path": "/api/v2/nonsense" } ``` #### 409 Conflict Returned by `POST /bets` when the `bet_reference` you sent already exists for your client with a **different** stake or price. The original bet is left unchanged and no duplicate is placed. Re-sending the same reference with the same stake and price instead returns the existing bet with a `201` (see [Place a bet](./walkthroughs/place-a-bet)). ```json { "status": 409, "message": "A bet with reference 'order-12345' already exists with different stake", "data": null } ``` #### 405 Method Not Allowed Wrong HTTP verb on a known route (for example, `POST /sports`). ```json { "timestamp": "2026-05-26T14:36:30.305+00:00", "status": 405, "error": "Method Not Allowed", "path": "/api/v2/sports" } ``` #### 415 Unsupported Media Type `POST /bets` sent without `Content-Type: application/json`, or with a different content type. ```json { "timestamp": "2026-05-26T14:37:46.633+00:00", "status": 415, "error": "Unsupported Media Type", "path": "/api/v2/bets" } ``` #### 500 Internal Server Error Unexpected server-side failure. Retry with exponential backoff; contact your integration manager if the failure persists. --- ### Notes - Multi-field validation errors are joined into a single `message` separated by `; `. - Empty response bodies on 404 are expected for some lookups. Always branch on the status code. - Framework-fallback responses do not currently expose a machine-readable error code; rely on the HTTP status. - Retry only `5xx` and idempotent reads. Never retry `POST /bets` on `4xx`. --- ## Get a bet by reference Look up a bet by the `bet_reference` you supplied when placing it. This lets you retrieve a bet using your own identifier instead of our bet id. If no bet matches the reference, a 404 is returned with the standard error envelope and the message `Bet not found`. Responses never echo your `bet_reference` back β€” store the reference-to-bet mapping on your side when you place the bet. Request --- ## Get a bet Fetch a single bet by its id. Returns the full bet record, including its current status and the selection it was placed on. Use the id returned to you by `POST /bets`. Request --- ## List bets Returns bets placed by your API key, filtered by time window, event, sport, and other dimensions. When no time filter is provided, the response defaults to all bets placed. Responses are paginated β€” every response includes a `pagination` object; use `page` and `page_size` to walk through the full result set. Request --- ## Get a competition Fetch a single competition by its id. Returns the same competition object you get from the list endpoint. Use this when you already hold a competition id and want its current details. Request --- ## List competitions Returns the competitions your API key has access to: leagues, tournaments, championships, etc. Filter by `sport_id` to scope to one sport; use `expand=sport` to embed the full sport object inline instead of issuing a follow-up call to `/sports`. Request --- ## Get event by ID Returns a single event by its `id`. Use `expand` to embed sport, competition, and participants inline. The response is a bare `EventDTO` object (no `data` wrapper), consistent with the other single-resource lookups. Request --- ## List events Returns events the API key has access to. Filter by sport, competition, or time window, or use `has_prices` to get back only events that have prices. Default behavior returns only `scheduled` events. Pass `status` explicitly to query live, finished, or cancelled events. Responses are paginated β€” every response includes a `pagination` object; use `page` and `page_size` to walk through the full result set. Use `expand` to embed related entities (sport, competition, participants) inline and avoid follow-up calls. Request --- ## Get a market Fetch a single market on an event by its id. Use `expand` to fold in selections, participants, or stake limits in the same call. This returns the same market shape as the markets list endpoint. Request --- ## List markets for an event Returns the markets priced for a single event. Each market represents a betting line (moneyline, totals, spreads, etc.) and has one or more selections (e.g. Home/Away, Over/Under). **Always pass `expand=selections` if you need pricing data.** Prices live inside each selection; without expanding, the response contains markets but no prices. Add `stake_limits` to `expand` to read each market's minimum and maximum stake before placing a bet. Prices come from the Vertex pricing provider (`pricing_provider_id: 1`). Request --- ## Get a participant Fetch a single participant by its id. Returns the same participant object you get from the list endpoint. Use this when you already hold a participant id and want its current details. Request --- ## List participants Returns the participants your API key has access to: teams and players that compete in events. Responses are paginated β€” the participant catalog runs to tens of thousands of records, so page through with `page` and `page_size`. Filter by `sport_id` to scope to one sport; use `expand=sport` to embed the full sport object inline instead of issuing a follow-up call to `/sports`. Request --- ## Get a selection Fetch a single selection by its id. The response includes the selection with its current Vertex price. Use this to refresh the price on one selection without re-fetching the whole market. Request --- ## Get a sport Fetch a single sport by its id. Returns the same sport object you get from the list endpoint, including its code. Use this when you already hold a sport id and want its current details. Request --- ## List sports Returns every sport your API key has access to. This is the entry point to the catalog: every pricing call and bet placement is ultimately scoped to a sport returned here. Use `expand` to fold related entities into the same call instead of issuing follow-up requests. Request --- ## 🧭 Overview ## REST API Overview HTTP endpoints for catalog discovery, pricing, and bet placement on Vertex. The REST API covers: - **Catalog**: sports, competitions, participants, and events - **Markets and prices**: markets, selections, and current prices per event - **Bets**: bet placement and bet history For real-time price updates, use the WebSocket feed instead of polling REST. ### Response shape List endpoints return their results in a `{ "data": [ ... ] }` wrapper. The four top-level lists β€” `GET /events`, `GET /competitions`, `GET /participants`, and `GET /bets` β€” are paginated: each response carries a `pagination` object (`page`, `size`, `total`) alongside `data`, pages hold 100 results by default, and `page_size` accepts up to `1000`. `GET /sports` and `GET /events/{id}/markets` are the exceptions: both return the full list in one response, with no `pagination` object. Single-resource lookups (the `/{id}` style endpoints, such as `GET /sports/{id}`, `GET /events/{eventId}`, `GET /selections/{selectionId}`, `GET /bets/{id}`, and `GET /bets/by-reference/{reference}`) return the bare object directly, with no `data` wrapper. ### Timestamps **Every timestamp the REST API returns is UTC**, formatted as ISO-8601 with a trailing `Z` β€” for example `2026-10-04T20:25:00Z`. This applies to `start_time`, `placed_at`, `updated_at`, and every other date-time field. There is no per-account timezone setting and no localisation: the API speaks UTC in both directions. Send UTC on the way in too. Date-time query parameters such as `start_from`, `start_to`, `placed_from`, and `placed_to` are interpreted as UTC and should carry the `Z`. The one exception anywhere in the Vertex API is `start_time` on the WebSocket `event` channel, which is UTC but arrives without the `Z` β€” see [Message Format](../websocket/messages.md#event_updates). ### Expanding related entities Several catalog and event endpoints accept an `expand` query parameter to fold related entities inline instead of issuing follow-up calls β€” for example `GET /events?expand=sport,competition,participants`. `expand` resolves a **single level** only. It embeds the requested top-level relations, but does not recursively expand relations nested inside them β€” for example, expanding `competition` on an event populates the competition object but not that competition's own `sport`. Request only the relations you need. ### Base URL All endpoints are served under `/api/v2`: `https://trading.vertex-api.com/api/v2` ### Rate limits There are no enforced rate limits today. Requests are governed by fair use. Sustained tight loops or high-volume polling may be throttled at the network edge without warning. Implement exponential backoff on any `5xx` response. If you expect to drive unusual volume, contact your integration manager so we can plan capacity. --- ## Place a bet Places a bet on a single selection at a given price. The request must reference a `selection_id` from `GET /events/{eventId}/markets?expand=selections`, a `pricing_provider_id` (always `1`, Vertex), and your bettor's `external_account_ref_tokens`. Your client identity is taken from the API key on the request. The `price` you submit must match the price your client observed for that selection at the Vertex pricing provider; the server validates it. If pricing has moved, the request is rejected. See the [Place a bet walkthrough](/docs/rest-api/walkthroughs/place-a-bet) for a full end-to-end example. Request --- ## Place a Bet(Walkthroughs) End-to-end walkthrough for placing a bet against Vertex: discover a sport, find a scheduled event, pick a selection, resolve your account references, and `POST /bets`. Every example references `$VERTEX_TOKEN` from the [Quickstart setup](../../quickstart). ### 1. List sports Confirm the sport you want to bet on is available. **curl** ```bash curl -s -H "Authorization: Bearer $VERTEX_TOKEN" \ "https://trading.vertex-api.com/api/v2/sports" ``` **JavaScript** ```js const BASE = 'https://trading.vertex-api.com/api/v2'; const headers = { Authorization: `Bearer ${process.env.VERTEX_TOKEN}` }; const sports = await fetch(`${BASE}/sports`, { headers }).then(r => r.json()); console.log(sports.data); ``` **Python** ```python import os, requests BASE = "https://trading.vertex-api.com/api/v2" headers = {"Authorization": f"Bearer {os.environ['VERTEX_TOKEN']}"} sports = requests.get(f"{BASE}/sports", headers=headers).json() print(sports["data"]) ``` Response: ```json { "data": [ { "id": 1, "name": "Soccer", "code": "soc" }, { "id": 6, "name": "Ice Hockey", "code": "ih" }, { "id": 5, "name": "American Football", "code": "usf" } ] } ``` We'll use **`6` (Ice Hockey)** for the rest of this walkthrough because it's reliably populated. ### 2. Find a scheduled event List upcoming events for your chosen sport. Use `expand=participants` to get team names. Without it the response only contains IDs. **curl** ```bash curl -s -H "Authorization: Bearer $VERTEX_TOKEN" \ "https://trading.vertex-api.com/api/v2/events?sport_id=6&status=scheduled&expand=participants" ``` **JavaScript** ```js const events = await fetch( `${BASE}/events?sport_id=6&status=scheduled&expand=participants`, { headers } ).then(r => r.json()); const event = events.data[0]; console.log(event.id, event.participants.map(p => `${p.role}: ${p.name}`)); ``` **Python** ```python events = requests.get( f"{BASE}/events", params={"sport_id": 6, "status": "scheduled", "expand": "participants"}, headers=headers, ).json() event = events["data"][0] print(event["id"], [(p["role"], p["name"]) for p in event["participants"]]) ``` Response (trimmed to one event, `pagination` object omitted): ```json { "data": [ { "id": 6717, "sport_id": 6, "competition_id": 11, "start_time": "2026-05-23T00:00:00Z", "status": "scheduled", "participants": [ { "id": 19, "name": "Colorado Avalanche", "role": "away", "type": "team" }, { "id": 11, "name": "Vegas Golden Knights", "role": "home", "type": "team" } ] } ] } ``` Capture the `id` of the event you want to bet on. :::tip[Status is lowercase] Event status values (`scheduled`, `live`, `finished`, `cancelled`) are returned in **lowercase**. ::: ### 3. Load markets and pick a selection Fetch the markets for your event with selections and stake limits expanded. Prices live inside each selection, so `expand=selections` is required, and `stake_limits` tells you how much you can stake before you build the bet. **curl** ```bash EVENT_ID=6717 curl -s -H "Authorization: Bearer $VERTEX_TOKEN" \ "https://trading.vertex-api.com/api/v2/events/$EVENT_ID/markets?expand=selections,stake_limits" ``` **JavaScript** ```js const eventId = 6717; const markets = await fetch( `${BASE}/events/${eventId}/markets?expand=selections,stake_limits`, { headers } ).then(r => r.json()); // Find the moneyline market with an active Vertex price (pricing_provider_id: 1). // Key off market_type.code: several market types (including player props) share segment.code 'result'. const moneyline = markets.data.find(m => m.market_type.code === 'moneyline'); const selection = moneyline.selections.find(s => s.prices.some(p => p.pricing_provider_id === 1 && p.status === 'active') ); const price = selection.prices.find(p => p.pricing_provider_id === 1); console.log({ selection_id: selection.id, price: price.price }); ``` **Python** ```python event_id = 6717 markets = requests.get( f"{BASE}/events/{event_id}/markets", params={"expand": "selections,stake_limits"}, headers=headers, ).json() # Key off market_type.code: several market types (including player props) share segment.code "result" moneyline = next(m for m in markets["data"] if m["market_type"]["code"] == "moneyline") selection = next( s for s in moneyline["selections"] if any(p["pricing_provider_id"] == 1 and p["status"] == "active" for p in s["prices"]) ) price = next(p for p in selection["prices"] if p["pricing_provider_id"] == 1) print({"selection_id": selection["id"], "price": price["price"]}) ``` Response (trimmed to one selection): ```json { "data": [ { "id": 63959, "event_id": 6717, "segment": { "id": 2, "code": "result", "name": "result", "scope": "event" }, "market_type": { "id": 1, "code": "moneyline", "name": "Moneyline", "category": "result" }, "status": "open", "live": false, "selections": [ { "id": 35928, "market_id": 63959, "name": "Away", "participant_id": 19, "prices": [ { "pricing_provider_id": 1, "price": 2.6747, "status": "active" } ] } ], "stake_limits": [ { "pricing_provider_id": 1, "participant_id": null, "min_stake": 10, "max_stake": 1000 } ] } ] } ``` :::tip[Every price you are served is bettable] Prices that are not tradable are filtered out before they reach you, so `status` is always `active` here and the `status === 'active'` test above never fails. It is kept in the examples as a cheap guard, not because the value varies. What changes instead is whether a selection appears at all. On this endpoint a selection with no tradable price is **omitted from the response**, so if the side you want is missing, it is not currently quoted β€” pick another market or event and check again later. (`GET /selections/{selectionId}` behaves differently: the selection is always returned and its `prices` array is empty instead.) A price can still be withdrawn between your read and your bet. There is no way to pre-empt that, so handle the `Selection price is not active` rejection covered in [Errors](../errors) and re-read the market. ::: Capture three things from the selection: - `selection.id` β†’ goes into the bet body as `selection_id` - The matching `price.price` value β†’ goes in as `price` - The matching `price.pricing_provider_id` β†’ goes in as `pricing_provider_id` #### Check the stake limits The market's `stake_limits` entry tells you the stake range the bet must fall in. `min_stake` applies directly. `max_stake` caps your stake at prices of `2.00` or higher and your potential profit below `2.00`, so the cap on your stake depends on the price: ```text If price >= 2.00: max_accepted_stake = max_stake If price < 2.00: max_accepted_stake = max_stake / (price - 1) ``` In this example (`max_stake: 1000`, price `2.6747`), you can stake up to `1000`. At a price of `1.50` on the same market you could stake up to `2000`, because the limit shifts to your potential profit below `2.00`. See [Stake Limits](../../concepts/stake-limits.md) for the full explanation. ### 4. Know your account identifiers **Your client identity comes from your API key.** The Bearer token on the request identifies your firm, so there's no client-ID field in the bet body. The body carries one account-related field that you control: | Field | Type | Where it comes from | | --- | --- | --- | | `external_account_ref_tokens` | string | **Your bettor's account reference**: a single string, despite the plural name. You choose this, typically your internal user ID or account handle. The first time we see a new value we persist it; subsequent bets with the same value attach to that bettor record. | If you only have one bettor (e.g. a single trading account), you can use a fixed string. For consumer-facing operators, pass your end-user's identifier so we can track bets per bettor. ### 5. Place the bet You now have everything you need: ```json { "selection_id": 35928, "pricing_provider_id": 1, "stake": 10, "price": 2.6747, "external_account_ref_tokens": "bettor-001", "bet_reference": "order-12345" } ``` **curl** ```bash curl -s -X POST \ -H "Authorization: Bearer $VERTEX_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "selection_id": 35928, "pricing_provider_id": 1, "stake": 10, "price": 2.6747, "external_account_ref_tokens": "bettor-001", "bet_reference": "order-12345" }' \ "https://trading.vertex-api.com/api/v2/bets" ``` **JavaScript** ```js const bet = await fetch(`${BASE}/bets`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ selection_id: 35928, pricing_provider_id: 1, stake: 10, price: 2.6747, external_account_ref_tokens: 'bettor-001', bet_reference: 'order-12345', }), }).then(r => r.json()); console.log('Placed:', bet.bet_id); ``` **Python** ```python bet = requests.post( f"{BASE}/bets", headers={**headers, "Content-Type": "application/json"}, json={ "selection_id": 35928, "pricing_provider_id": 1, "stake": 10, "price": 2.6747, "external_account_ref_tokens": "bettor-001", "bet_reference": "order-12345", }, ).json() print("Placed:", bet["bet_id"]) ``` On success the server responds with **HTTP 201** and: ```json { "bet_id": 299, "message": "Bet placed" } ``` You've placed a bet. Confirm it in your bet history: ```bash curl -s -H "Authorization: Bearer $VERTEX_TOKEN" \ "https://trading.vertex-api.com/api/v2/bets?page_size=5" ``` To fetch the bet you just placed directly, use the returned `bet_id` against `GET /bets/{id}`, which returns the bet object on its own (no `data` wrapper). #### Use bet_reference for safe retries `bet_reference` is your own idempotency key: pick a value that is unique per bet on your side (for example, your internal order ID). It makes retries safe: - Re-sending `POST /bets` with the same `bet_reference` **and the same stake and price** returns the existing bet (`201`, with the message `Bet already placed`). No duplicate is created. - Re-sending the same `bet_reference` with a **different stake or price** is rejected with `409 Conflict`. The original bet is left untouched. Retrieve a bet at any time with `GET /bets/by-reference/{reference}`, or by its numeric id with `GET /bets/{id}`: ```bash curl -s -H "Authorization: Bearer $VERTEX_TOKEN" \ "https://trading.vertex-api.com/api/v2/bets/by-reference/order-12345" ``` :::tip[Retrying a bet placement] If a `POST /bets` call returns a network error, timeout, or `5xx`, the request may have succeeded server-side. If you sent a `bet_reference`, you can safely retry with the **same** reference, stake, and price: you will get the original bet back rather than a duplicate. Without a `bet_reference`, do not retry blindly; query `GET /bets` to confirm state first. ::: ### What you just learned - List endpoints are wrapped in `{ data: [...] }`; single-resource lookups (like `GET /bets/{id}`) return the object directly. - Expansion is opt-in: `expand=participants` for team names on events, `expand=selections` for prices on markets, `expand=stake_limits` for the stake range. - A bet is identified by **selection Γ— price** against the Vertex pricing provider (`pricing_provider_id: 1`). - `stake` must fall within the market's [stake limits](../../concepts/stake-limits.md) (`min_stake` ≀ stake ≀ the price-adjusted maximum) and `price` must be at least `1.0`; otherwise the bet returns `400`. ### Next steps - **[REST API reference](..)**: every endpoint with request/response schemas and a built-in try-it-now. - **[Concepts](../../concepts)**: pricing, data hierarchy, stake limits. - **[WebSocket Feed](../../websocket)**: live price streaming. --- ## πŸ”‘ Authentication(Websocket) ## Authentication The WebSocket feed authenticates at the **handshake**, using the same API key as the REST API. The key is passed as a `token` query parameter on the connection URL. There is no login message and no `Authorization` header. ``` wss://trading-ws.vertex-api.com/ws?token= ``` :::note This differs from the REST API, which sends the key as an `Authorization: Bearer ` header. On the WebSocket feed the same key goes in the query string instead. There is no separate WebSocket credential. ::: ### How it works The server validates the token during the WebSocket upgrade, before the connection is established: - **Valid key**: the handshake completes (`101 Switching Protocols`) and the connection opens. The key's entitlements (which sports, competitions, and channels you may receive) are bound to the session. - **Missing, malformed, or revoked key**: the server rejects the upgrade with **HTTP 401** and no WebSocket connection is opened. This surfaces in most clients as a connection error at `connect()` time, not as a close frame. Because authentication happens at the handshake, an authentication failure is an HTTP error, not an in-stream message. Handle it where you handle connection failures. ### Entitlements The key determines what you can receive. Subscribing to a channel your key is not entitled to is rejected per-subscription rather than closing the connection (see [Subscriptions](./subscriptions.md)). Within an entitled channel, you receive only the sports and competitions your key covers. ### Token handling - Treat the token as a secret. Because it appears in the connection URL, avoid logging full WebSocket URLs and avoid embedding the token in client-side code that ships to browsers. - Always connect over `wss://` (TLS). The token is only protected in transit under TLS. - Partner API keys are long-lived and do not expire on a timer. If a key is rotated or revoked, existing connections using it will fail to re-establish. Reconnect with the new key. To request or rotate a key, contact your integration manager. --- ## ⚠️ WebSocket Errors ## WebSocket Errors The feed surfaces problems in three places, depending on when they occur: 1. **At the handshake**: authentication failures, reported as an HTTP status before the connection opens. 2. **In a reply**: a `subscribe` / `unsubscribe` that can't be honoured, reported with `status: "error"` while the connection stays open. 3. **In-stream**: a generic `error` message for malformed input or server-side problems. ### Handshake failures The token is validated during the WebSocket upgrade. A missing, malformed, or revoked key is rejected with **HTTP 401** and no WebSocket connection is opened. Most clients surface this as a connection error at `connect()` time, not as a close frame. Handle it where you handle connection failures, and do not retry with the same bad key. See [Authentication](./authentication.md). ### Subscribe / unsubscribe errors A subscribe or unsubscribe that can't be honoured is rejected with `status: "error"` and a `reason`. **The connection stays open.** Fix the request and resend. ```json { "type": "subscribe", "status": "error", "reason": "unknown channel: bet" } ``` | `reason` | Meaning | Client action | | --- | --- | --- | | `unknown channel: ` | The `channel` is not a recognised, subscribable channel. Only `event` and `market` are available to a partner key; any other value β€” including internal channels β€” is reported this way. The reply carries no `channel` field and does not distinguish an internal channel from an unrecognised name. | Subscribe only to `event` or `market`. | See [Subscriptions](./subscriptions.md) for the full subscribe model. ### In-stream errors For malformed input or server-side issues, the server sends a generic `error` message. The connection generally stays open. ```json { "type": "error", "message": "Unknown message type: subcribe" } ``` | Field | Type | Description | | --- | --- | --- | | `type` | string | Always `"error"` | | `message` | string | Human-readable explanation, intended for logging and debugging. Not a stable code, so do not branch on its text. | Common causes: - The message was not a JSON object with a `type` field. - The JSON could not be parsed. - The `type` was not a recognised message type. - The connection was not authenticated. These are almost always client bugs. Log the `message` and fix the offending request. ### Close codes When the server closes a connection it uses a standard WebSocket close code per [RFC 6455 Β§7.4](https://datatracker.ietf.org/doc/html/rfc6455#section-7.4). Treat a close as a signal to reconnect with backoff and re-subscribe (see [Reconnecting](./reconnect.md)); the close code tells you how cautious to be. | Code | Meaning | Client action | | --- | --- | --- | | `1000` | Normal closure (e.g. graceful server shutdown) | Reconnect with backoff. | | `1001` | Server going away (e.g. deployment) | Reconnect with backoff. | | `1006` | Abnormal closure: connection dropped without a close frame (network blip) | Reconnect with backoff. | A handshake authentication failure is reported as HTTP 401, not a close code, because it happens before the connection is established. --- ## πŸ’“ Heartbeat ## Heartbeat The feed supports an optional client-initiated application-level ping you can use to probe the connection. ### Optional application-level ping You can also probe the connection from the client with an application-level message. The server replies with a `pong` carrying its current server time in epoch milliseconds: ```json { "type": "ping" } ``` ```json { "type": "pong", "timestamp": 1747741200000 } ``` It is an ordinary JSON message you can use as an end-to-end liveness check or to measure round-trip latency. It is optional. --- ## βœ‰οΈ Message Format ## 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`. ```json { "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`](#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](./subscriptions.md), [Heartbeat](./heartbeat.md), and [Errors](./errors.md). 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` ```json { "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`](../rest-api/get-markets) 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](../concepts/stake-limits.md)) | #### `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: ```json { "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. ```json { "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](../concepts/stake-limits.md)) | 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: ```json { "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`: ```json { "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. - **`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](#suspended_market_updates). 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: ```js new Date(start_time + "Z") // JavaScript ``` ```python datetime.fromisoformat(start_time).replace(tzinfo=timezone.utc) # Python ``` ```java 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: ```json { "type": "event_sync", "published_at": "2026-05-30T14:00:05.123Z", "message": "event_change" } ``` When it arrives, reload your event list from [`GET /events`](../rest-api/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. --- ## πŸ“‘ WebSocket Overview ## WebSocket Overview The Vertex WebSocket feed is the real-time interface for consuming pricing and fixture data. It is push-based and low-latency, intended for integration into trading systems, risk engines, and odds displays. Use REST for catalog discovery and bet placement; use the WebSocket feed for everything that moves. ### What the feed delivers The feed exposes two channels: | Channel | Delivers | Typical volume | | --- | --- | --- | | `event` | Fixture lifecycle and metadata: competition, participants, start time, status | Low | | `market` | Market definitions, lines, and their selections with current prices, plus stake limits and their changes | Medium | These are the channels available to a partner API key. The feed carries internal channels that an external key is not entitled to subscribe to. Attempting to subscribe to one is rejected (see [Subscriptions](./subscriptions.md)). ### Timestamps **Every timestamp on the feed is UTC.** `published_at` and `updated_at` are ISO-8601 with a trailing `Z` β€” for example `2026-05-30T14:00:05.123Z`. The single exception is `start_time` on the `event` channel. It is still UTC, but it arrives **without the `Z`** (`2026-05-30T14:00:00`), so a parser that assumes local time will silently shift it. See [Message Format](./messages.md#event_updates) for how to parse it safely. ### When to use WebSocket vs REST | Use case | Interface | | --- | --- | | Continuous real-time price consumption | WebSocket | | Initial / bulk load of catalogs (sports, competitions, events) | REST | | Re-establishing state after a disconnect | REST (re-fetch; the feed has no replay, see [Reconnecting](./reconnect.md)) | | One-off lookup of a single event or market | REST | | Placing or checking bets | REST (bets are REST-only) | ### Connection URL Connect to `/ws` and pass your API key as a `token` query parameter: ``` wss://trading-ws.vertex-api.com/ws?token= ``` The token is the same key you use for the REST API, supplied differently. See [Authentication](./authentication.md). ### How a session works 1. Open the WebSocket connection with your token in the query string. The server authenticates at the handshake and rejects a bad key with HTTP 401 before the connection upgrades. 2. Send one or more `subscribe` messages to choose channels and optionally filter by event. 3. Receive a steady stream of update messages for everything you are subscribed to and entitled to. You can optionally probe liveness with an application-level ping (see [Heartbeat](./heartbeat.md)). ```mermaid sequenceDiagram participant Client participant Server as Vertex Feed Client->>Server: WebSocket handshake (wss://.../ws?token=...) Server-->>Client: 101 Switching Protocols Client->>Server: { "type": "subscribe", "channel": "market" } Server-->>Client: { "type": "subscribe", "status": "ok", ... } loop Steady state Server-->>Client: update messages (market_updates, ...) end Note over Client,Server: On disconnect, the client reconnects and
re-subscribes. There is no replay of missed messages. ``` The feed does not replay messages missed while disconnected and has no per-message sequence cursor. Reconnect handling is therefore a re-subscribe, not a resume. See [Reconnecting](./reconnect.md). --- ## πŸ”„ Reconnecting ## Reconnecting Disconnects are normal: network blips, client restarts, deployments. This page explains exactly what the feed does and does not do on reconnect, so you can keep your local state correct. ### What the feed does not do The feed has **no replay and no resume**. Specifically: - There is **no message buffer or cursor.** The server does not retain messages for you while you are disconnected, and there is no "resume from where I left off" mechanism. - There are **no per-message sequence numbers.** You cannot detect a gap by comparing message IDs. - **Subscriptions are dropped on disconnect.** When the connection closes, the server forgets what you were subscribed to. The practical consequence: **any messages sent while you were disconnected are gone.** After a reconnect your local state may be stale, and the feed will not tell you which entities changed in the gap. ### How to reconnect correctly 1. **Reconnect** to `/ws` with your token (see [Authentication](./authentication.md)), with a backoff between attempts. 2. **Re-subscribe** to every channel and filter you need. Send the full filter set; subscriptions do not carry over (see [Subscriptions](./subscriptions.md)). 3. **Re-establish a baseline from REST** for anything where staleness matters. Because no messages are replayed, fetch the current state of your subscribed events/markets via the REST API, then apply live WebSocket updates on top of it. If you maintain your subscription list on the client, step 2 is just resending it. The `subscriptions` message lets you confirm what the server now has after you re-subscribe: ```json { "type": "subscriptions" } ``` Note that `subscriptions` reports current subscription state only. It does not replay any data. ### Recommended pattern For state that must be accurate (e.g. current price per selection), treat **REST as the source of truth for the baseline** and the **WebSocket feed as the source of changes**: - On first connect, and on every reconnect, load the baseline from REST. - Apply WebSocket updates as they arrive. - On any disconnect, discard nothing prematurely, but re-baseline from REST on reconnect before trusting your local state again. For low-frequency, non-critical data you may simply re-subscribe and let the next update refresh your view. ### The `event_sync` hint On the `event` channel the server may send an `event_sync` message (`{ "type": "event_sync", "message": "event_change" }`). It carries no data. It means event-level data has changed: reload your event list from `GET /events` and reconcile it with what you hold, without waiting for a disconnect. It arrives even if your `event` subscription is filtered by `event_ids`. See [Message Format](./messages.md#event_sync). --- ## πŸ”” Subscriptions ## Subscriptions After connecting, the client chooses what to receive by sending `subscribe` messages. Subscriptions are **dynamic**: you can add, change, or drop them at any time over the life of the connection, without reconnecting. ### Subscribing Send a `subscribe` message naming the channel. Optionally restrict it to specific events with `event_ids`: ```json { "type": "subscribe", "channel": "market", "event_ids": [12345, 12346] } ``` Omit `event_ids` (or send an empty array) to receive **all** updates on that channel that your key is entitled to: ```json { "type": "subscribe", "channel": "market" } ``` The server replies with an acknowledgement echoing the **effective** filter that is now active: ```json { "type": "subscribe", "status": "ok", "channel": "market", "effective": { "event_ids": [12345, 12346] } } ``` A wildcard subscription comes back as `"effective": { "event_ids": [] }`. The filter applies to every data message on the channel except [`event_sync`](./messages.md#event_sync), which goes to all `event` channel subscribers regardless of `event_ids`. :::note[Don't assume the ack arrives first] On a busy feed, data messages can arrive before the subscribe acknowledgement. Branch on `type` rather than assuming the first message after subscribing is the ack. ::: #### Subscribable channels A partner key may subscribe to `event` and `market`. These are described in [Message Format](./messages.md). Subscribe to one channel per message; send multiple messages to subscribe to several. ### Resubscribing replaces, it does not add :::warning A second `subscribe` on a channel **replaces** the previous filter for that channel. It does not merge with it. Always send the full set of `event_ids` you want. ::: For example, to watch events `12345` then add `99999`, the second message must list both: ```json { "type": "subscribe", "channel": "market", "event_ids": [12345] } ``` ```json { "type": "subscribe", "channel": "market", "event_ids": [12345, 99999] } ``` Sending `{ "type": "subscribe", "channel": "market", "event_ids": [99999] }` as the second message would **drop** `12345`. Track the desired filter set on the client and always send it in full. ### Unsubscribing Stop one channel: ```json { "type": "unsubscribe", "channel": "market" } ``` ```json { "type": "unsubscribe", "status": "ok", "channel": "market" } ``` Stop everything on the connection at once: ```json { "type": "unsubscribe_all" } ``` ```json { "type": "unsubscribe_all", "status": "ok" } ``` ### Inspecting current subscriptions Ask the server what the connection is currently subscribed to. This is useful for reconciling client state after a reconnect: ```json { "type": "subscriptions" } ``` ```json { "type": "subscriptions", "data": [ { "channel": "market", "event_ids": [12345, 12346] }, { "channel": "event", "event_ids": [] } ] } ``` This reports current subscription state only. It does **not** replay any data. See [Reconnecting](./reconnect.md). ### Rejected subscriptions A subscribe that the server cannot honour is rejected with `status: "error"`; the connection stays open. Fix the request and resend. | `reason` | Meaning | | --- | --- | | `unknown channel: ` | The `channel` is not a recognised, subscribable channel. Only `event` and `market` are available to a partner key; any other value β€” including internal channels β€” is reported this way. The reply carries no `channel` field and does not distinguish an internal channel from an unrecognised name. | ```json { "type": "subscribe", "status": "error", "reason": "unknown channel: bet" } ``` --- ## Changelog This page records changes to the Vertex API β€” the REST endpoints, the WebSocket feed, and the developer documentation itself. New capabilities, updated behaviour, and any deprecations or breaking changes are posted here as they ship, most recent first. Check back here when integrating against a new capability or before upgrading. ### Release 8 β€” 2026-11-02 A new filter on `GET /events`. Existing requests are unaffected. #### Added - **`has_prices` on `GET /events`.** Set `has_prices=true` to get back only events that have prices available to you. It defaults to `false`, so requests without it return the same events as before. See [`GET /events`](/docs/rest-api/get-events). ### Release 7 β€” 2026-10-15 Stake limits now arrive with each market update on the WebSocket `market` channel. Existing fields are unchanged. #### Added - **`stake_limits` on `market_updates`.** Each market in a `market_updates` frame now carries its current stake limits, with the same shape and values that `GET /events/{eventId}/markets?expand=stake_limits` returns for your API key. You no longer need a REST call to read a market's limits when it opens. `suspended_market_updates` never carry them. See [`market_updates`](/docs/websocket/messages#market_updates) and [Stake Limits](/docs/concepts/stake-limits). ### Release 6 β€” 2026-09-25 The WebSocket `event` channel now tells you when an event stops trading and when it ends. **Check your integration before this release: some messages now arrive with fewer fields than before.** #### Added - **`active` on `event_updates`.** An update with `active: false` means the event has stopped trading. Stop quoting every price you hold for it straight away. Previously these updates were not sent at all, so nothing on the feed marked the moment an event stopped trading. `status` is not a reliable signal for this: at kickoff it may already read `live`. See [`event_updates`](/docs/websocket/messages#event_updates). - **Final statuses after an event stops trading.** You now receive the event's final status, such as `finished`, even after it has gone inactive. Previously these were dropped, and the only sign an event had ended was that it disappeared from `GET /events`. - **`event_sync` is now sent.** It was documented before but was never actually sent. When it arrives, reload your event list from `GET /events`. It goes to every `event` subscriber, including subscriptions filtered by `event_ids`. - **`sport_id` and `competition_id` on `event_updates`**, and a `type` (`team` or `player`) on each participant, matching `GET /events`. - **`active` on `GET /events` and `GET /events/{eventId}`.** It is always `true` there, because inactive events are not returned. #### Updated - **`event_updates` items are patches.** A missing field means "unchanged" β€” keep the value you hold, and do not clear it. An update for an event that has stopped trading carries only `id`, `status` and `active: false`. `participants` is sent when an event becomes available to your key, and left out otherwise. - **`suspended_market_updates` can arrive with identifiers only.** When the market's event is no longer available to your key, the message carries only market, selection and price ids with their statuses. Everything else is `null` or missing. Drop the prices for those ids as usual. **Make sure your client does not reject this shape.** - **The event update comes first, and suspensions may follow.** When an event stops trading, the `event_updates` item with `active: false` is sent first, and the market suspensions follow it. Act on the event update. Don't wait for the suspensions. - **Correction: `participants` on `event_updates`.** These docs showed `participants` as always populated. Since June 2026 it has been `null` on every update. It is now sent as described above. ### Release 5 β€” 2026-09-16 A documentation correction. No API behaviour changed β€” the values described below are what the feed has always sent, and were documented incorrectly until now. #### Updated - **`start_time` on the WebSocket `event` channel is UTC.** It was previously described as an "ISO-8601 local datetime", which was wrong and actively misleading: the value has always been UTC. The wire format is unchanged β€” it arrives without a trailing `Z` (`2026-05-30T14:00:00`), which is the only place in the Vertex API where a timestamp omits the designator. **Check how your client parses it.** Most date libraries read a zone-less string as the reader's local time, which shifts the kickoff by your own offset and can place it on the wrong date; an integration that followed the previous wording is likely doing exactly that. The [Message Format](/docs/websocket/messages#event_updates) page now carries parse-as-UTC examples, and `GET /events` returns the same instant with the `Z` attached as a cross-check. - **Added a Timestamps section to the [REST](/docs/rest-api/overview) and [WebSocket](/docs/websocket/overview) overviews**, stating the convention in one place: every timestamp in the Vertex API is UTC, in both directions. ### Release 4 β€” 2026-09-10 Documentation corrections. No API behaviour changed in this release β€” the behaviour described below has been in place since July 2026, and was documented incorrectly until now. #### Updated - **A price you are served is always tradable, and `status` is always `active`.** Prices that are not tradable are filtered out before they reach a partner API key, on the REST endpoints and on the WebSocket `market_updates` stream. The previous wording said suspended prices were "shown for context", which was wrong. If your integration branches on `status == "suspended"` on those surfaces, that branch never runs, and a client regenerated from the schema will now see `status` as a single-valued enum β€” a strongly typed comparison against `"suspended"` will no longer compile. - **`suspended_market_updates` is the exception, and it is how withdrawals reach you.** That message is deliberately not filtered: the prices inside it carry `status: "suspended"`. Treat it as the instruction to drop the prices you are holding for those selections. You will not get a `market_updates` frame saying the same thing. - **An empty `prices` array means different things on different endpoints.** `GET /selections/{selectionId}` returns the selection with `prices: []` when nothing is quoted. The markets endpoints instead omit the selection entirely, so a selection you receive there always carries a price. - **Documented the rejection you get when a price is withdrawn between your read and your bet:** `Selection price is not active`. This is distinct from `Event market type is not tradable for this provider`, which means the provider has stopped trading that market type and will not clear on a re-read. ### Release 3 β€” 2026-09-09 This release documents a new price freshness timestamp. The change is additive β€” no existing integration is affected. #### Added - Every price on a selection now carries `updated_at` β€” the ISO-8601 UTC timestamp of the last write to that price. It is stamped with the same instant carried by the matching WebSocket selection-price update, so a REST read and a streamed update for the same price agree. Use it to tell a fresh quote from a stale one. Returned by `GET /selections/{selectionId}`, `GET /events/{eventId}/markets`, and `GET /events/{eventId}/markets/{marketId}`. ### Release 2 β€” 2026-08-06 This release documents new `GET /bets` filtering capabilities and a new WebSocket freshness timestamp. All changes are additive β€” no existing integration is affected. #### Added - Every WebSocket data update now carries a root-level `published_at` β€” the server-set publish time as an ISO-8601 UTC timestamp with fixed three-digit millisecond precision, so raw string comparison orders updates chronologically. Use it to order updates and measure feed freshness. See the WebSocket messages reference. - `GET /bets` supports a new optional `market_side_filters` parameter for per-market side filtering: each market-definition group carries its own set of sides (for example home-only Moneyline bets and away-only Spread bets in a single query). When present it supersedes the flat `market_definition_ids` parameter β€” see the `GET /bets` reference for the format and valid side values. - `GET /bets` supports the `statuses` filter β€” a comma-separated list of bet statuses (for example `open,won`) to narrow bet history by outcome. - Bet responses include a `profit` field: total won minus stake (`potential_payout βˆ’ stake` on a win, `βˆ’stake` on a loss, `0` on a void). ### Release 1 β€” 2026-07-06 This release improves the accuracy and completeness of the Vertex API documentation, with fuller pagination and `expand` coverage, newly documented `GET /bets` filters, and clearer response schemas. #### Updated - `GET /bets` now documents the `market_type_codes` and `team_names` filters. - `expand` is now documented on `/selections/{selectionId}`, `/competitions`, and `/participants`, letting you embed related objects and avoid follow-up calls. - Pagination is now fully documented across all list endpoints (`/events`, `/competitions`, `/participants`, `/bets`): 100 results per page by default, `page_size` up to 1000. Page through to retrieve the complete set β€” `/participants` alone runs to tens of thousands of records, so make sure your integration pages through all results. - `expand` embeds are additive β€” the foreign-key id is returned alongside the embedded object. - `status` and `result` values are documented in lowercase (for example `scheduled`, `open`, `win`) to match API responses. - `market_type` is documented as always present on markets. - `bet_reference` on `POST /bets` is documented as optional and write-only β€” accepted on submission, never returned in a response. #### Removed - Documentation for fields and parameters the API does not support: a `name` field on events, an `account` filter, and the `paginated` parameter on `/bets`. #### Documentation site - The interactive "Try it" console now remembers your API key across every endpoint page for the duration of your browser session, so you only need to enter it once.