Foreline/Docs

API · v1

One 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.

API reference → Integration guide → Starter and Pro are self-serve: buy them and get your key in your account. Enterprise is issued per contract; a trial key is issued on request from the same account.
Quickstart

From a key to a full ladder in three calls

Check the service is up 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.
Read one event in full 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.
Stay in sync 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.

Authentication

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 403 rather than an empty result — you always know the difference between “not permitted” and “nothing there”.
  • Usage: GET /v1/usage returns your consumption against the rpm and rpd quotas on the key, so you can alert on your own headroom instead of discovering it as 429s.
Cadence & polling

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 classLimitEndpointsWhy
bulk4 / 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.
point60 / min, 30 000 / day GET /v1/surface?event_id= Sized for interactive use — a trading screen, a risk check at bet placement.
asof20 / 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 / alerts12 / 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.
score120 / 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.

Delivery shapes

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.

Endpoints by product

Which call serves which product

ProductEndpointsPage
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
AllGET /v1/health, GET /v1/usage Reference
Errors

Three statuses worth handling

StatusMeaningWhat to do
401Missing, malformed or revoked key Fix the header. Retrying will not help.
403Authenticated, 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.
429Per-class limit or account quota exceeded Wait for Retry-After (seconds) and resume. Do not back off blindly — the header tells you exactly when.

Full error reference →

Verifiability

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-proofspython3 verify.py checks 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.

Go to the API reference →

Access

Request a trial key

Keys are issued per contract — tell us which products, sports and history depth you need and we will scope one.

Email us

We usually reply within one business day. Pricing is quoted per scope — ask and we will send terms.

Request trial