Odds HTTP API reference
The ArbPlay Odds API has two read-only JSON endpoints at https://api.arbplay.app: GET /v1/files lists the files your key can read, and GET /v1/data/{filename} returns one file with its updated_at time. Authenticate with X-API-Key, or with your marketplace key through RapidAPI, Zyla or JoJ API.
Base URL and authentication
Direct keys call https://api.arbplay.app with the header X-API-Key: YOUR_API_KEY. A key normally belongs to one bookmaker (its company key) and reads only that bookmaker's files; a direct key can also cover several bookmakers or a chosen set of files, and then company carries that bundle's name.
Marketplace subscribers call the marketplace's gateway instead, with the marketplace's own key. The endpoints and responses are the same:
| Marketplace | Your key goes in | Base URL and full path |
|---|---|---|
| RapidAPI | X-RapidAPI-Key and X-RapidAPI-Host | Endpoints tab of the listing |
| Zyla API Hub | Authorization: Bearer <access key> | Code sample on the listing |
| JoJ API | X-JoJAPI-Key (or Authorization: Bearer) | Code sample on the listing |
Keep keys server-side: the API sends no CORS headers, so browser calls fail by design.
GET /v1/files
Lists the files your key can read, sorted by name. Only files that exist right now are listed.
{
"company": "stoiximan",
"count": 5,
"files": [
"site_live_stoiximan.json",
"site_pregame_stoiximan_basketball.json",
"site_pregame_stoiximan_soccer.json",
"site_pregame_stoiximan_soccer_leagues.json",
"site_pregame_stoiximan_tennis.json"
]
}
GET /v1/data/{filename}
Returns one file inside a four-key envelope, always in this order:
| Key | Type | Meaning |
|---|---|---|
company | string | Company key of your API key (e.g. opapstore); a bundle key shows its own name |
file | string | The filename you asked for |
updated_at | string (ISO 8601, UTC) | When the file was last written; also sent as Last-Modified |
data | object or array | The bookmaker's own JSON, byte-for-byte as scraped |
{
"company": "bwin",
"file": "site_pregame_bwin_soccer.json",
"updated_at": "2026-09-26T20:02:24Z",
"data": {
"fixtures": [
"… Bwin's own JSON, unchanged …"
]
}
}
The file is streamed from disk as-is — never parsed or re-serialized — so data is exactly what the scraper wrote. Files are replaced atomically, so a response is always one complete version of the file. Response headers: Content-Length, Last-Modified and Cache-Control: no-cache. See each bookmaker page for the structure inside data, or download a real sample.
Conditional requests (304)
Send the last Last-Modified value back as If-Modified-Since. Until the file is rewritten the API answers 304 Not Modified with no body. An invalid or future date is ignored. updated_at and Last-Modified have one-second resolution, and a 304 still counts toward your rate limit.
curl -sS -o /dev/null -w "%{http_code}\n" \
-H "X-API-Key: YOUR_API_KEY" \
-H "If-Modified-Since: Sat, 26 Sep 2026 20:02:34 GMT" \
https://api.arbplay.app/v1/data/site_live_stoiximan.json
# 304 until the file is rewritten, then 200 with the new body
Compression
Send Accept-Encoding: gzip (curl --compressed; Python requests and Node fetch do it for you). Responses arrive about 6–7 times smaller (Fonbet's 8.6 MB file comes down as about 1.2 MB), which matters most for the largest files: Superbet pre-match soccer is around 200 MB uncompressed and Bwin pre-match soccer around 10 MB.
Errors
Every error body is {"detail": "<message>"}. Checks run in this order and the first failure wins — note that an unknown filename is 403, not 404.
| Status | detail | Cause | What to do |
|---|---|---|---|
401 | Missing X-API-Key header | No key sent | Send X-API-Key (or the marketplace's header) |
401 | Invalid API key | Unknown key | Check the key; marketplace keys only work through that marketplace |
429 | Rate limit exceeded | Too many requests on this path | Wait Retry-After seconds |
400 | Invalid filename | .. or a path in the name | Send a bare filename |
403 | Only site_*.json files are allowed | Name does not match site_*.json | Use a name from /v1/files |
403 | File is not mapped to any configured company | Unknown filename | Use a name from /v1/files |
403 | Not allowed for this API key | File is not among your key's files | Subscribe to that bookmaker's listing |
404 | File not found | Known file, temporarily missing on disk | Retry later |
500 | Data file is empty | Zero-byte file | Retry later |
500 | Invalid JSON file | File is not a JSON document | Retry later |
Rate limits
- Limits are counted per key and per path: each filename is its own bucket, and
/v1/filesis another. - Marketplace traffic is limited to 10 requests per second per path at the origin; your plan's monthly quota is enforced by the marketplace.
- A
429carriesRetry-After(seconds). Polling withIf-Modified-Sinceat the file's refresh rate is the efficient pattern. - What counts as a request, and how to size a plan: Odds API FAQ.
Freshness
- Live files are rewritten about every 1–3 s and pre-match files about every 4–7 s during active hours.
File catalogue
All 34 files. Each key reads the files of its own bookmaker, or of its bundle; GET /v1/files returns the current list.
| File | Bookmaker | Type | Sport | Refresh |
|---|---|---|---|---|
site_live_stoiximan.json | Stoiximan | Live | All sports | ≈1–3 s |
site_pregame_stoiximan_soccer.json | Stoiximan | Pre-match | Soccer | ≈4–7 s |
site_pregame_stoiximan_soccer_leagues.json | Stoiximan | Pre-match | Soccer (15 major leagues) | ≈4–7 s |
site_pregame_stoiximan_basketball.json | Stoiximan | Pre-match | Basketball | ≈4–7 s |
site_pregame_stoiximan_tennis.json | Stoiximan | Pre-match | Tennis | ≈4–7 s |
site_live_novibet.json | Novibet | Live | All sports | ≈1–3 s |
site_pregame_novibet_soccer.json | Novibet | Pre-match | Soccer | ≈4–7 s |
site_pregame_novibet_basketball.json | Novibet | Pre-match | Basketball | ≈4–7 s |
site_pregame_novibet_tennis.json | Novibet | Pre-match | Tennis | ≈4–7 s |
site_livepre_fonbet.json | Fonbet | Live + pre-match | All sports | ≈1–3 s |
site_live_superbet_soccer.json | Superbet | Live | Soccer | ≈1–3 s |
site_live_superbet_basketball.json | Superbet | Live | Basketball | ≈1–3 s |
site_live_superbet_tennis.json | Superbet | Live | Tennis | ≈1–3 s |
site_pregame_superbet_soccer.json | Superbet | Pre-match | Soccer | ≈4–7 s |
site_pregame_superbet_basketball.json | Superbet | Pre-match | Basketball | ≈4–7 s |
site_pregame_superbet_tennis.json | Superbet | Pre-match | Tennis | ≈4–7 s |
site_live_elabet_soccer.json | Elabet | Live | Soccer | ≈1–3 s |
site_live_elabet_basketball.json | Elabet | Live | Basketball | ≈1–3 s |
site_live_elabet_tennis.json | Elabet | Live | Tennis | ≈1–3 s |
site_pregame_elabet_soccer.json | Elabet | Pre-match | Soccer | ≈4–7 s |
site_pregame_elabet_basketball.json | Elabet | Pre-match | Basketball | ≈4–7 s |
site_pregame_elabet_tennis.json | Elabet | Pre-match | Tennis | ≈4–7 s |
site_live_opapstore_soccer.json | OPAP (Allwyn) | Live | Soccer | ≈1–3 s |
site_live_opapstore_basketball.json | OPAP (Allwyn) | Live | Basketball | ≈1–3 s |
site_live_opapstore_tennis.json | OPAP (Allwyn) | Live | Tennis | ≈1–3 s |
site_pregame_opapstore_soccer.json | OPAP (Allwyn) | Pre-match | Soccer | ≈4–7 s |
site_pregame_opapstore_basketball.json | OPAP (Allwyn) | Pre-match | Basketball | ≈4–7 s |
site_pregame_opapstore_tennis.json | OPAP (Allwyn) | Pre-match | Tennis | ≈4–7 s |
site_live_bwin_soccer.json | Bwin | Live | Soccer | ≈1–3 s |
site_live_bwin_basketball.json | Bwin | Live | Basketball | ≈1–3 s |
site_live_bwin_tennis.json | Bwin | Live | Tennis | ≈1–3 s |
site_pregame_bwin_soccer.json | Bwin | Pre-match | Soccer | ≈4–7 s |
site_pregame_bwin_basketball.json | Bwin | Pre-match | Basketball | ≈4–7 s |
site_pregame_bwin_tennis.json | Bwin | Pre-match | Tennis | ≈4–7 s |
Code examples
Poll one file with If-Modified-Since (Python)
import time
import requests
URL = "https://api.arbplay.app/v1/data/site_live_novibet.json"
session = requests.Session()
session.headers["X-API-Key"] = "YOUR_API_KEY"
last_modified = None
while True:
headers = {"If-Modified-Since": last_modified} if last_modified else {}
resp = session.get(URL, headers=headers, timeout=60)
if resp.status_code == 200:
last_modified = resp.headers["Last-Modified"]
payload = resp.json() # {company, file, updated_at, data}
print("file updated", payload["updated_at"])
elif resp.status_code == 429:
time.sleep(int(resp.headers.get("Retry-After", "1")))
continue
elif resp.status_code != 304:
resp.raise_for_status()
time.sleep(2)
List and download every file (Node 18+)
// Node 18+, server-side (no CORS)
const BASE = "https://api.arbplay.app";
const headers = { "X-API-Key": process.env.ARBPLAY_API_KEY };
const { files } = await (await fetch(`${BASE}/v1/files`, { headers })).json();
for (const file of files) {
const res = await fetch(`${BASE}/v1/data/${file}`, { headers });
if (!res.ok) { console.error(file, res.status, (await res.json()).detail); continue; }
const { updated_at, data } = await res.json();
console.log(file, updated_at);
}
Per-bookmaker parsers for match-result odds are on each bookmaker page.
Samples, OpenAPI and Postman
- OpenAPI 3.1 spec — envelope schemas, every error, headers and examples
- Postman collection — set
apiKeyand run - Real samples — one or more per bookmaker, in the exact response format
- /llms.txt — a plain-text summary for AI assistants
Frequently asked questions
Why do I get 403 instead of 404 for a filename?
The API only answers 404 for a known file that is missing on disk. A filename that does not match site_*.json, is not in any bookmaker's list, or is not among your key's files returns 403 with a detail message. Call GET /v1/files to see the exact names your key can read.
What does updated_at mean?
The time the file was last written, in UTC with one-second resolution. It is the freshness of the whole file, not of each price inside it. The same time is sent as the Last-Modified header.
How should I poll?
Send the last Last-Modified value as If-Modified-Since. You get 304 with no body until the file changes. Polling a live file every 1 to 3 seconds or a pre-match file every 4 to 7 seconds is enough; a 304 still counts toward your rate limit.
Can I call the API from a browser?
No. The API sends no CORS headers and your key must stay secret, so call it from a server, script or backend function and pass the results to your front end.
Are responses compressed?
Send Accept-Encoding: gzip (curl --compressed; requests and most HTTP clients do this by default). JSON arrives about 6-7 times smaller (Fonbet's 8.6 MB file as about 1.2 MB), which matters for the multi-megabyte pre-match files.
Bookmaker pages: Stoiximan · Novibet · Fonbet · Superbet · Elabet · OPAP (Allwyn) · Bwin.api.arbplay.app is the Odds HTTP API. It is separate from the ArbPlay app's own endpoints under arbplay.app/api/* (sign-in, the arbitrage feed), which are not a public API.