Provides one function for each read endpoint of the 'OddsRelay' web service < https://oddsrelay.io/docs>, which serves bookmaker and exchange odds for the UK and Ireland. Requests ask for compressed replies, send entity tags so that unchanged data is not transferred again, wait as the service asks when it is busy, and signal refusals as classed conditions that carry the service's error code and request id. Replies are returned as lists and data frames, with the token and rate limit headers attached as attributes.
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 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_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.