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 and copy
your key from the dashboard. Put it 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 leftNo 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$remainingFor 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_modifiedA 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.
MIT. See LICENSE.md.