Data API

API reference

Try each endpoint with your key on this page

Base URLhttps://flintstat.com/api/v1v1.0.1
Keys

Send 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

Quota and limits

Test has 100 requests in total and 10 per minute. A request uses quota only when it answers 200. Errors and 304 are free

Units

Percentage fields end in _pct and hold percent values. A cell with fewer than 20 bets shows its count and no metrics

Caching

Every GET answer has an ETag that changes with the data. Send it back in If-None-Match and get 304 for free

License

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

List sports

Each sport with its bets, events, seasons and first and last season

Response

FieldTypeDescription
dataarray of object
data[].sportstring
data[].betsintegerBets in the data. One bet is one side of one event
data[].matchesintegerEvents
data[].seasonsinteger
data[].first_seasonstring or null
data[].last_seasonstring or null
data[].coverage_pctinteger or nullShare 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
sourcesarray of objectSources of the sports in this answer. Their licences require credit where the data is shown
sources[].sportstring
sources[].titlestringDataset name
sources[].authorstring
sources[].urlstring
sources[].licencestring
sources[].licence_urlstring or null
sources[].originstring or nullWhere the dataset itself took the data from
sources[].noticestring or nullA statement the licence requires. Show it as is next to the data
sources[].notestring or nullWhat Flintstat did with the data

Samples

curl https://flintstat.com/api/v1/sports \
  -H "Authorization: Bearer $FLINTSTAT_KEY"
Try it

GET/sports/{sport}/filters

Filters of a sport

Every filter key of the sport with its values, and the range of the best price

Parameters

FieldTypeDescription
sportrequiredstring, pathSport key from GET /sports

Response

FieldTypeDescription
dataobject
data.sportstring
data.filtersobjectFilter key to its label and values
data.filters.{key}object
data.filters.{key}.labelstring
data.filters.{key}.valuesarray of string
data.oddsobjectRange of the best price in decimal odds
data.odds.minnumber or null
data.odds.maxnumber or null
sourcesarray of objectSources of the sports in this answer. Their licences require credit where the data is shown
sources[].sportstring
sources[].titlestringDataset name
sources[].authorstring
sources[].urlstring
sources[].licencestring
sources[].licence_urlstring or null
sources[].originstring or nullWhere the dataset itself took the data from
sources[].noticestring or nullA statement the licence requires. Show it as is next to the data
sources[].notestring or nullWhat Flintstat did with the data

Samples

curl https://flintstat.com/api/v1/sports/basketball/filters \
  -H "Authorization: Bearer $FLINTSTAT_KEY"
Try it

POST/facets

Values left after filtering

For each filter key, the values still available given the other filters. A key is never narrowed by its own selection

Request body

FieldTypeDescription
sportrequiredstringSport key from GET /sports, such as basketball
filtersobjectFilter 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_oddsnumber or nullLowest best price in decimal odds, included
max_oddsnumber or nullHighest best price in decimal odds, not included

Response

FieldTypeDescription
dataobject
data.sportstring
data.filtersobjectFor each filter key, the values still available with the other filters applied
data.filters.{key}object
data.filters.{key}.labelstring
data.filters.{key}.valuesarray of string
sourcesarray of objectSources of the sports in this answer. Their licences require credit where the data is shown
sources[].sportstring
sources[].titlestringDataset name
sources[].authorstring
sources[].urlstring
sources[].licencestring
sources[].licence_urlstring or null
sources[].originstring or nullWhere the dataset itself took the data from
sources[].noticestring or nullA statement the licence requires. Show it as is next to the data
sources[].notestring or nullWhat 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

Metrics of a slice

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

FieldTypeDescription
sportrequiredstringSport key from GET /sports, such as basketball
filtersobjectFilter 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_oddsnumber or nullLowest best price in decimal odds, included
max_oddsnumber or nullHighest best price in decimal odds, not included

Response

FieldTypeDescription
dataobject
data.betsintegerBets in the slice
data.suppressedbooleantrue when the slice has fewer than 20 bets. Every metric is then null
data.win_rate_pctnumber or nullShare of bets that won. In percent
data.roi_pctnumber or nullReturn per bet at the best price across bookmakers. In percent
data.roi_se_pctnumber or nullStandard error of roi_pct. In percent
data.roi_avg_pctnumber or nullReturn per bet at the average price across bookmakers. In percent
data.roi_avg_se_pctnumber or nullStandard error of roi_avg_pct. In percent
data.books_avgnumber or nullBookmakers per bet on average. 1 means the best and the average price are the same
data.implied_prob_pctnumber or nullAverage implied probability of the best price. In percent
data.avg_oddsnumber or nullAverage best price in decimal odds
data.seasons_totalintegerSeasons with at least 20 bets
data.seasons_positiveintegerSeasons with a positive ROI at the best price
data.roi_std_pctnumber or nullSpread of the season ROI. In percent
data.roi_worst_pctnumber or nullWorst season ROI. In percent
data.roi_by_seasonarray of objectSeasons with at least 20 bets
data.roi_by_season[].seasonstring
data.roi_by_season[].betsinteger
data.roi_by_season[].roi_pctnumber or nullROI at the best price. In percent
data.roi_by_season[].roi_avg_pctnumber or nullROI at the average price. In percent
data.calibrationarray of objectImplied probability against the actual win rate by odds range
data.calibration[].bucketstringOdds range of the best price, such as 1.5-2.0
data.calibration[].betsinteger
data.calibration[].suppressedbooleantrue when the bucket has fewer than 20 bets. Its metrics are then null
data.calibration[].implied_pctnumber or nullAverage implied probability. In percent
data.calibration[].actual_pctnumber or nullShare of bets that won. In percent
data.calibration[].roi_pctnumber or nullROI at the best price. In percent
data.calibration[].roi_avg_pctnumber or nullROI at the average price. In percent
sourcesarray of objectSources of the sports in this answer. Their licences require credit where the data is shown
sources[].sportstring
sources[].titlestringDataset name
sources[].authorstring
sources[].urlstring
sources[].licencestring
sources[].licence_urlstring or null
sources[].originstring or nullWhere the dataset itself took the data from
sources[].noticestring or nullA statement the licence requires. Show it as is next to the data
sources[].notestring or nullWhat 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

Leagues

Each league with its events, seasons, favourite win rate and ROI and the bookmaker margin

Parameters

FieldTypeDescription
sportstring, queryOnly the leagues of this sport

Response

FieldTypeDescription
dataarray of object
data[].sportstring
data[].leaguestring
data[].matchesinteger
data[].betsinteger
data[].first_seasonstring or null
data[].last_seasonstring or null
data[].fav_win_rate_pctnumber or nullShare of favourites that won. In percent
data[].fav_roi_pctnumber or nullROI of backing every favourite at the best price. In percent
data[].fav_roi_avg_pctnumber or nullROI of backing every favourite at the average price. In percent
data[].overround_pctnumber or nullBookmaker margin of the two sides together. In percent
sourcesarray of objectSources of the sports in this answer. Their licences require credit where the data is shown
sources[].sportstring
sources[].titlestringDataset name
sources[].authorstring
sources[].urlstring
sources[].licencestring
sources[].licence_urlstring or null
sources[].originstring or nullWhere the dataset itself took the data from
sources[].noticestring or nullA statement the licence requires. Show it as is next to the data
sources[].notestring or nullWhat 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 and players

Teams, players or fighters of a sport with at least 20 bets, page by page

Parameters

FieldTypeDescription
sportrequiredstring, pathSport key from GET /sports
limitinteger, queryRows per page. Default 50
offsetinteger, queryRows to skip. Default 0
sortstring, queryColumn to sort by. One of bets, roi_pct, roi_avg_pct, roi_shrunk_pct, win_rate_pct, avg_odds. Default bets
orderstring, querySort direction. One of desc, asc. Default desc

Response

FieldTypeDescription
dataarray of object
data[].namestring
data[].betsinteger
data[].winsinteger
data[].win_rate_pctnumber or nullShare of bets that won. In percent
data[].roi_pctnumber or nullROI at the best price. In percent
data[].roi_avg_pctnumber or nullROI at the average price. Null where the data has no average price. In percent
data[].avg_oddsnumber or null
data[].first_seasonstring or null
data[].last_seasonstring or null
data[].roi_shrunk_pctnumber or nullROI pulled toward the sport average by sample size, so a short record does not look extreme. In percent
paginationobject
pagination.limitinteger
pagination.offsetinteger
pagination.totalinteger
sourcesarray of objectSources of the sports in this answer. Their licences require credit where the data is shown
sources[].sportstring
sources[].titlestringDataset name
sources[].authorstring
sources[].urlstring
sources[].licencestring
sources[].licence_urlstring or null
sources[].originstring or nullWhere the dataset itself took the data from
sources[].noticestring or nullA statement the licence requires. Show it as is next to the data
sources[].notestring or nullWhat Flintstat did with the data

Samples

curl https://flintstat.com/api/v1/sports/basketball/entities \
  -H "Authorization: Bearer $FLINTSTAT_KEY"
Try it

GET/health

HealthNo key needed

Whether the data engine answers. Uses no quota

Response

FieldTypeDescription
statusstringnot_open until the Data API opens; keyed requests then answer 503 NOT_OPEN. One of ok, unavailable, not_open
openbooleanWhether keyed requests are served
data_versionstring or nullChanges whenever the data changes. The ETag of every GET carries it
built_atstring or nullDay the data was built
schema_versionstring or null
workersobject
workers.sizeinteger
workers.readyinteger
workers.busyinteger
workers.queuedinteger

Samples

curl https://flintstat.com/api/v1/health
Try it

GET/openapi.json

This documentNo key needed

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."
  }
}
FieldTypeDescription
errorobject
error.codestringOne 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.messagestring
error.plans_urlstringWhere to get more requests. Only on QUOTA_EXCEEDED
CodeHTTPDescription
INVALID_REQUEST400The body or a parameter does not match the schema, or the JSON does not parse
UNKNOWN_SPORT400The sport is not served. GET /sports lists the sports
UNKNOWN_FILTER400The filter key does not exist for this sport
UNKNOWN_VALUE400A filter value does not exist for this sport
INVALID_RANGE400min_odds is not lower than max_odds
MISSING_KEY401No API key in the request
INVALID_KEY401The key is wrong
KEY_REVOKED401The key was revoked
KEY_EXPIRED401The key was rotated and its 24 hours are over
NOT_FOUND404No such endpoint
METHOD_NOT_ALLOWED405The endpoint takes another method
BODY_TOO_LARGE413The body is over 100 kB
RATE_LIMITED429Too many requests this minute. Retry-After says when to try again
QUOTA_EXCEEDED429The quota of the plan is used up. plans_url says where to get more
TOO_MANY_CONCURRENT429Too many requests of this account at the same time
TOO_MANY_FAILED_KEYS429Too many wrong keys from this address. Retry-After says when to try again
SERVER_ERROR500Our fault. The request did not use quota
UNAVAILABLE503The data engine is starting or restarting. The request did not use quota
OVERLOADED503Too many queries are waiting. The request did not use quota
QUERY_TIMEOUT503The query took too long. The request did not use quota
NOT_OPEN503The Data API is not open yet. The waitlist is on the Data API page

Headers

Every keyed answer carries these

HeaderDescription
RateLimit-LimitRequests allowed per minute
RateLimit-RemainingRequests left this minute
RateLimit-ResetSeconds until the minute starts over
X-Quota-LimitRequests in the plan quota
X-Quota-RemainingRequests left in the quota
X-Quota-ResetWhen a monthly quota starts over, midnight UTC on the 1st of the next month. Absent on the test plan
ETagVersion 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

NBA Betting Databy cviaxmiwnptr on Kaggle
License CC0 1.0
Original data from SportsbookReviewsOnline (sportsbookreviewsonline.com), the original source of the odds
Modified, team names unified and aggregates computed

MLB

MLB Odds Databy Christopher Treasure on Kaggle
License Other, as specified in the dataset description
Odds joined with the Retrosheet game logs, aggregates computed
Game results
The information used here was obtained free of charge from and is copyrighted by Retrosheet. Interested parties may contact Retrosheet at "www.retrosheet.org".

UFC

Ultimate UFC Datasetby mdabbert on Kaggle
License CC BY 4.0
Original data from bestfightodds.com for the odds, ufcstats.com for the results
Modified, fighter names unified and aggregates computed

NFL

MGM Grand NFL betting databy Casey Durfee (caseydurfee) on Kaggle
License CC BY-SA 4.0
Original data from Yahoo Sports pages, odds of BetMGM
Modified, served as aggregates only