Build your own tools on the league — a cheat sheet, a "best available" board, a script that sets your pick queue, or a season manager that picks your six, moves the armband and works the wire for you. Everything you can do on the site, you can do here.
- Acts as you. A key resolves to one manager. It reads league-public data and can only ever change your own team — your queue, your six, your armband, your wire bids. You can't see or touch anyone else's.
- Instance-stamped. Every response includes
"instance": "league"(or"demo") so you always know which server answered. - Add-only fields. Existing fields won't be renamed or change type; new ones may be added. Safe to build on without a version pin.
- CORS-open. The API sends
Access-Control-Allow-Origin: *, so a browser tool hosted anywhere can call it (auth is by key, never cookies).
export line.)
The demo is read-only — sign in to your own league to mint a key.
Base URL https://ligazo.app/demo. Authenticate with your key as
?key=… or an X-Api-Key header. If a key is presented and it's wrong,
you get a 401 even in a signed-in browser — a bad key never silently falls back
to your session.
/api/draft — every league at once (array under leagues)./api/draft/<league> — one league./api/draft/<league>/status — cheap poll: picks_made,
on_the_clock, your_next_pick. Poll this; fetch the big one on change./api/draft/<league>/queue — your current pick queue, in order./api/draft/<league>/queue — set your whole queue. Body
{"players": ["id1", "id2", …]} in priority order. Ids that aren't draftable
(already taken, wrong league, unknown) come back under rejected./api/lineup/<league> — your six, plus the whole roster with each player's
state, lock and armband eligibility. Add ?week=N./api/lineup/<league> — set the six. Body
{"players": ["id", …]} — at most 6, at most one club GK, and every id must be
on your roster for that week./api/lineup/<league>/captain — set the armband. Body
{"player": "<id>"}./api/wire/<league> — your pending bids, credits, droppable roster, and how
the wire behaves right now (settles_week, adds_left)./api/wire/<league> — sealed FAAB bid. Body
{"add": "<id>", "drop": "<id>"|null, "bid": <credits>}./api/wire/<league>/drop — a pure drop, applied immediately (not a bid).
Body {"drop": "<id>"}./api/wire/<league>/cancel/<intent_id> — cancel one of your pending
bids. The id comes from pending[].intent_id./api/rules — the scoring matrix, roster/lineup/armband structure and the
wire economy, straight from the engine so it never drifts./api/scores/<league>?week=N — the complete composition of every
player's score. Points per rule, the raw stat line they came from, and the residual.
?player=<id> for one, ?mine=1 for your roster./api/standings (or /<league>) — the table: cumulative points
per manager, overall and per league, ranked (ties share a rank)./api/fixtures (or /<league>) — real-match Score Center: each
game's clubs, score, status (sched/live/ht/ft), minute, kickoff.409, not a 400 — the request was
well-formed, the rules said no. The body is { "error": "<the reason, in plain
English>" } and it is meant to be shown to a human: "Igor Thiago has already
played this week — you can't drop him until Monday's reset." A 400 means the
body was malformed; 404 means the league name is wrong (the response lists the
valid ones).429 with a Retry-After header — pause and retry.ETag. Send it
back as If-None-Match and you get 304 Not Modified (no body) when nothing
changed — cheap polling. Pair it with /status for a near-free refresh loop.<league> is the exact, URL-encoded league name. All five,
in draft order (append /queue or /status to any of these):
https://ligazo.app/demo/api/draft/ESP-La%20Liga?key=$KEY
https://ligazo.app/demo/api/draft/ENG-Premier%20League?key=$KEY
https://ligazo.app/demo/api/draft/FRA-Ligue%201?key=$KEY
https://ligazo.app/demo/api/draft/ITA-Serie%20A?key=$KEY
https://ligazo.app/demo/api/draft/GER-Bundesliga?key=$KEY
A single league object (the all-leagues call wraps these under
leagues:[…] with instance, season, generated_at):
{
"instance": "league", // "league" or "demo" — which server answered
"generated_at": "2026-08-09T…Z",
"league": "ESP-La Liga", // exact id — use it in the URL
"name": "La Liga",
"status": "live", // pending | live | complete
"snake": true, // draft snakes: even rounds reverse
"roster_size": 8,
"picks_made": 4,
"slots_remaining": 92,
"available_count": 477,
"on_the_clock": { // null unless status == "live"
"pick": 5, "round": 1, "manager_id": 12, "manager": "…", "team": "…",
"deadline": "2026-08-09T16:33:35Z"
},
"picks": [ { "pick": 1, "round": 1, "made_at": "…", "manager_id": 7,
"manager": "…", "team": "…", ...player }, … ],
"managers": [ { "manager_id": 1, "manager": "…", "team": "…",
"seat": 0, // draft-seat order (0-based)
"slots_left": 7,
"autopick": false, // on Auto Draft?
"roster": [ ...player ] }, … ],
"available": [ ...player ], // who's LEFT, best prev-season first
"you": { // present when you call with your key
"manager_id": 1,
"your_next_pick": 14, // your next pick number (null if done) — no snake math
"autopick": false
}
}
// ...player =
{ "player_id": "p123", "player": "Kylian Mbappé", "pos": "FWD", "club": "Real Madrid",
"prev_pts": 152, // last season's Ligazo points; 0 for newcomers
"kind": "player", // "player" | "club_gk"
"display_name": "Kylian Mbappé" } // for a club-GK: "FC Barcelona (GK)"
{ instance, league, manager_id, queue:[…player…] },
plus rejected:[…ids…] on a POST. A goalkeeper slot is drafted by club, not
person: it comes back as kind: "club_gk" — use display_name and don't
parse the raw GK::<club> id.Three things run your season: the six you field, the armband, and the wire. Two rules govern all of them.
roster; he's listed separately under
incoming. Fielding him early would score a guaranteed zero, so the API refuses
it.?week=N takes the current week or the next one, nothing else
(anything further is a 400 with a hint). The current week is still fully editable
for every club that hasn't kicked off.
GET /api/lineup/<league>?week=3
{
"league": "ITA-Serie A", "manager_id": 1,
"week": 3, "current_week": 3, "editable_weeks": [3, 4],
"lineup_size": 6,
"six": ["c0045", "p01879", …], // the submitted six, in id order
"captain": "p01879", // null if nobody wears it
"captain_state": "open", // "open" | "locked" (his match kicked off)
"unlocked_count": 5, // how many you can still move
"roster": [ {
"player_id": "c0045", "player": "GK::AC Milan", "pos": "GK", "club": "AC Milan",
"starting": true, // in the six right now
"locked": false, // his club has kicked off this week
"state": "starting", // played | starting | bench | unavailable
"armband_uses_left": 5, // of 6, per player, per season
"armband_eligible": true, // uses left AND past the 3-week quarantine
"acquired_week": null // null = drafted, so never quarantined
}, … ],
"incoming": [ { "player_id": "…", "player": "…", "pos": "…", "club": "…",
"from_week": 4 } ] // yours LATER — can't be fielded this week
}
state is the one field to branch on:
played = locked and counted, leave him alone · starting = in the six and still
movable · bench = movable, not in the six · unavailable = his club has played
and he wasn't in the six, so he can't come in until Monday.POST /api/lineup/<league>?week=3 {"players": ["c0045", "p01879", …]}
POST /api/lineup/<league>/captain {"player": "p01879"}
The wire clears every morning (08:05 Europe/London) into the week being played, so a claim you win overnight is startable that day.
GET /api/wire/<league>
{
"week": 3, "settles_week": 3, // the week a bid placed NOW would land in
"credits": 100, // your FAAB warchest — same one the market uses
"opening_window": true, // weeks 1-3: adds are uncapped
"adds_used": 0, "adds_left": null, // adds_left is null while uncapped
"roster_size": 8,
"roster": [ { "player_id": "…", "player": "…", "pos": "…", "club": "…",
"locked": false } ], // your drop candidates; locked ones must wait
"pending": [ …your bids in THIS league… ],
"pending_all": [ …your bids in EVERY league… ],
"rule": "…how the wire behaves right now, in one sentence…"
}
// a pending bid
{ "intent_id": 291, "league": "ITA-Serie A", "week": 3, "bid": 4,
"add": { "player_id": "…", "player": "…", "locked": false },
"drop": { "player_id": "…", "player": "…", "locked": true },
"held": true, // it will NOT clear tonight
"held_by": ["Alex Scott"], // who is blocking it
"clears": "Monday's run" } // vs "the next morning clear"
adds_left — while pure drops stay unlimited. Pending bids are free; it's the win
that spends the allowance, so contingent bids cost you nothing.GK::<club>
in player on these endpoints (the draft API's display_name isn't
here yet) — render them from club, and remember the roster cap is one./api/scores/<league>?week=N breaks every player's week
into the rule that paid for it.
{
"league": "ITA-Serie A", "week": 2, "frozen": true, "count": 397,
"rules": { "appearance": "+2 for 60 minutes or more, +1 below that", … },
"players": [ {
"player_id": "p01879", "player": "Jurgen Ekkelenkamp", "pos": "MID", "club": "Udinese",
"total": 8, "tier1": 7, "tier2": 1, "matches": 1, "best_match": 8,
"breakdown": { "appearance": 2, "goals": 5, "shots_on_target": 1, … }, // POINTS
"residual": 0, // points the breakdown can't account for
"exact": false, // captured at scoring time, or reconstructed after?
"stats": { "minutes": 90, "goals": 1, "shots_on_target": 2, "cbit": 1, … } // COUNTS
}, … ]
}
breakdown plus
residual always equal total. A breakdown can therefore never
disagree with the score a manager was paid — if something can't be explained it shows up as
a residual rather than being smeared across the components.exact is true when the composition was captured from
the match that actually scored. It is false for weeks frozen before we started capturing it,
where the numbers are reconstructed from the week's stat line. Two things that cannot survive
that reconstruction: a double gameweek — a week scores the single BEST match while the
stat line is the week's SUM — and own goals, missed penalties and saved penalties,
which were never stored. Both land in residual. Currently 224 of 5,115
player-weeks carry one.stats is the whole week, breakdown is the
scoring match. For a single-match week they describe the same football. For a double they
don't, and the stat line will look richer than the points — that is the best-match rule, not
an error.export KEY=your-key and the
examples below work as-is.
curl "https://ligazo.app/demo/api/draft?key=$KEY"
curl "https://ligazo.app/demo/api/draft/ESP-La%20Liga/status?key=$KEY"
curl "https://ligazo.app/demo/api/rules?key=$KEY"
curl "https://ligazo.app/demo/api/standings?key=$KEY" curl "https://ligazo.app/demo/api/fixtures/ESP-La%20Liga?key=$KEY"
curl "https://ligazo.app/demo/api/scores/ESP-La%20Liga?week=2&mine=1&key=$KEY" # one player, and the whole league-week curl "https://ligazo.app/demo/api/scores/ESP-La%20Liga?week=2&player=p01879&key=$KEY" curl "https://ligazo.app/demo/api/scores/ESP-La%20Liga?week=2&key=$KEY"
curl "https://ligazo.app/demo/api/lineup/ESP-La%20Liga?key=$KEY"
curl -X POST "https://ligazo.app/demo/api/lineup/ESP-La%20Liga?key=$KEY" \
-H 'Content-Type: application/json' \
-d '{"players":["id1","id2","id3","id4","id5","id6"]}'
curl -X POST "https://ligazo.app/demo/api/lineup/ESP-La%20Liga/captain?key=$KEY" \
-H 'Content-Type: application/json' -d '{"player":"id1"}'
curl "https://ligazo.app/demo/api/wire/ESP-La%20Liga?key=$KEY"
curl -X POST "https://ligazo.app/demo/api/wire/ESP-La%20Liga?key=$KEY" \
-H 'Content-Type: application/json' \
-d '{"add":"newguy","drop":"oldguy","bid":4}'
curl -X POST "https://ligazo.app/demo/api/wire/ESP-La%20Liga/drop?key=$KEY" \
-H 'Content-Type: application/json' -d '{"drop":"oldguy"}'
curl -X POST "https://ligazo.app/demo/api/wire/ESP-La%20Liga/cancel/291?key=$KEY"
curl -sD- -o/dev/null -H 'If-None-Match: "<etag-from-last-response>"' \ "https://ligazo.app/demo/api/draft/ESP-La%20Liga?key=$KEY"
curl "https://ligazo.app/demo/api/draft/ESP-La%20Liga?key=$KEY"
curl -X POST "https://ligazo.app/demo/api/draft/ESP-La%20Liga/queue?key=$KEY" \
-H "Content-Type: application/json" \
-d '{"players": ["id1", "id2", "id3"]}'
import os, requests, urllib.parse
BASE, KEY = "https://ligazo.app/demo", os.environ["KEY"]
draft = requests.get(f"{BASE}/api/draft", params={"key": KEY}).json()
for lg in draft["leagues"]:
if lg["status"] == "complete":
continue
fwds = [p["player_id"] for p in lg["available"] if p["pos"] == "FWD"][:5]
path = urllib.parse.quote(lg["league"]) # encodes the space in the name
requests.post(f"{BASE}/api/draft/{path}/queue",
params={"key": KEY}, json={"players": fwds})
print(lg["league"], "queued", len(fwds))