Football odds and risk management, by API — on your odds or ours
A self-service subscription for licensed betting operators and the platforms that serve them. Register, get a key, and take the whole pre-match catalogue of a match from our own price — or send us your 1X2 and Over/Under 2.5 for the match and get your catalogue, priced from your odds. REST and JSON, one key; live updates over SignalR on the paid plans.
Not for private individuals and not a betting site. No bets are taken here. The output is information for your trading; you set your prices, margin and limits and you answer to your players and your regulator.
Plans
Loading the plans…
Quotas are hard limits: over them a request is refused with 429, nothing is charged. The free
plan can be extended on request. Competitions in own-price mode are those whose data sources permit it.
Register
Business-to-business only, not for businesses established in the United Kingdom. We record the company, its country, its licence or registration number, a contact email and the version of the terms you accept. We do not verify the licence automatically; a paid plan is enabled after we have.
For businesses only — betting operators, platforms, software developers and start-ups — never for consumers, and not for businesses established in the United Kingdom. Register for your company: we record its name, country and registration or VAT number, the betting licence number if you have one, your email, the competitions you choose (a country or the European club competitions), your password as a hash only (none when you sign up with Google or Microsoft), and the version of the terms you accept. Offering bets to players with our odds needs a licence and a paid plan: the Free plan is for evaluation, development and testing.
Your key — shown only now
Keep it: we store only its hash and cannot show it again. Send it in the X-Api-Key header
(access_token for the live hub).
Quick start
1. Today's fixtures in your competitions
curl -H "X-Api-Key: $KEY" "https://eskape.pro/api/fixtures/daily?date=2026-10-24"
2. Our catalogue for a match (~140 markets)
curl -H "X-Api-Key: $KEY" "https://eskape.pro/api/fixtures/odds-snapshot?date=2026-10-24&home=Levski%20Sofia&away=CSKA%20Sofia"
3. Your odds, our maths — send your 1X2 and Over/Under 2.5, then ask for the catalogue again
curl -X PUT -H "X-Api-Key: $KEY" -H "Content-Type: application/json" https://eskape.pro/api/fixtures/quote \
-d '{"homeTeamName":"Levski Sofia","awayTeamName":"CSKA Sofia","date":"2026-10-24",
"homeWinDecimal":2.10,"drawDecimal":3.40,"awayWinDecimal":3.60,"overDecimal":1.95,"underDecimal":1.90}'
The quote prices the match for 60 minutes or until you send a newer one; without a fresh quote you get our price. Sending the same quote twice is harmless (idempotent). Your quote is never shown to anyone else and never stored on disk.
4. Live updates — SignalR instead of polling (paid plans)
const c = new signalR.HubConnectionBuilder()
.withUrl("https://eskape.pro/hubs/feed-odds", { accessTokenFactory: () => KEY })
.withAutomaticReconnect().build();
c.on("fixtureOdds", snapshot => render(snapshot));
await c.start();
await c.invoke("SubscribeFixture", "2026-10-24", "Levski Sofia", "CSKA Sofia");
c.onreconnected(() => c.invoke("SubscribeFixture", "2026-10-24", "Levski Sofia", "CSKA Sofia"));
The connection counts as one request; the updates do not. After a reconnect, subscribe to your fixtures again.
The full reference: OpenAPI document (import it into Postman or generate a client).
Same-game combinations: POST /api/betbuilder/quote. Binary contracts: GET /api/fixtures/binary.
Settlement after full time: GET /api/settlement.
Code samples
import requests
KEY = "esk_..."
BASE = "https://eskape.pro"
H = {"X-Api-Key": KEY}
fixtures = requests.get(f"{BASE}/api/fixtures/daily", params={"date": "2026-10-24"}, headers=H).json()
first = fixtures[0]
# Your odds for the match, then your catalogue
requests.put(f"{BASE}/api/fixtures/quote", headers=H, json={
"homeTeamName": first["homeTeamName"], "awayTeamName": first["awayTeamName"], "date": "2026-10-24",
"homeWinDecimal": 2.10, "drawDecimal": 3.40, "awayWinDecimal": 3.60, "overDecimal": 1.95, "underDecimal": 1.90,
}).raise_for_status()
r = requests.get(f"{BASE}/api/fixtures/odds-snapshot", headers=H,
params={"date": "2026-10-24", "home": first["homeTeamName"], "away": first["awayTeamName"]})
r.raise_for_status()
print(r.headers.get("X-RateLimit-Remaining"), "requests left today")
for market in r.json()["markets"]:
print(market["marketCode"], [(s["key"], s["publishedOdds"]) for s in market["selections"]])
using var http = new HttpClient { BaseAddress = new Uri("https://eskape.pro") };
http.DefaultRequestHeaders.Add("X-Api-Key", "esk_...");
var fixtures = await http.GetFromJsonAsync<List<FixtureListItem>>("/api/fixtures/daily?date=2026-10-24");
var first = fixtures![0];
// Your odds for the match, then your catalogue
var quote = new { homeTeamName = first.HomeTeamName, awayTeamName = first.AwayTeamName, date = "2026-10-24",
homeWinDecimal = 2.10, drawDecimal = 3.40, awayWinDecimal = 3.60, overDecimal = 1.95, underDecimal = 1.90 };
(await http.PutAsJsonAsync("/api/fixtures/quote", quote)).EnsureSuccessStatusCode();
using var response = await http.GetAsync(
$"/api/fixtures/odds-snapshot?date=2026-10-24&home={Uri.EscapeDataString(first.HomeTeamName)}&away={Uri.EscapeDataString(first.AwayTeamName)}");
if ((int)response.StatusCode == 429)
{
// Retry-After says when; the body is application/problem+json with "code": "minute-quota-exceeded", "daily-quota-exceeded", "monthly-quota-exceeded" or "fair-use-exceeded".
}
response.EnsureSuccessStatusCode();
var snapshot = await response.Content.ReadFromJsonAsync<JsonElement>();
record FixtureListItem(string HomeTeamName, string AwayTeamName, string? Competition, DateTimeOffset KickoffUtc);
const BASE = "https://eskape.pro";
const H = { "X-Api-Key": process.env.ESKAPE_KEY, "Content-Type": "application/json" };
const fixtures = await (await fetch(`${BASE}/api/fixtures/daily?date=2026-10-24`, { headers: H })).json();
const first = fixtures[0];
// Your odds for the match, then your catalogue
await fetch(`${BASE}/api/fixtures/quote`, { method: "PUT", headers: H, body: JSON.stringify({
homeTeamName: first.homeTeamName, awayTeamName: first.awayTeamName, date: "2026-10-24",
homeWinDecimal: 2.10, drawDecimal: 3.40, awayWinDecimal: 3.60, overDecimal: 1.95, underDecimal: 1.90 }) });
const url = new URL(`${BASE}/api/fixtures/odds-snapshot`);
url.search = new URLSearchParams({ date: "2026-10-24", home: first.homeTeamName, away: first.awayTeamName });
const r = await fetch(url, { headers: H });
if (r.status === 429) console.log("retry after", r.headers.get("Retry-After"), "s");
const snapshot = await r.json();
console.log(r.headers.get("X-RateLimit-Remaining"), "requests left today", snapshot.markets.length, "markets");
My subscription
Paste your key to see your plan, today's usage and your statistics. The key is sent only to this server, in the header, and is not stored by this page.
Sign in with the email and password of your registration to see your plan, today's usage, your statistics and the first characters of your key — the key itself was shown once; a lost key is replaced by rotating it here.
Usage — last 7 days
Your new key — shown only now
The previous key keeps working until .
Limits and errors
| Status | When | What to do |
|---|---|---|
401 | missing or unknown key | send X-Api-Key; a rotated key stops after its overlap |
403 | subscription pending, suspended or ended; a fixture outside your plan's competitions (a competition of the paid plans says so); an endpoint not in the subscription; the live hub on a plan without it (realtime-not-in-plan); your own odds on more fixtures a day than the plan allows | read detail; write to us |
429 + Retry-After | over the plan's requests a minute (minute-quota-exceeded), a day (daily-quota-exceeded) or a month (monthly-quota-exceeded); more requests a minute outside the quota than fair use allows (fair-use-exceeded, the body says how many) | wait Retry-After seconds; follow X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window that runs out first resets: 00:00 UTC, or the first day of the next month); the day's fixture list and your standing (/api/subscriptions/me) are outside the quota, and refused requests are not counted; on a paid plan, use the live hub instead of polling |
400 / 404 / 409 | invalid input; fixture not listed for that date; settlement before full time | the body says why |
A refused key, plan or quota is application/problem+json (RFC 9457): { "type", "title",
"status", "detail", "code", "requestId", "error" }; other refusals (400, 404, 409, a fixture
outside your plan) are RFC 9110 problem details with status and detail. Quote requestId
(also the X-Request-Id header) when you write to us. Compatible changes (new fields) come without notice; a breaking change gets a new path
(/api/v2/…) and at least 90 days' notice with Deprecation and Sunset headers.
Try the maths without a key: the demo sportsbook and GET /api/demo/match?home=2.1&draw=3.4&away=3.6&over=1.95&under=1.9.
Service health: GET /health/live.