The OddsRelay R client. One function for each GET route of the OddsRelay API, which serves bookmaker and exchange odds for the UK & Ireland as JSON. Replies come back as lists and data frames.
install.packages("oddsrelay")
# or the development version
# install.packages("pak")
pak::pak("oddsrelay/oddsrelay-r")
Sign up at app.oddsrelay.io/signup and copy your key from the dashboard. Free gives you 50,000 tokens for 14 days from sign-up, with no card, then no tokens until you choose a plan, on the same key. Plans and token prices. Paid plans are for your own use. A product that serves other people runs on Enterprise.
Put your key in your .Renviron (open it with file.edit("~/.Renviron")), never in a script:
ODDSRELAY_KEY=your key
Restart R, then:
library(oddsrelay)
board <- or_odds("standard", region = "uk", sports = "soccer", markets = "h2h") # soccer is the API's key for football
board$data[, c("commence_time", "home_team", "away_team")]
attr(board, "tokens") # what the call cost and what is left
No key yet? These answer without one:
or_status()
names(or_coverage(region = "uk")$products)
| Function | Route | What it returns |
|---|---|---|
or_odds(type, ...) |
GET /v2/odds/{type} |
Matched odds for one board: standard, 2up, dutching, each-way, extra-place or bog
|
or_raw(...) |
GET /v2/odds/raw |
Bookmaker and exchange prices by event, unpaired, a page at a time |
or_event(id, ...) |
GET /v2/odds/event/{id} |
One event's raw prices |
or_sports(...) |
GET /v2/sports |
Sport and competition keys |
or_events(sport, ...) |
GET /v2/events |
Upcoming events for one sport key |
or_bookmakers(...) |
GET /v2/bookmakers |
The bookmakers and exchanges you can filter on, with their keys |
or_regions() |
GET /v2/regions |
The region codes open to your key |
or_coverage(...) |
GET /v2/coverage |
Which bookmakers and sports each product carries (keyless; more detail with a key) |
or_status(...) |
GET /v2/status |
Whether each product is serving (keyless) |
or_health() |
GET /v2/health |
When each board was last processed, and whether any is behind |
or_usage() |
GET /v2/usage |
Tokens used and left, and what your key may read |
or_pricing() |
GET /v2/pricing |
The token rate card |
or_openapi() |
GET /v2/openapi.json |
The API description (keyless) |
or_healthz() |
GET /healthz |
A plain-text liveness check (keyless) |
Each returns the decoded body with every field kept, including fields the API adds after this release. Arrays of
records become data frames, so board$data above is one. Pass format = "bare" where a route takes it to get the data
frame alone.
Every query parameter in the API reference is a named argument. The API's
camelCase and R's snake_case both work, so odds_format = "decimal" and oddsFormat = "decimal" send the same thing.
A vector becomes one comma-separated value, TRUE and FALSE become true and false, a date-time becomes a UTC
timestamp, and NULL is left out:
now <- Sys.time()
or_odds(
"each-way",
region = "uk",
sports = "horse_racing",
commence_time_from = now,
commence_time_to = now + 6 * 3600
)
The API checks every filter itself. An unknown name or value signals oddsrelay_bad_request or a more specific class
such as oddsrelay_unknown_sport, and where the API lists the accepted values, e$error$valid holds them.
quote = TRUE asks what a call would cost instead of making it. A quote spends no tokens:
quote <- or_odds("standard", region = "uk", quote = TRUE)
quote$cost
quote$remaining
For each URL it has fetched in this session, the package keeps the reply's ETag and sends it back as
If-None-Match. When nothing has changed the API answers 304, which spends no tokens, and you get back the body you
already had with attr(x, "not_modified") set to TRUE:
repeat {
board <- or_odds("standard", region = "uk", sports = "soccer")
if (!attr(board, "not_modified")) update_my_screen(board$data)
Sys.sleep(poll_seconds)
}
or_raw(), or_sports() and or_events() return a page at a time. Pass the next page's cursor back as cursor:
cursor <- NULL
repeat {
page <- or_raw(region = "uk", sports = "tennis", cursor = cursor)
handle(page$data)
cursor <- page$meta$next_cursor
if (is.null(cursor)) break
}
A refusal signals a condition of class oddsrelay_error. In front of that sit oddsrelay_<code>, from the error
code in the reply, and oddsrelay_http_<status>. Each condition carries status, code, request_id, error (the
whole error object), tokens and rate_limit.
| Status | Example classes |
|---|---|
| 400 |
oddsrelay_bad_request, oddsrelay_unknown_sport, oddsrelay_http_400
|
| 401 |
oddsrelay_missing_api_key, oddsrelay_invalid_api_key, oddsrelay_http_401
|
| 402 |
oddsrelay_insufficient_tokens, with cost, remaining and resets_at
|
| 403 |
oddsrelay_sport_not_in_plan, oddsrelay_http_403
|
| 404 |
oddsrelay_unknown_event, oddsrelay_http_404
|
| 429 |
oddsrelay_too_many_requests, with retry_after, once the package's own retries are used up |
| 5xx |
oddsrelay_http_500, oddsrelay_http_502, oddsrelay_http_503
|
Branch on the class or e$code, never on the message, which may change. Quote request_id when you contact us.
tryCatch(
or_odds("dutching", region = "uk"),
oddsrelay_insufficient_tokens = function(e) {
message("needs ", e$cost, " tokens, ", e$remaining, " left until ", e$resets_at)
}
)
A 429 is retried for you: the package says how long it will wait, waits the Retry-After seconds and tries again,
three requests in all.
Network failures are left as 'httr2' signals them, so a long-running loop also handles httr2_failure.
Every result carries the reply's headers as attributes:
tokens: cost, used, remaining, limit and reset, from the X-Tokens-* headersrate_limit: limit, remaining and reset, from the X-RateLimit-* headersetag, request_id and not_modified
A header the reply did not carry reads as NA. Printing a result shows the body and one line of tokens, not the
attributes; unclass() gives the plain list.
The key travels only in the Authorization header. The package never puts it in a URL, and it is redacted when a
request is printed and removed from any condition message. A filter named like a key (key, api_key, token or
any name starting with a dot) is refused before anything is sent, so a misspelt .key never reaches a URL. Spaces and newlines around the key, common when it is
read from a file, are dropped.
The Config/oddsrelay/contract-version field of DESCRIPTION names the version of the API description these
functions follow:
utils::packageDescription("oddsrelay")[["Config/oddsrelay/contract-version"]]
The API only adds to /v2, so new fields and new error codes reach you without an upgrade.
pip install oddsrelay
npm install oddsrelay
MIT. See LICENSE.md.