API reference
Try each endpoint with your key on this page
https://flintstat.com/api/v1v1.0.1Send your key in the Authorization header as Bearer, or in X-API-Key. Health and the OpenAPI file need no key. The API is for server-side use. Keep the key on your server, never in a web page or an app you ship. Browsers on other sites cannot call it
Test has 100 requests in total and 10 per minute. A request uses quota only when it answers 200. Errors and 304 are free
Percentage fields end in _pct and hold percent values. A cell with fewer than 20 bets shows its count and no metrics
Every GET answer has an ETag that changes with the data. Send it back in If-None-Match and get 304 for free
Personal, non-commercial use with credit to Data by Flintstat and the sources. No resale of the data as a dataset or feed
Kept in this tab only and sent only to this site
GET/sports
Each sport with its bets, events, seasons and first and last season
Response
| Field | Type | Description |
|---|---|---|
data | array of object | |
data[].sport | string | |
data[].bets | integer | Bets in the data. One bet is one side of one event |
data[].matches | integer | Events |
data[].seasons | integer | |
data[].first_season | string or null | |
data[].last_season | string or null | |
data[].coverage_pct | integer or null | Share of the played matches the data holds, in percent, rounded to a whole number and never 100 while a match is missing. Measured against an independent schedule of each sport; null when not measured |
sources | array of object | Sources of the sports in this answer. Their licences require credit where the data is shown |
sources[].sport | string | |
sources[].title | string | Dataset name |
sources[].author | string | |
sources[].url | string | |
sources[].licence | string | |
sources[].licence_url | string or null | |
sources[].origin | string or null | Where the dataset itself took the data from |
sources[].notice | string or null | A statement the licence requires. Show it as is next to the data |
sources[].note | string or null | What Flintstat did with the data |
Samples
curl https://flintstat.com/api/v1/sports \ -H "Authorization: Bearer $FLINTSTAT_KEY"
Try it
GET/sports/{sport}/filters
Every filter key of the sport with its values, and the range of the best price
Parameters
| Field | Type | Description |
|---|---|---|
sportrequired | string, path | Sport key from GET /sports |
Response
| Field | Type | Description |
|---|---|---|
data | object | |
data.sport | string | |
data.filters | object | Filter key to its label and values |
data.filters.{key} | object | |
data.filters.{key}.label | string | |
data.filters.{key}.values | array of string | |
data.odds | object | Range of the best price in decimal odds |
data.odds.min | number or null | |
data.odds.max | number or null | |
sources | array of object | Sources of the sports in this answer. Their licences require credit where the data is shown |
sources[].sport | string | |
sources[].title | string | Dataset name |
sources[].author | string | |
sources[].url | string | |
sources[].licence | string | |
sources[].licence_url | string or null | |
sources[].origin | string or null | Where the dataset itself took the data from |
sources[].notice | string or null | A statement the licence requires. Show it as is next to the data |
sources[].note | string or null | What Flintstat did with the data |
Samples
curl https://flintstat.com/api/v1/sports/basketball/filters \ -H "Authorization: Bearer $FLINTSTAT_KEY"
Try it
POST/facets
For each filter key, the values still available given the other filters. A key is never narrowed by its own selection
Request body
| Field | Type | Description |
|---|---|---|
sportrequired | string | Sport key from GET /sports, such as basketball |
filters | object | Filter key to the values to keep. Keys and values come from GET /sports/{sport}/filters. Values of one key are alternatives, different keys all apply |
filters.{key} | array of string | |
min_odds | number or null | Lowest best price in decimal odds, included |
max_odds | number or null | Highest best price in decimal odds, not included |
Response
| Field | Type | Description |
|---|---|---|
data | object | |
data.sport | string | |
data.filters | object | For each filter key, the values still available with the other filters applied |
data.filters.{key} | object | |
data.filters.{key}.label | string | |
data.filters.{key}.values | array of string | |
sources | array of object | Sources of the sports in this answer. Their licences require credit where the data is shown |
sources[].sport | string | |
sources[].title | string | Dataset name |
sources[].author | string | |
sources[].url | string | |
sources[].licence | string | |
sources[].licence_url | string or null | |
sources[].origin | string or null | Where the dataset itself took the data from |
sources[].notice | string or null | A statement the licence requires. Show it as is next to the data |
sources[].note | string or null | What Flintstat did with the data |
Samples
curl -X POST https://flintstat.com/api/v1/facets \
-H "Authorization: Bearer $FLINTSTAT_KEY" \
-H "Content-Type: application/json" \
-d '{
"sport": "basketball",
"filters": {
"is_favorite": ["false"]
},
"min_odds": 2,
"max_odds": 5
}'Try it
POST/query
Backs every bet in the slice at the best and at the average price. Count, win rate, ROI with its standard error, calibration by odds range and ROI by season
Request body
| Field | Type | Description |
|---|---|---|
sportrequired | string | Sport key from GET /sports, such as basketball |
filters | object | Filter key to the values to keep. Keys and values come from GET /sports/{sport}/filters. Values of one key are alternatives, different keys all apply |
filters.{key} | array of string | |
min_odds | number or null | Lowest best price in decimal odds, included |
max_odds | number or null | Highest best price in decimal odds, not included |
Response
| Field | Type | Description |
|---|---|---|
data | object | |
data.bets | integer | Bets in the slice |
data.suppressed | boolean | true when the slice has fewer than 20 bets. Every metric is then null |
data.win_rate_pct | number or null | Share of bets that won. In percent |
data.roi_pct | number or null | Return per bet at the best price across bookmakers. In percent |
data.roi_se_pct | number or null | Standard error of roi_pct. In percent |
data.roi_avg_pct | number or null | Return per bet at the average price across bookmakers. In percent |
data.roi_avg_se_pct | number or null | Standard error of roi_avg_pct. In percent |
data.books_avg | number or null | Bookmakers per bet on average. 1 means the best and the average price are the same |
data.implied_prob_pct | number or null | Average implied probability of the best price. In percent |
data.avg_odds | number or null | Average best price in decimal odds |
data.seasons_total | integer | Seasons with at least 20 bets |
data.seasons_positive | integer | Seasons with a positive ROI at the best price |
data.roi_std_pct | number or null | Spread of the season ROI. In percent |
data.roi_worst_pct | number or null | Worst season ROI. In percent |
data.roi_by_season | array of object | Seasons with at least 20 bets |
data.roi_by_season[].season | string | |
data.roi_by_season[].bets | integer | |
data.roi_by_season[].roi_pct | number or null | ROI at the best price. In percent |
data.roi_by_season[].roi_avg_pct | number or null | ROI at the average price. In percent |
data.calibration | array of object | Implied probability against the actual win rate by odds range |
data.calibration[].bucket | string | Odds range of the best price, such as 1.5-2.0 |
data.calibration[].bets | integer | |
data.calibration[].suppressed | boolean | true when the bucket has fewer than 20 bets. Its metrics are then null |
data.calibration[].implied_pct | number or null | Average implied probability. In percent |
data.calibration[].actual_pct | number or null | Share of bets that won. In percent |
data.calibration[].roi_pct | number or null | ROI at the best price. In percent |
data.calibration[].roi_avg_pct | number or null | ROI at the average price. In percent |
sources | array of object | Sources of the sports in this answer. Their licences require credit where the data is shown |
sources[].sport | string | |
sources[].title | string | Dataset name |
sources[].author | string | |
sources[].url | string | |
sources[].licence | string | |
sources[].licence_url | string or null | |
sources[].origin | string or null | Where the dataset itself took the data from |
sources[].notice | string or null | A statement the licence requires. Show it as is next to the data |
sources[].note | string or null | What Flintstat did with the data |
Samples
curl -X POST https://flintstat.com/api/v1/query \
-H "Authorization: Bearer $FLINTSTAT_KEY" \
-H "Content-Type: application/json" \
-d '{
"sport": "basketball",
"filters": {
"is_favorite": ["false"]
},
"min_odds": 2,
"max_odds": 5
}'Try it
GET/leagues
Each league with its events, seasons, favourite win rate and ROI and the bookmaker margin
Parameters
| Field | Type | Description |
|---|---|---|
sport | string, query | Only the leagues of this sport |
Response
| Field | Type | Description |
|---|---|---|
data | array of object | |
data[].sport | string | |
data[].league | string | |
data[].matches | integer | |
data[].bets | integer | |
data[].first_season | string or null | |
data[].last_season | string or null | |
data[].fav_win_rate_pct | number or null | Share of favourites that won. In percent |
data[].fav_roi_pct | number or null | ROI of backing every favourite at the best price. In percent |
data[].fav_roi_avg_pct | number or null | ROI of backing every favourite at the average price. In percent |
data[].overround_pct | number or null | Bookmaker margin of the two sides together. In percent |
sources | array of object | Sources of the sports in this answer. Their licences require credit where the data is shown |
sources[].sport | string | |
sources[].title | string | Dataset name |
sources[].author | string | |
sources[].url | string | |
sources[].licence | string | |
sources[].licence_url | string or null | |
sources[].origin | string or null | Where the dataset itself took the data from |
sources[].notice | string or null | A statement the licence requires. Show it as is next to the data |
sources[].note | string or null | What Flintstat did with the data |
Samples
curl https://flintstat.com/api/v1/leagues \ -H "Authorization: Bearer $FLINTSTAT_KEY"
Try it
GET/sports/{sport}/entities
Teams, players or fighters of a sport with at least 20 bets, page by page
Parameters
| Field | Type | Description |
|---|---|---|
sportrequired | string, path | Sport key from GET /sports |
limit | integer, query | Rows per page. Default 50 |
offset | integer, query | Rows to skip. Default 0 |
sort | string, query | Column to sort by. One of bets, roi_pct, roi_avg_pct, roi_shrunk_pct, win_rate_pct, avg_odds. Default bets |
order | string, query | Sort direction. One of desc, asc. Default desc |
Response
| Field | Type | Description |
|---|---|---|
data | array of object | |
data[].name | string | |
data[].bets | integer | |
data[].wins | integer | |
data[].win_rate_pct | number or null | Share of bets that won. In percent |
data[].roi_pct | number or null | ROI at the best price. In percent |
data[].roi_avg_pct | number or null | ROI at the average price. Null where the data has no average price. In percent |
data[].avg_odds | number or null | |
data[].first_season | string or null | |
data[].last_season | string or null | |
data[].roi_shrunk_pct | number or null | ROI pulled toward the sport average by sample size, so a short record does not look extreme. In percent |
pagination | object | |
pagination.limit | integer | |
pagination.offset | integer | |
pagination.total | integer | |
sources | array of object | Sources of the sports in this answer. Their licences require credit where the data is shown |
sources[].sport | string | |
sources[].title | string | Dataset name |
sources[].author | string | |
sources[].url | string | |
sources[].licence | string | |
sources[].licence_url | string or null | |
sources[].origin | string or null | Where the dataset itself took the data from |
sources[].notice | string or null | A statement the licence requires. Show it as is next to the data |
sources[].note | string or null | What Flintstat did with the data |
Samples
curl https://flintstat.com/api/v1/sports/basketball/entities \ -H "Authorization: Bearer $FLINTSTAT_KEY"
Try it
GET/health
Whether the data engine answers. Uses no quota
Response
| Field | Type | Description |
|---|---|---|
status | string | not_open until the Data API opens; keyed requests then answer 503 NOT_OPEN. One of ok, unavailable, not_open |
open | boolean | Whether keyed requests are served |
data_version | string or null | Changes whenever the data changes. The ETag of every GET carries it |
built_at | string or null | Day the data was built |
schema_version | string or null | |
workers | object | |
workers.size | integer | |
workers.ready | integer | |
workers.busy | integer | |
workers.queued | integer |
Samples
curl https://flintstat.com/api/v1/health
Try it
GET/openapi.json
OpenAPI 3.1 for Swagger, Postman or a client generator
Samples
curl https://flintstat.com/api/v1/openapi.json
Try it
Errors
Every error has the same body with a code and a message
{
"error": {
"code": "UNKNOWN_FILTER",
"message": "Unknown filter \"tier\" for mma. Its filters are season, league, odds_bucket, is_favorite, side_label."
}
}| Field | Type | Description |
|---|---|---|
error | object | |
error.code | string | One of INVALID_REQUEST, UNKNOWN_SPORT, UNKNOWN_FILTER, UNKNOWN_VALUE, INVALID_RANGE, MISSING_KEY, INVALID_KEY, KEY_REVOKED, KEY_EXPIRED, NOT_FOUND, METHOD_NOT_ALLOWED, BODY_TOO_LARGE, RATE_LIMITED, QUOTA_EXCEEDED, TOO_MANY_CONCURRENT, TOO_MANY_FAILED_KEYS, SERVER_ERROR, UNAVAILABLE, OVERLOADED, QUERY_TIMEOUT, NOT_OPEN |
error.message | string | |
error.plans_url | string | Where to get more requests. Only on QUOTA_EXCEEDED |
| Code | HTTP | Description |
|---|---|---|
INVALID_REQUEST | 400 | The body or a parameter does not match the schema, or the JSON does not parse |
UNKNOWN_SPORT | 400 | The sport is not served. GET /sports lists the sports |
UNKNOWN_FILTER | 400 | The filter key does not exist for this sport |
UNKNOWN_VALUE | 400 | A filter value does not exist for this sport |
INVALID_RANGE | 400 | min_odds is not lower than max_odds |
MISSING_KEY | 401 | No API key in the request |
INVALID_KEY | 401 | The key is wrong |
KEY_REVOKED | 401 | The key was revoked |
KEY_EXPIRED | 401 | The key was rotated and its 24 hours are over |
NOT_FOUND | 404 | No such endpoint |
METHOD_NOT_ALLOWED | 405 | The endpoint takes another method |
BODY_TOO_LARGE | 413 | The body is over 100 kB |
RATE_LIMITED | 429 | Too many requests this minute. Retry-After says when to try again |
QUOTA_EXCEEDED | 429 | The quota of the plan is used up. plans_url says where to get more |
TOO_MANY_CONCURRENT | 429 | Too many requests of this account at the same time |
TOO_MANY_FAILED_KEYS | 429 | Too many wrong keys from this address. Retry-After says when to try again |
SERVER_ERROR | 500 | Our fault. The request did not use quota |
UNAVAILABLE | 503 | The data engine is starting or restarting. The request did not use quota |
OVERLOADED | 503 | Too many queries are waiting. The request did not use quota |
QUERY_TIMEOUT | 503 | The query took too long. The request did not use quota |
NOT_OPEN | 503 | The Data API is not open yet. The waitlist is on the Data API page |
Headers
Every keyed answer carries these
| Header | Description |
|---|---|
RateLimit-Limit | Requests allowed per minute |
RateLimit-Remaining | Requests left this minute |
RateLimit-Reset | Seconds until the minute starts over |
X-Quota-Limit | Requests in the plan quota |
X-Quota-Remaining | Requests left in the quota |
X-Quota-Reset | When a monthly quota starts over, midnight UTC on the 1st of the next month. Absent on the test plan |
ETag | Version of the data. Send it in If-None-Match |
The contract of v1 only grows. Fields are never renamed or removed
Sources
Every API answer names its sources. Their licenses ask you to credit them wherever you show the data
NBA
MLB
The information used here was obtained free of charge from and is copyrighted by Retrosheet. Interested parties may contact Retrosheet at "www.retrosheet.org".