API reference · Contract 1.0.0
Pricing intelligence,
built for integration.
Submit Betfair tennis market snapshots, receive an asynchronous prediction job, and retrieve normalized pre-match closing prices and model probabilities.
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, and polling request. 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.
Rotation is immediate.A rotated or revoked key stops authenticating without a grace period.
POST /v1/betfair/prediction-jobs
Send the documented JSON body with your managed API key. Every accepted submission creates a new job.
/v1/betfair/prediction-jobs202 Acceptedcurl --request POST 'https://marketengine.statsmatics.com/v1/betfair/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.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": []
}
],
"requested_markets": [
{ "type": "MONEYLINE", "periods": ["MATCH", "SET_1", "SET_2"] },
{ "type": "HANDICAP", "periods": ["MATCH", "SET_1", "SET_2"] },
{ "type": "TOTALS", "periods": ["MATCH", "SET_1", "SET_2"] }
],
"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": "/v1/prediction-jobs/job_d6faa1ae",
"data": null,
"error": null,
"request_id": "req_93cce4c2",
"accepted_at": "2026-07-31T09:30:00Z"
}Request schema
The request body is strictly validated. All identifiers are strings, Betfair 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 | Betfair 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 | Must include the first in_play=true marker. |
market_entries[].pt | int64 | Yes | Betfair publish time as Unix epoch milliseconds. |
market_entries[].clk | string | Yes | Opaque Betfair stream cursor; never parse it as time. |
market_entries[].status | enum | Yes | OPEN, SUSPENDED, or CLOSED. |
market_entries[].selections | array | Yes | May be empty on the first in-play marker. |
selections[].ltp | number | null | No | Carried-forward Betfair rc[].ltp above 1.0, or null. |
requested_markets | array | null | No | Defaults to all entitled markets and periods. |
metadata | object | null | No | Supporting mapping context; not a required key. |
Normalize Betfair deltas before submission
MarketChangeMessages are incremental. Apply each mc update to your cached market state, then emit a normalized snapshot for every changed market. A definition-only history contains no source odds until a runner change supplies ltp.
| Betfair 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 Betfair 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 | Betfair source price; null when absent or invalid. |
The earliest normalized entry with in_play=true establishes match start using its pt. The scheduled market_time never replaces that observation.
Market selection
Specify the market families and periods required by your integration, or send null to use every output enabled for your client.
MONEYLINE
Win pricing for each player
No line · PLAYER_1 / PLAYER_2Supported periodsMATCH · SET_1 · SET_2HANDICAP
Spread pricing around a modelled line
Numeric line · PLAYER_1 / PLAYER_2Supported periodsMATCH · SET_1 · SET_2TOTALS
Over/under pricing around a modelled line
Numeric line · OVER / UNDERSupported periodsMATCH · SET_1 · SET_2Explicit selection
Request only the outputs your client needs.
"requested_markets": [
{
"type": "MONEYLINE",
"periods": ["MATCH", "SET_1", "SET_2"]
},
{
"type": "HANDICAP",
"periods": ["MATCH", "SET_1", "SET_2"]
},
{
"type": "TOTALS",
"periods": ["MATCH", "SET_1", "SET_2"]
}
]Enabled defaults
Use every market and period enabled for your client.
"requested_markets": nullGET /v1/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/v1/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",
"line": null,
"source_odds_available": true,
"selections": [
{
"selection": "PLAYER_1",
"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",
"name": "Miguel Damas",
"source_price_timestamp_utc": "2026-03-03T20:08:27.586Z",
"model_probability": 0.316,
"decimal_price": 3.165
}
]
}]
},
"error": null,
"request_id": "req_93cce4c2"
}The public decimal_price is generated by NumLab. The Betfair ltp is retained privately for audit and is not returned to the client.
| 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 1.0.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 |
NUMLAB_TIMEOUT | The private prediction worker timed out. | Yes |
NUMLAB_ERROR | The prediction worker returned an error. | Varies |
NUMLAB_INVALID_RESPONSE | The worker response 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." }
]
}