Foreline/Docs
API · v1One key, one base URL, three delivery shapes
Everything Foreline publishes is reachable over HTTPS from
https://api.foreline.io with a single header. This page gets you to a first
response; the reference documents every endpoint, parameter and
field.
From a key to a full ladder in three calls
GET /v1/health is the cheapest call in the API: service
state, data readiness and our clock. It does not validate your key — for that call
GET /v1/usage, which answers 401 on a bad one.GET /v1/surface?event_id=… returns, per market, the main
line, the fair price, the measured margin and every rung of the ladder with its band. The
id is an integer from GET /v1/catalog/events.GET /v1/surface?since=… returns everything that changed
after your cursor and hands back next_cursor. Send that back in
since while more is true. Same loop on
/v1/line-events and /v1/alerts:
Cursors & paging.# 1 — health (no key required; it does not check one)
curl -s "https://api.foreline.io/v1/health"
{ "status": "ok", "version": "foreline-gw/1.0", "ts": 1785450885.03,
"registry": { "readable": true }, "data": { "warm": true, … } }
# 2 — one event, full ladder
curl -s "https://api.foreline.io/v1/surface?event_id=1632432003" \
-H "X-Foreline-Key: $FORELINE_KEY"
# 3 — bulk delta from the start, then repeat with the returned next_cursor
curl -s "https://api.foreline.io/v1/surface?since=0&limit=200" \
-H "X-Foreline-Key: $FORELINE_KEY"
{ "events": [ … ], "more": true,
"next_cursor": "1785433471.4442058:1632973256", "next_since": 1785433471.4442058, … }
# 4 — the next page: next_cursor goes back in `since`, verbatim.
# Loop on `more`, never on the row count, and never on next_since.
curl -s "https://api.foreline.io/v1/surface?since=1785433471.4442058:1632973256&limit=200" \
-H "X-Foreline-Key: $FORELINE_KEY"
The same three calls as a script you can run
Python 3.7+ and the standard library — no SDK, no pip install, no OAuth. Paste your key on the second line and run it. It prints what your key carries, picks a match from the catalog and prints that match's whole ladder.
import json, urllib.request, urllib.error
KEY = "PASTE-YOUR-KEY-HERE"
BASE = "https://api.foreline.io"
def get(path, key=KEY):
req = urllib.request.Request(BASE + path, headers={"X-Foreline-Key": key})
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)
except urllib.error.HTTPError as e:
body = json.load(e) # every error body is JSON
raise SystemExit("HTTP %s on %s\n %s\n %s" % (
e.code, path, body.get("error"), body.get("hint", "")))
# 1. Is the service up? The cheapest call in the API — it needs no key.
h = get("/v1/health", key="")["data"]
print("service ok, %s events in memory, last tick %.0fs ago"
% (h["events"], h["last_tick_age_s"]))
# 2. What does your key carry? Products, sports, history depth, limits.
u = get("/v1/usage")
print("products: %s | sports: %s | history: %s | cadence: %ss"
% (", ".join(u["scopes"]), ", ".join(u["sports"]),
u["asof_days"] or "unlimited", u["data_cadence_s"]))
# 3. Which matches can you ask about? Always address events by OUR event_id.
ev = get("/v1/catalog/events?hours=24&limit=3")["events"]
for e in ev:
print("%s %s vs %s (%s)"
% (e["event_id"], e["home"], e["away"], e["league"]))
# 4. The product: the whole ladder for one match — the rungs the reference
# market quotes AND the rungs it does not.
s = get("/v1/surface?event_id=%s" % ev[0]["event_id"])
for name in ("spread", "totals"):
m = s["markets"].get(name)
if m is None:
continue # a market the reference is not quoting right now
print("\n%s: main line %s, fair %s, vig tier %s"
% (name, m["main_line"], m["fair_market"], m["vig_tier"]))
for r in m["rungs"][:5]:
p = r["p_home"] if name == "spread" else r["p_over"]
band = r["band_pp"]
print(" line %-6s p %-8s band %-14s %s"
% (r["line"], p,
"n/a" if band is None else "%+.2f..%+.2f" % tuple(band),
r["src"]))
print("\n'quote' = the reference market quotes that rung.")
print("'table' = we reconstruct it, and the band widens to say so.")
Values in examples throughout the docs come from real responses and will
not match yours. next_since in that envelope is a legacy epoch boundary, not a
paging cursor — see Cursors & paging for what it
costs to loop on it.
One header, one key per environment
- Header:
X-Foreline-Key: <your key>on every request, including the SSE stream. There is no OAuth dance and no token exchange. - Transport: HTTPS only. Requests without the header, or with a key that has been
revoked, return
401. - Scope: a key carries the products, sports and history depth in your contract. A
call to something outside that scope returns
403rather than an empty result — you always know the difference between “not permitted” and “nothing there”. - Usage:
GET /v1/usagereturns your consumption against the rpm and rpd quotas on the key, so you can alert on your own headroom instead of discovering it as429s.
The data moves every 30 seconds, so the limits are shaped around that
Our rate limits are not a monetisation device; they follow from how often the
underlying data actually changes. Polling faster than the cadence returns the same bytes with
the same updated_ts.
data_cadence_s is returned on every surface and line-event response, and its
value today is 30: thirty seconds is how often the reference data behind a price
can change, at the fastest. Alongside it, data_cadence_tiers_s breaks the figure
out by distance to kick-off — {"lt_24h": 30, "24h_96h": 60, "gt_96h": 60}. We
refresh events starting within a day twice as often as the rest, so a client that schedules
per event rather than per listing spends half the calls on the far ones for the same freshness.
Schedule off these fields rather than off a constant of your own, and your poller keeps pace
with the data if the numbers ever change.
| Call class | Limit | Endpoints | Why |
|---|---|---|---|
| bulk | 4 / min, 3 000 / day | GET /v1/surface — both the parameterless summary and
?since= — plus /v1/catalog/events and
/v1/catalog/leagues |
All four are full scans; four passes a minute already outruns the 30-second cadence on the near tier. The catalog shares the class — put a catalog refresh on a different minute from your sync loop. |
| point | 60 / min, 30 000 / day | GET /v1/surface?event_id= |
Sized for interactive use — a trading screen, a risk check at bet placement. |
| asof | 20 / min, 5 000 / day | GET /v1/surface?asof= |
A past instant is rebuilt from the feed archive on disk, not read from memory, so it carries its own budget. |
| events / alerts | 12 / min · 60 / min | /v1/line-events · /v1/alerts |
Catch-up polling, and the working loop of Line Radar respectively — which is why they are separate classes. For live delivery use the SSE stream instead of polling harder. |
| score | 120 / min, 60 000 / day | POST /v1/score |
Sized for a book that posts each bet as it accepts it. |
Those are the classes a first integration meets; the full set of fourteen,
with the endpoint of each, is in the
reference. Quotas are per client on top of them: an rpm
(requests per minute) and an rpd (requests per day) ceiling on the key, both visible from
GET /v1/usage together with your actual per-class limits. Exceeding either
returns 429 with a Retry-After header and a body naming the class
that bit — respect it and you will not be throttled further.
Three ways to receive the same facts
Most integrations use two: a bulk delta to keep a local mirror warm, and the stream for the moments that matter.
Point read
One object, right now or as of a past timestamp.
GET /v1/surface?event_id= and ?asof=. Use it for screens, for a
risk check at placement, and for settling a dispute about what was published when.
Bulk delta
Everything that changed after your cursor, as full surfaces:
GET /v1/surface?since=, with next_cursor and more in
the response. This is the backbone of a local mirror — no diff logic on your side.
Server-sent events
A held connection on GET /v1/stream emitting
event: line_change as main-line moves are detected — the same rows
/v1/line-events returns. Across a restart, close the gap by replaying
/v1/line-events?since=<your last next_cursor>: the stream resumes by
timestamp, the feed resumes exactly.
Which call serves which product
| Product | Endpoints | Page |
|---|---|---|
| Fair-Price Surface | GET /v1/surface — summary, ?event_id=,
?asof=, ?since= |
Fair-Price Surface |
| Player Radar | POST /v1/score |
Player Radar |
| Line Radar | GET /v1/line-events, GET /v1/stream,
GET /v1/alerts |
Line Radar |
| Parlay Protection | POST /v1/parlay/price, POST /v1/parlay/audit,
GET /v1/parlay — joint prices for a coupon and the correlation gap against
a naive multiply; marginals are the same GET /v1/surface |
API reference |
| Event catalog | GET /v1/catalog/events, GET /v1/catalog/leagues — our
ids, team names, kick-off and priced markets; JSON or CSV |
Integration guide |
| All | GET /v1/health, GET /v1/usage |
Reference |
Three statuses worth handling
| Status | Meaning | What to do |
|---|---|---|
401 | Missing, malformed or revoked key | Fix the header. Retrying will not help. |
403 | Authenticated, but outside the scope of your contract — a sport, product or history depth you do not hold | Treat as a permanent answer for that call; talk to us if the scope is wrong. |
429 | Per-class limit or account quota exceeded | Wait for Retry-After (seconds) and resume. Do not back off blindly —
the header tells you exactly when. |
Check the data before you integrate it
The proof layer is public and needs neither a key nor an NDA — you can audit the record before you write a line of client code.
- Hash chain and Merkle commitments. Every published row is chained; every pass is committed; the daily roots are anchored in Bitcoin via OpenTimestamps. Rows cannot be edited or selectively dropped after the fact.
- Public verifier:
github.com/foreline-io/foreline-proofs
—
python3 verify.pychecks the daily roots, the commitment chain and the Bitcoin stamps. - Tamper-evident from 23 July 2026. As-of history reaches further back — football from 2022 — and rows predating the ledger are served as archive, labelled as such.
Request a trial key
Keys are issued per contract — tell us which products, sports and history depth you need and we will scope one.
We usually reply within one business day. Pricing is quoted per scope — ask and we will send terms.
Foreline