SM
Statsmatics Market EngineDeveloper documentation
Contract2.1.0

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.com
TransportHTTPS + JSONAuthenticationManaged API keyExecutionAsynchronous queueScopeTennis singles

Quick start

The client-facing flow stays small. The service owns match mapping, queueing, retries, validation, and response normalization.

01

Get credentials

Your API key is issued once from the private operations console.

02

Submit a snapshot

Send the documented JSON closing-market history.

03

Poll the job

Read the status URL until the job is COMPLETED or FAILED.

No recommendation output

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_key

Keys 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.

GET/v2/inplay/matches?status=live200 OK
Query (exactly one)Returns
status=liveMatches currently live.
status=upcomingUpcoming matches.
date=YYYY-MM-DDUpcoming, 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"
    }
  ]
}
FieldClient use
match_idUse this exact string in the WebSocket subscribe frame.
players.p1 / players.p2Player names to display.
status / drawChoose a live or upcoming singles match.
format / genderSelect the correct client workflow.
surface / tournament / scheduled_timeMatch context for display.
Match ID

Use the match_id returned by discovery exactly as given. Do not invent or transform it. Dates are UTC calendar dates.

Errors

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.

WSSwss://marketengine.statsmatics.com/v2/inplay/ws101 Switching Protocols
01

Authenticate

Send the API key in the first JSON frame. Nothing else is accepted before it.

02

Subscribe

Send one subscribe frame per match_id from discovery. One connection can hold several matches.

03

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 }
      ]
    }
  ]
}
Set markets

Future-set Moneylines are conditional on that set being played. Completed or impossible sets are not priced.

Messages you receive

typestatusMeaning
ackauthenticated · subscribed · pending · unsubscribedReply to your authenticate, subscribe, or unsubscribe frame.
snapshotliveFirst complete markets array for a match, including player names.
pricesliveComplete markets array, sent whenever the priced state changes.
statuspendingA current price is being prepared. Wait for the next snapshot or prices message.
statuslivePrices are current again and unchanged since the last prices message.
statusunavailableThe match cannot be priced for you now. Refresh discovery and choose an available match.
completedcompletedThe match has finished. Stop consuming it and keep the final update.

Connection recovery

ConditionClient action
Invalid or revoked keyThe socket closes. Obtain a valid API key and authenticate again.
Match unavailableRefresh the match list and choose an available match.
Service temporarily unavailableWait and continue when the service sends the next current price.
Match completedStop consuming prices for that match and retain the final update.
Socket closesReconnect, 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.

Open the complete in-play PDF guide.

POST /v2/prediction-jobs

Send the documented JSON body with your managed API key. Every accepted submission creates a new job.

POST/v2/prediction-jobs202 Accepted
curl --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.

POST/v2/future-prediction-jobs202 Accepted
Player fields

Each 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 ZWE

Tournament 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.

Canonical tournament mapping is preferred, not required.

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.

Open the complete future pre-game PDF guide.

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 pathTypeRequiredRule
event.event_idstringYesSource event identifier.
event.event_namestringYesNon-empty event label.
playersarray[2]YesExactly one PLAYER_1 and one PLAYER_2.
players[].namestringYesUsed with the UTC match date for mapping.
market_entriesarrayYesDeclares the targets to price and must include the first in_play=true marker.
market_entries[].ptint64YesSource publish time as Unix epoch milliseconds.
market_entries[].clkstringYesOpaque source stream cursor; never parse it as time.
market_entries[].statusenumYesOPEN, SUSPENDED, or CLOSED.
market_entries[].selectionsarrayYesComplete two-sided target pairs; may be empty only on the in-play marker.
selections[].linenumber | nullYesExact handicap/total line for that side; null for moneyline.
selections[].ltpnumber | nullNoOptional source price. A null value still requests model pricing for that line.
metadataobject | nullNoOptional 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.

FieldType
event_typestring | null
tournament_namestring | null
roundstring | null
surfacestring | null
indoorboolean | null
genderstring | 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 pathRequest pathRule
message.clkmarket_entries[].clkOpaque cursor; preserve as a string.
message.ptmarket_entries[].ptPublish time in Unix epoch milliseconds.
mc[].idmarket_entries[].market_idStringify the source market identifier.
marketDefinitionmarket_type, market_name, market_time, timezone, in_play, statusCarry the latest definition forward across deltas.
marketDefinition.eventId / eventNameevent.event_id / event.event_nameThe normalized request contains one event.
marketDefinition.runners[] + rc[]selections[]Join by runner id and handicap; carry the latest valid LTP forward.
rc[].ltpselections[].ltpSource price; null when absent or invalid.
Start and closing-price boundary

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_typeName and selection requirementInferred output
MATCH_ODDSMatch market; two player selections with null lines.MONEYLINE / MATCH
SET_WINNERSet number 1-5 in market_name; two player selections with null lines.MONEYLINE / SET_n
HANDICAP or MATCH_HANDICAPMatch market; two player selections with opposite half-point lines.HANDICAP / MATCH
SET_HANDICAPSet number 1-5 in market_name; two player selections with opposite half-point lines.HANDICAP / SET_n
COMBINED_TOTAL or MATCH_TOTALSMatch market; OVER and UNDER at the same positive half-point line.TOTAL / MATCH
SET_TOTALSSet number 1-5 in market_name; OVER and UNDER at the same positive half-point line.TOTAL / SET_n
MO

MONEYLINE

Win pricing for each player

SelectionsNull line · PLAYER_1 / PLAYER_2Supported periodsMATCH · SET_1 · SET_2 · SET_3 · SET_4 · SET_5
HA

HANDICAP

Spread pricing at the supplied player lines

SelectionsPer-player line · PLAYER_1 / PLAYER_2Supported periodsMATCH · SET_1 · SET_2 · SET_3 · SET_4 · SET_5
TO

TOTAL

Over/under pricing at the supplied total

SelectionsNumeric line · OVER / UNDERSupported periodsMATCH · SET_1 · SET_2 · SET_3 · SET_4 · SET_5

Exact 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 engine

GET /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'
1QUEUED2PROCESSING3COMPLETEDor!FAILED

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"
}
Selection-level lines

Each returned selection carries its own line. This keeps PLAYER_1 and PLAYER_2 handicap lines explicit and prevents unrequested distribution lines from appearing.

Canonical player order

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.

Model price, not source LTP

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.

Direct source-odds indicator

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.

Stateprediction_iddataerror
QUEUEDnullnullnull
PROCESSINGnullnullnull
COMPLETEDstringobjectnull
FAILEDnullnullobject
REJECTEDnullnullobject

Error responses

Submission errors use the relevant HTTP status. Jobs that fail after acceptance return status: FAILED with a structured public error object.

CodeWhen it occursRetry guidance
VALIDATION_ERRORRequest does not satisfy contract 2.1.0.No
RATE_LIMITEDPer-minute submit or read limit was reached.Yes
QUOTA_EXCEEDEDDaily or queued-job quota was reached.After reset
CLOSING_NOT_REACHEDThe first in-play marker is missing.Yes
MATCH_UNMAPPEDNo canonical match passed mapping.No
MATCH_AMBIGUOUSMore than one candidate remained.No
MODEL_INPUT_INCOMPLETERequired canonical match or current player-strength input is unavailable.No
PREDICTION_TIMEOUTThe pricing engine timed out.Yes
PREDICTION_REJECTEDThe pricing engine rejected the mapped request.No
PREDICTION_FAILEDThe pricing engine could not complete the prediction.Varies
PREDICTION_INVALID_RESPONSEThe pricing result failed validation.Yes
INTERNAL_ERRORAn 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." }
  ]
}