Working code samples for every endpoint. In the curl samples, replace YOUR_API_KEY with your key; the Python and JavaScript samples run on your server and read it from PUNTERSEDGE_API_KEY. Putting prices on a public page? See showing prices on a public website.
Returns the best win and place price for every runner across the Australian bookmakers, with the book offering it, a deep link to that book's race page, and the market percentage so you can see the overround.
curl "https://api.puntersedge.online/v1/racing/best-odds?categories=horse" \ -H "X-API-Key: YOUR_API_KEY"
import os
import requests
r = requests.get(
"https://api.puntersedge.online/v1/racing/best-odds",
params={"categories": "horse"},
headers={"X-API-Key": os.environ["PUNTERSEDGE_API_KEY"]},
timeout=15,
)
r.raise_for_status()[
{
"race_id": "race_123", "venue": "Rosehill", "race_number": 8, "category": "horse",
"country": "AU", "start_time": "2026-08-15T05:45:00Z", "race_name": "TAB ROSEBUD",
"track_condition": "Good (4)", "market_percentage": 106.4, "books_compared": 11,
"scratchings": [], "data_age_seconds": 41, "freshest_age_seconds": 3, "stale": false,
"runners": [
{
"name": "Chilly Girl", "number": 7, "barrier": 3,
"jockey": "J Mcdonald", "trainer": "C Waller",
"best_win": {"price": 6.5, "bookmaker": "betright", "age_seconds": 6, "stale": false},
"best_place": {"price": 2.15, "bookmaker": "tab", "age_seconds": 4, "stale": false}
}
]
}
]Returns the next N races with runners and current fixed odds. Cost: 2 credits.
# All racing categories curl "https://api.puntersedge.online/v1/racing/next-to-go?num_races=5&country=AU" \ -H "X-API-Key: YOUR_API_KEY" # Horse racing only curl "https://api.puntersedge.online/v1/racing/next-to-go?num_races=5&categories=horse&country=AU" \ -H "X-API-Key: YOUR_API_KEY"
import os
import requests
r = requests.get(
"https://api.puntersedge.online/v1/racing/next-to-go",
params={"num_races": 5, "categories": "horse"},
headers={"X-API-Key": os.environ["PUNTERSEDGE_API_KEY"]},
timeout=15,
)
r.raise_for_status()// Server-side: Node 18+, Deno or Bun. Returns a JSON ARRAY of races.
const res = await fetch(
"https://api.puntersedge.online/v1/racing/next-to-go?num_races=5&categories=horse&country=AU",
{
headers: { "X-API-Key": process.env.PUNTERSEDGE_API_KEY },
signal: AbortSignal.timeout(10_000),
}
);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const races = await res.json();
for (const race of races) {
console.log(`${race.venue} R${race.race_number} (${race.category})`);
for (const runner of race.runners.slice(0, 4)) {
const best = runner.bookmakers.reduce((a, b) => (b.win_price > a.win_price ? b : a));
console.log(` ${runner.name} ${best.win_price} (${best.key})`);
}
}Your API key stays on a server. Anything sent to a browser can be read by whoever loads the page, so a key in front-end JavaScript or a mobile app is a published key, and anyone holding it can spend your credits (terms, section 4.2). There are two supported ways to put prices in front of visitors:
/widget/racing is an iframe with no key and no signup: the next Australian races with the best price per runner and a Powered by PuntersEdge credit. The greyhound, harness, thoroughbred and sports versions, and the theme, layout and limit options, are on the widgets page. On Platform, the white-label widget shows the same prices in your colours, with your bookmaker links and no PuntersEdge credit.// server.mjs: Node 18+, no dependencies. The key never leaves this process.
import { createServer } from "node:http";
const UPSTREAM = "https://api.puntersedge.online/v1/racing/next-to-go?num_races=5&categories=horse&country=AU";
let cache = { at: 0, job: null };
async function load() {
const res = await fetch(UPSTREAM, {
headers: { "X-API-Key": process.env.PUNTERSEDGE_API_KEY },
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) throw new Error(`PuntersEdge ${res.status}: ${await res.text()}`);
// Only what the page shows: venue, race, jump time, best win price per runner.
return JSON.stringify((await res.json()).map((r) => ({
venue: r.venue, race: r.race_number, starts: r.start_time,
runners: r.runners
.map((x) => ({ name: x.name, best: Math.max(0, ...x.bookmakers.map((b) => b.win_price ?? 0)) }))
.filter((x) => x.best > 0),
})));
}
createServer(async (req, res) => {
if (req.url !== "/prices") return res.writeHead(404).end();
if (!cache.job || Date.now() - cache.at > 10_000) {
cache = { at: Date.now(), job: load() }; // one upstream call per 10 seconds
}
try {
const body = await cache.job;
res.writeHead(200, { "Content-Type": "application/json" }).end(body);
} catch (err) {
cache.job = null; // try upstream again on the next request
console.error(err); // the detail stays in your logs
res.writeHead(502, { "Content-Type": "application/json" }).end('{"error":"prices unavailable"}');
}
}).listen(3000);The page then calls fetch("/prices") on your own site, with no key anywhere in it. Every visitor inside the same ten seconds shares one upstream call, so the cost is one call per cache window while the page has visitors, not one per visitor; lengthen the window to suit your plan. Which plan covers showing prices to your own users is set out in section 8.1 of the terms.
Runtime support and key safety are separate questions: fetch and the TypeScript client run in Node, Deno, Bun and browsers, but a browser is not somewhere a key can live.
Returns credits used and remaining this month. Cost: 0 credits.
curl https://api.puntersedge.online/v1/usage \ -H "X-API-Key: YOUR_API_KEY"
{
"plan": "free",
"credits_limit": 3000,
"credits_used": 47,
"credits_remaining": 1453,
"reset_at": "2026-09-01T00:00:00",
"usage_by_endpoint": [
{"endpoint": "/v1/racing/next-to-go", "calls": 20, "credits": 40}
]
}Returns all sports with active odds coverage. Cost: 1 credit.
curl https://api.puntersedge.online/v1/sports \ -H "X-API-Key: YOUR_API_KEY"
import os
import requests
r = requests.get(
"https://api.puntersedge.online/v1/sports",
headers={"X-API-Key": os.environ["PUNTERSEDGE_API_KEY"]},
timeout=15,
)
r.raise_for_status()
print(r.json())// Server-side: Node 18+, Deno or Bun.
const res = await fetch("https://api.puntersedge.online/v1/sports", {
headers: { "X-API-Key": process.env.PUNTERSEDGE_API_KEY },
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const data = await res.json();
console.log(data);[
{"key": "afl", "title": "AFL", "group": "Australian Rules", "active": true},
{"key": "nrl", "title": "NRL", "group": "Rugby League", "active": true},
{"key": "nba", "title": "NBA", "group": "Basketball", "active": true}
]Returns upcoming NRL matches with h2h prices from all bookmakers. Cost: 1 credit.
curl "https://api.puntersedge.online/v1/sports/nrl/odds?markets=h2h" \ -H "X-API-Key: YOUR_API_KEY"
import os
import requests
r = requests.get(
"https://api.puntersedge.online/v1/sports/nrl/odds",
params={"markets": "h2h"},
headers={"X-API-Key": os.environ["PUNTERSEDGE_API_KEY"]},
timeout=15,
)
r.raise_for_status()
print(r.json())// Server-side: Node 18+, Deno or Bun.
const res = await fetch(
"https://api.puntersedge.online/v1/sports/nrl/odds?markets=h2h",
{
headers: { "X-API-Key": process.env.PUNTERSEDGE_API_KEY },
signal: AbortSignal.timeout(10_000),
}
);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const data = await res.json();[
{
"id": "abc123",
"sport_key": "nrl",
"sport_title": "NRL",
"home_team": "Melbourne Storm",
"away_team": "Penrith Panthers",
"commence_time": "2026-06-07T09:50:00Z",
"bookmakers": [
{
"key": "sportsbet",
"title": "Sportsbet",
"markets": [
{
"key": "h2h",
"outcomes": [
{"name": "Melbourne Storm", "price": 1.85},
{"name": "Penrith Panthers", "price": 1.95}
]
}
]
}
]
}
]Get h2h and line (spreads) in one call. Cost: 2 credits (1 per market type).
curl "https://api.puntersedge.online/v1/sports/afl/odds?markets=h2h,spreads" \ -H "X-API-Key: YOUR_API_KEY"
import os
import requests
r = requests.get(
"https://api.puntersedge.online/v1/sports/afl/odds",
params={"markets": "h2h,spreads"},
headers={"X-API-Key": os.environ["PUNTERSEDGE_API_KEY"]},
timeout=15,
)
r.raise_for_status()Returns the best available price per selection across all bookmakers, and flags any arbitrage opportunities. Cost: 3 credits.
curl "https://api.puntersedge.online/v1/best-odds/afl" \ -H "X-API-Key: YOUR_API_KEY"
import os
import requests
r = requests.get(
"https://api.puntersedge.online/v1/best-odds/afl",
headers={"X-API-Key": os.environ["PUNTERSEDGE_API_KEY"]},
timeout=15,
)
r.raise_for_status()// Server-side: Node 18+, Deno or Bun.
// Also a bare array. Each event carries `selections`, already merged across books.
const res = await fetch("https://api.puntersedge.online/v1/best-odds/afl", {
headers: { "X-API-Key": process.env.PUNTERSEDGE_API_KEY },
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const events = await res.json();
for (const ev of events) {
console.log(`${ev.home_team} v ${ev.away_team}`);
for (const sel of ev.selections) {
console.log(` ${sel.name} ${sel.best_price} (${sel.best_bookmaker})`);
}
if (ev.arb_exists) console.log(` arb ${ev.arb_profit_pct}%`);
}[
{
"id": "def456",
"sport_key": "afl",
"home_team": "Collingwood",
"away_team": "Carlton",
"commence_time": "2026-08-22T09:20:00Z",
"selections": [
{"name": "Collingwood", "best_price": 1.92, "best_bookmaker": "tab"},
{"name": "Carlton", "best_price": 2.10, "best_bookmaker": "sportsbet"}
],
"arb_exists": false,
"arb_profit_pct": -3.8,
"data_age_seconds": 41,
"freshest_age_seconds": 3,
"stale": false,
"stale_bookmakers": []
}
]Scans upcoming markets for true arbitrage and best-odds overlays, returning arb_pct, optimal stakes and per-selection price comparison. Set min_profit_pct=0 to also see value overlays. Cost: 3 credits.
curl "https://api.puntersedge.online/v1/arb/sports?sport_key=nrl&min_profit_pct=0" \ -H "X-API-Key: YOUR_API_KEY"
import os
import requests
r = requests.get(
"https://api.puntersedge.online/v1/arb/sports",
params={"sport_key": "nrl", "min_profit_pct": 0},
headers={"X-API-Key": os.environ["PUNTERSEDGE_API_KEY"]},
timeout=15,
)
r.raise_for_status()
for evt in r.json():
if evt["is_arb"]:
print(evt["home_team"], "vs", evt["away_team"], evt["arb_pct"], "%")[
{
"sport_key": "nrl",
"home_team": "Melbourne Storm",
"away_team": "Penrith Panthers",
"commence_time": "2026-06-21T09:50:00Z",
"is_arb": true,
"arb_pct": 2.84,
"max_overlay_pct": 3.1,
"optimal_stakes": [
{"name": "Melbourne Storm", "stake": 52.7, "bookmaker": "sportsbet", "profit_if_wins": 5.41},
{"name": "Penrith Panthers", "stake": 47.3, "bookmaker": "neds", "profit_if_wins": 5.39}
],
"selections": [
{"name": "Melbourne Storm", "best_price": 1.96, "best_bookmaker": "sportsbet",
"avg_price": 1.88, "overlay_pct": 3.1,
"all_prices": [{"bookmaker": "sportsbet", "price": 1.96}, {"bookmaker": "tab", "price": 1.90}]}
]
}
]Snapshots over a date range and detected price moves. Available on Standard, Business and Scale (and legacy Pro, Growth and Enterprise keys). Free and Hobby get a 403. Cost: 5 credits each.
# Historical h2h snapshots for the last 7 days curl "https://api.puntersedge.online/v1/sports/afl/odds/history?market=h2h" \ -H "X-API-Key: YOUR_API_KEY" # Price movements in the last 24h (min 0.05 drift) curl "https://api.puntersedge.online/v1/sports/afl/odds/movements?min_delta=0.05" \ -H "X-API-Key: YOUR_API_KEY"
# Every error is application/problem+json with the same four keys.
# Missing API key — 401
{"type": "https://puntersedge.online/errors/unauthorized",
"title": "X-API-Key header required",
"status": 401,
"detail": "X-API-Key header required"}
# Invalid API key — 401
{"type": "https://puntersedge.online/errors/unauthorized",
"title": "Invalid API key",
"status": 401,
"detail": "Invalid API key"}
# Monthly credits exhausted — 402 (detail names your plan and limit)
{"type": "https://puntersedge.online/errors/payment-required",
"title": "Credit limit reached (3000/mo on free plan). Upgrade at puntersedge.online/api",
"status": 402,
"detail": "Credit limit reached (3000/mo on free plan). Upgrade at puntersedge.online/api"}
# Plan does not include this endpoint — 403 (historical odds & movements)
{"type": "https://puntersedge.online/errors/forbidden",
"title": "Historical odds are available on Standard, Business, Scale and Enterprise plans only",
"status": 403,
"detail": "Historical odds are available on Standard, Business, Scale and Enterprise plans only"}
# Unknown sport_key — 404 (the full list of valid keys is returned with it)
{"type": "https://puntersedge.online/errors/not-found",
"title": "Unknown sport_key 'notasport'. Valid sport_keys: afl, aflw, basketball_other, ... See GET /v1/sports.",
"status": 404,
"detail": "Unknown sport_key 'notasport'. Valid sport_keys: ... See GET /v1/sports."}
# The live response spells out every active key (19 as at 2026-08-16). Read them from
# GET /v1/sports rather than hard-coding this list — sports are added without notice.
# Too many requests — 429 (detail is an object here, not a string)
{"type": "https://puntersedge.online/errors/rate-limit",
"title": "Error",
"status": 429,
"detail": {"error": "rate_limit_exceeded",
"message": "Rate limit exceeded: 30 requests/minute on free plan.",
"upgrade_url": "https://puntersedge.online/api",
"retry_after_seconds": 60}}Open-source, MIT licensed. Each one runs against the keyless demo endpoints before you register, so you see real output first and add a key second.