API reference · Contract 2.1.0
Pricing intelligence,
built for integration.
Submit only the complete tennis market entries you want priced, receive an asynchronous prediction job, and retrieve exactly those inferred model targets.
Base URL
https://marketengine.statsmatics.comQuick start
The client-facing flow stays small. The service owns match mapping, queueing, retries, validation, and response normalization.
Get credentials
Your API key is issued once from the private operations console.
Submit a snapshot
Send the documented JSON closing-market history.
Poll the job
Read the status URL until the job is COMPLETED or FAILED.
The API returns pricing and probabilities only. It does not produce BET / NO_BET decisions.
Authentication
Send the key in every capabilities, submission, polling, and in-play discovery request. The in-play WebSocket takes it in the first frame instead. Keep it server-side and never embed it in browser code.
X-API-Key: tpa_live_your_keyKeys are client-scoped.Your key can only retrieve jobs created for the same client.
Access follows your agreement.Your API key works for the services enabled for your account. Any other endpoint returns 403.
Rotation is immediate.A rotated or revoked key stops authenticating without a grace period.
GET /v2/inplay/matches
Live in-play pricing starts with discovery. List live or upcoming matches, or every match for one UTC date, and keep the returned match_id for the WebSocket subscription.
/v2/inplay/matches?status=live200 OK| Query (exactly one) | Returns |
|---|---|
status=live | Matches currently live. |
status=upcoming | Upcoming matches. |
date=YYYY-MM-DD | Upcoming, live, completed, and cancelled matches for one UTC date. |
curl 'https://marketengine.statsmatics.com/v2/inplay/matches?status=live' \
--header 'X-API-Key: tpa_live_your_key'Response
{
"matches": [
{
"match_id": "194136",
"players": {
"p1": { "name": "Rigele Te" },
"p2": { "name": "Bernard Tomic" }
},
"status": "live",
"gender": "men",
"draw": "singles",
"format": "BO3",
"surface": "hard",
"tournament": "Hangzhou",
"scheduled_time": "2026-09-22T09:00:00.000Z"
}
]
}| Field | Client use |
|---|---|
match_id | Use this exact string in the WebSocket subscribe frame. |
players.p1 / players.p2 | Player names to display. |
status / draw | Choose a live or upcoming singles match. |
format / gender | Select the correct client workflow. |
surface / tournament / scheduled_time | Match context for display. |
Use the match_id returned by discovery exactly as given. Do not invent or transform it. Dates are UTC calendar dates.
400 INVALID_QUERY when the query is not exactly one of the forms above; 503 MATCH_LIST_UNAVAILABLE when the match list is temporarily unavailable. Retry the 503 after a short wait.
WSS /v2/inplay/ws
Open one WebSocket, authenticate with the first frame, then subscribe to one or more matches. Each match starts with a snapshot and then receives a complete prices message whenever the priced state changes.
wss://marketengine.statsmatics.com/v2/inplay/ws101 Switching ProtocolsAuthenticate
Send the API key in the first JSON frame. Nothing else is accepted before it.
Subscribe
Send one subscribe frame per match_id from discovery. One connection can hold several matches.
Receive prices
Route every message by match_id: snapshot first, then prices, status, and completed.
Authenticate
→ send (first frame)
{
"type": "authenticate",
"request_id": "auth-1",
"api_key": "tpa_live_your_key"
}
← receive
{ "protocol": "live.v1", "type": "ack", "request_id": "auth-1", "status": "authenticated" }Subscribe and unsubscribe
→ send (one frame per match)
{ "type": "subscribe", "match_id": "194136" }
{ "type": "subscribe", "match_id": "194137" }
← receive
{ "protocol": "live.v1", "type": "ack", "match_id": "194136", "status": "pending" }
{ "protocol": "live.v1", "type": "ack", "match_id": "194137", "status": "subscribed" }
→ stop a match
{ "type": "unsubscribe", "match_id": "194137" }Snapshot and price updates
The default subscription returns MONEYLINE, HANDICAP, and TOTAL markets together: match Moneyline, applicable set Moneylines, seven Handicap lines, and seven Total lines. The main Handicap and Total lines are closest to 50/50, with three whole-game steps on each side. This excerpt shows one line of each type; prices messages carry the same complete markets array.
{
"protocol": "live.v1",
"type": "snapshot",
"match_id": "194136",
"players": [
{ "position": "PLAYER_1", "name": "Rigele Te" },
{ "position": "PLAYER_2", "name": "Bernard Tomic" }
],
"status": "live",
"calibration_status": "SOLVED",
"price_timestamp_utc": "2026-09-22T10:15:30.123Z",
"markets": [
{
"type": "MONEYLINE", "period": "MATCH",
"selections": [
{ "selection": "PLAYER_1", "line": null, "name": "Rigele Te", "model_probability": 0.413, "decimal_price": 2.421 },
{ "selection": "PLAYER_2", "line": null, "name": "Bernard Tomic", "model_probability": 0.587, "decimal_price": 1.704 }
]
},
{
"type": "HANDICAP", "period": "MATCH",
"selections": [
{ "selection": "PLAYER_1", "line": -2.5, "name": "Rigele Te", "model_probability": 0.524, "decimal_price": 1.908 },
{ "selection": "PLAYER_2", "line": 2.5, "name": "Bernard Tomic", "model_probability": 0.476, "decimal_price": 2.101 }
]
},
{
"type": "TOTAL", "period": "MATCH",
"selections": [
{ "selection": "OVER", "line": 22.5, "name": "Over 22.5", "model_probability": 0.548, "decimal_price": 1.825 },
{ "selection": "UNDER", "line": 22.5, "name": "Under 22.5", "model_probability": 0.452, "decimal_price": 2.212 }
]
}
]
}Future-set Moneylines are conditional on that set being played. Completed or impossible sets are not priced.
Messages you receive
| type | status | Meaning |
|---|---|---|
ack | authenticated · subscribed · pending · unsubscribed | Reply to your authenticate, subscribe, or unsubscribe frame. |
snapshot | live | First complete markets array for a match, including player names. |
prices | live | Complete markets array, sent whenever the priced state changes. |
status | pending | A current price is being prepared. Wait for the next snapshot or prices message. |
status | live | Prices are current again and unchanged since the last prices message. |
status | unavailable | The match cannot be priced for you now. Refresh discovery and choose an available match. |
completed | completed | The match has finished. Stop consuming it and keep the final update. |
Connection recovery
| Condition | Client action |
|---|---|
| Invalid or revoked key | The socket closes. Obtain a valid API key and authenticate again. |
| Match unavailable | Refresh the match list and choose an available match. |
| Service temporarily unavailable | Wait and continue when the service sends the next current price. |
| Match completed | Stop consuming prices for that match and retain the final update. |
| Socket closes | Reconnect, authenticate, and send the same match subscriptions again. Continue from the fresh snapshot. |
Server-side only.Connect from your server. Connections that send a browser Origin header are refused, and the key must never appear in a URL, subprotocol, or log.
Subscription limit.Each client can follow a limited number of matches at the same time. A subscription beyond that limit is reported as unavailable.
Command rate.Send at most 60 commands per 10 seconds on one connection. Invalid frames or a higher rate close the socket.
Recovery is handled by the service.Feed recovery and state are managed server-side. You only reconnect, authenticate, and resubscribe.
POST /v2/prediction-jobs
Send the documented JSON body with your managed API key. Every accepted submission creates a new job.
/v2/prediction-jobs202 Acceptedcurl --request POST 'https://marketengine.statsmatics.com/v2/prediction-jobs' \
--header 'X-API-Key: tpa_live_your_key' \
--data '{
"event": {
"event_id": "35321298",
"event_name": "Guillen Meza v Mi Damas"
},
"players": [
{ "position": "PLAYER_1", "name": "Alvaro Guillen Meza" },
{ "position": "PLAYER_2", "name": "Miguel Damas" }
],
"market_entries": [
{
"market_id": "1.254640337",
"market_type": "MATCH_HANDICAP",
"market_name": "Match Handicap -2.5",
"clk": "4491333115",
"pt": 1772568507587,
"market_time": "2026-03-03T20:00:00.000Z",
"timezone": "UTC",
"in_play": false,
"status": "OPEN",
"selections": [
{ "selection_id": "15745245", "name": "Alvaro Guillen Meza", "line": -2.5, "ltp": 1.91 },
{ "selection_id": "15313770", "name": "Miguel Damas", "line": 2.5, "ltp": 1.99 }
]
},
{
"market_id": "1.254640336",
"market_type": "MATCH_ODDS",
"market_name": "Match Odds",
"clk": "4491333114",
"pt": 1772568507586,
"market_time": "2026-03-03T20:00:00.000Z",
"timezone": "UTC",
"in_play": false,
"status": "OPEN",
"selections": [
{ "selection_id": "15745245", "name": "Alvaro Guillen Meza", "line": null, "ltp": 1.47 },
{ "selection_id": "15313770", "name": "Miguel Damas", "line": null, "ltp": 3.05 }
]
},
{
"market_id": "1.254640336",
"market_type": "MATCH_ODDS",
"market_name": "Match Odds",
"clk": "4491362894",
"pt": 1772568624002,
"market_time": "2026-03-03T20:00:00.000Z",
"timezone": "UTC",
"in_play": true,
"status": "OPEN",
"selections": []
}
],
"metadata": {
"event_type": "Challenger Men Singles",
"tournament_name": "Brasilia 2",
"round": "1/16-finals",
"surface": "Clay",
"indoor": false,
"gender": "Male"
}
}'Accepted response
{
"job_id": "job_d6faa1ae",
"prediction_id": null,
"status": "QUEUED",
"stage": "VALIDATION_COMPLETE",
"status_url": "/v2/prediction-jobs/job_d6faa1ae",
"data": null,
"error": null,
"request_id": "req_93cce4c2",
"accepted_at": "2026-07-31T09:30:00Z"
}POST /v2/future-prediction-jobs
Use the additive future-fixture endpoint when the match is not yet present in the normal canonical feed. Send player names, tournament context and exact market targets; all existing pre-game routes remain unchanged.
/v2/future-prediction-jobs202 AcceptedEach player requires position and name. The three-letter country_code is optional. Send a supported code when available; do not send a full country name.
Supported player country codes
Codes are case-insensitive on input and normalized to uppercase.
ABW AFG AGO ALB AND ARE ARG ARM ATG AUS AUT AZE BDI BEL BEN BFA BGD BGR BHR BHS BIH BLR BMU BOL BRA BRB BRN BTN BWA CAN CHE CHL CHN CIV CMR COD COG COL CRI CUB CUW CYP CZE DEU DJI DNK DOM DZA ECU EGY ESP EST ETH FIN FJI FRA GAB GBR GEO GHA GRC GTM GUM HKG HND HRV HTI HUN IDN IND IRL IRN IRQ ISL ISR ITA JAM JOR JPN KAZ KEN KGZ KHM KOR KWT LAO LBN LBY LCA LIE LKA LSO LTU LUX LVA MAC MAR MCO MDA MDG MDV MEX MKD MLI MLT MMR MNE MNG MOZ MRT MUS MYS NAM NCL NGA NIC NLD NOR NPL NZL PAK PER PHL PNG POL PRI PRT PRY QAT ROU RUS RWA SAU SDN SEN SGP SLV SMR SRB SVK SVN SWE SYC SYR TGO THA TJK TKM TPE TTO TUN TUR TZA UGA UKR URY USA UZB VEN VIR VNM WLD XKX YEM ZAF ZWETournament fields
name, event_type, country_code, city, surface, indoor, gender and best_of_sets are required. round is optional. scheduled_at is required at the request root.
If one compatible canonical tournament can be identified, its tournament-specific serving history is used. Otherwise the future job continues with the validated event type, surface, indoor status, gender and format supplied in the request, while the tournament-edition serving-residual feature is set to zero. Player mapping and player strengths are still required.
Request schema
The request body is strictly validated. All identifiers are strings, source publish time stays in pt, human-readable timestamps use UTC, and missing optional values are explicit JSON nulls.
| JSON path | Type | Required | Rule |
|---|---|---|---|
event.event_id | string | Yes | Source event identifier. |
event.event_name | string | Yes | Non-empty event label. |
players | array[2] | Yes | Exactly one PLAYER_1 and one PLAYER_2. |
players[].name | string | Yes | Used with the UTC match date for mapping. |
market_entries | array | Yes | Declares the targets to price and must include the first in_play=true marker. |
market_entries[].pt | int64 | Yes | Source publish time as Unix epoch milliseconds. |
market_entries[].clk | string | Yes | Opaque source stream cursor; never parse it as time. |
market_entries[].status | enum | Yes | OPEN, SUSPENDED, or CLOSED. |
market_entries[].selections | array | Yes | Complete two-sided target pairs; may be empty only on the in-play marker. |
selections[].line | number | null | Yes | Exact handicap/total line for that side; null for moneyline. |
selections[].ltp | number | null | No | Optional source price. A null value still requests model pricing for that line. |
metadata | object | null | No | Optional object; accepted fields are listed below. |
Optional metadata
metadata may be omitted or set to null. When supplied, it accepts the nullable fields below. Omit values you do not know.
| Field | Type |
|---|---|
event_type | string | null |
tournament_name | string | null |
round | string | null |
surface | string | null |
indoor | boolean | null |
gender | string | null |
Normalize source market deltas before submission
MarketChangeMessages are incremental. Apply each mc update to your cached market state, then emit normalized snapshots only for the supported markets you want priced. A definition-only history contains no source odds until a runner change supplies ltp.
| Source stream path | Request path | Rule |
|---|---|---|
message.clk | market_entries[].clk | Opaque cursor; preserve as a string. |
message.pt | market_entries[].pt | Publish time in Unix epoch milliseconds. |
mc[].id | market_entries[].market_id | Stringify the source market identifier. |
marketDefinition | market_type, market_name, market_time, timezone, in_play, status | Carry the latest definition forward across deltas. |
marketDefinition.eventId / eventName | event.event_id / event.event_name | The normalized request contains one event. |
marketDefinition.runners[] + rc[] | selections[] | Join by runner id and handicap; carry the latest valid LTP forward. |
rc[].ltp | selections[].ltp | Source price; null when absent or invalid. |
The earliest normalized entry with in_play=true establishes match start using its pt. If no complete pre-match Moneyline pair exists, the pricing engine skips closing-price calibration and uses its unadjusted model.
Market selection
Send only the complete pre-in-play market entries you want priced. The service infers, validates, and deduplicates every exact type, period, and two-sided line; it never guesses or substitutes another line.
Supported input market types
Only the values and aliases listed below are part of the public contract. Set markets must identify a set from 1 through 5 in market_name.
Accepted input market_type | Name and selection requirement | Inferred output |
|---|---|---|
MATCH_ODDS | Match market; two player selections with null lines. | MONEYLINE / MATCH |
SET_WINNER | Set number 1-5 in market_name; two player selections with null lines. | MONEYLINE / SET_n |
HANDICAP or MATCH_HANDICAP | Match market; two player selections with opposite half-point lines. | HANDICAP / MATCH |
SET_HANDICAP | Set number 1-5 in market_name; two player selections with opposite half-point lines. | HANDICAP / SET_n |
COMBINED_TOTAL or MATCH_TOTALS | Match market; OVER and UNDER at the same positive half-point line. | TOTAL / MATCH |
SET_TOTALS | Set number 1-5 in market_name; OVER and UNDER at the same positive half-point line. | TOTAL / SET_n |
MONEYLINE
Win pricing for each player
Null line · PLAYER_1 / PLAYER_2Supported periodsMATCH · SET_1 · SET_2 · SET_3 · SET_4 · SET_5HANDICAP
Spread pricing at the supplied player lines
Per-player line · PLAYER_1 / PLAYER_2Supported periodsMATCH · SET_1 · SET_2 · SET_3 · SET_4 · SET_5TOTAL
Over/under pricing at the supplied total
Numeric line · OVER / UNDERSupported periodsMATCH · SET_1 · SET_2 · SET_3 · SET_4 · SET_5Exact target entries
Each complete supported pair in a strictly pre-in-play entry becomes one output. Repeated snapshots of the same target are priced once.
"market_entries": [
{
"market_id": "1.254640337",
"market_type": "MATCH_HANDICAP",
"market_name": "Match Handicap -2.5",
"clk": "4491333115",
"pt": 1772568507587,
"market_time": "2026-03-03T20:00:00.000Z",
"timezone": "UTC",
"in_play": false,
"status": "OPEN",
"selections": [
{ "selection_id": "15745245", "name": "Player A", "line": -2.5, "ltp": null },
{ "selection_id": "15313770", "name": "Player B", "line": 2.5, "ltp": null }
]
},
{
"market_id": "1.254640339",
"market_type": "MATCH_TOTALS",
"market_name": "Match Total Games 22.5",
"clk": "4491333117",
"pt": 1772568507589,
"market_time": "2026-03-03T20:00:00.000Z",
"timezone": "UTC",
"in_play": false,
"status": "OPEN",
"selections": [
{ "selection_id": "over", "name": "Over 22.5", "line": 22.5, "ltp": null },
{ "selection_id": "under", "name": "Under 22.5", "line": 22.5, "ltp": null }
]
}
]Line rules
There are no automatic defaults or closest-line substitutions. Unsupported, partial, or malformed target entries are rejected.
MONEYLINE → complete PLAYER_1 / PLAYER_2 pair, line null
HANDICAP → complete opposite half-point player lines
TOTAL → complete OVER / UNDER pair at one positive half-point
ltp null → target is still priced by the pricing engineGET /v2/prediction-jobs/{'{job_id}'}
The poll endpoint returns HTTP 200 for a found job, including terminal failures. Use the response state and error object—not the HTTP status alone.
curl 'https://marketengine.statsmatics.com/v2/prediction-jobs/job_d6faa1ae' \
--header 'X-API-Key: tpa_live_your_key'Completed response
{
"job_id": "job_d6faa1ae",
"prediction_id": "pred_218f1619",
"status": "COMPLETED",
"stage": "COMPLETED",
"data": {
"match": { "event_id": "35321298", "match_date": "2026-03-03" },
"markets": [
{
"type": "MONEYLINE",
"period": "MATCH",
"source_odds_available": true,
"selections": [
{
"selection": "PLAYER_1",
"line": null,
"name": "Alvaro Guillen Meza",
"source_price_timestamp_utc": "2026-03-03T20:08:27.586Z",
"model_probability": 0.684,
"decimal_price": 1.462
},
{
"selection": "PLAYER_2",
"line": null,
"name": "Miguel Damas",
"source_price_timestamp_utc": "2026-03-03T20:08:27.586Z",
"model_probability": 0.316,
"decimal_price": 3.165
}
]
},
{
"type": "HANDICAP",
"period": "MATCH",
"source_odds_available": false,
"selections": [
{
"selection": "PLAYER_1",
"line": -2.5,
"name": "Alvaro Guillen Meza",
"source_price_timestamp_utc": null,
"model_probability": 0.524,
"decimal_price": 1.908
},
{
"selection": "PLAYER_2",
"line": 2.5,
"name": "Miguel Damas",
"source_price_timestamp_utc": null,
"model_probability": 0.476,
"decimal_price": 2.101
}
]
}
]
},
"error": null,
"request_id": "req_93cce4c2"
}Each returned selection carries its own line. This keeps PLAYER_1 and PLAYER_2 handicap lines explicit and prevents unrequested distribution lines from appearing.
Returned PLAYER_1 and PLAYER_2 follow the resolved canonical match order. Use each returned name; do not assume the model order matches the incoming player-array order.
The public decimal_price is generated by the Statsmatics pricing engine. The source ltp is retained privately for audit and is not returned to the client.
source_odds_available is true only when both selections have valid direct source odds for that exact market, period, and line. It does not indicate whether the overall simulation used Match Moneyline calibration.
| State | prediction_id | data | error |
|---|---|---|---|
QUEUED | null | null | null |
PROCESSING | null | null | null |
COMPLETED | string | object | null |
FAILED | null | null | object |
REJECTED | null | null | object |
Error responses
Submission errors use the relevant HTTP status. Jobs that fail after acceptance return status: FAILED with a structured public error object.
| Code | When it occurs | Retry guidance |
|---|---|---|
VALIDATION_ERROR | Request does not satisfy contract 2.1.0. | No |
RATE_LIMITED | Per-minute submit or read limit was reached. | Yes |
QUOTA_EXCEEDED | Daily or queued-job quota was reached. | After reset |
CLOSING_NOT_REACHED | The first in-play marker is missing. | Yes |
MATCH_UNMAPPED | No canonical match passed mapping. | No |
MATCH_AMBIGUOUS | More than one candidate remained. | No |
MODEL_INPUT_INCOMPLETE | Required canonical match or current player-strength input is unavailable. | No |
PREDICTION_TIMEOUT | The pricing engine timed out. | Yes |
PREDICTION_REJECTED | The pricing engine rejected the mapped request. | No |
PREDICTION_FAILED | The pricing engine could not complete the prediction. | Varies |
PREDICTION_INVALID_RESPONSE | The pricing result failed validation. | Yes |
INTERNAL_ERROR | An unexpected service failure occurred. | Yes |
{
"code": "VALIDATION_ERROR",
"message": "The request does not satisfy the defined contract.",
"retryable": false,
"fields": [
{ "path": "market_entries", "reason": "The first in-play marker is required." }
]
}