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://www.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/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://www.ligazo.app/demo/api/draft/ESP-La%20Liga?key=$KEY
https://www.ligazo.app/demo/api/draft/ENG-Premier%20League?key=$KEY
https://www.ligazo.app/demo/api/draft/FRA-Ligue%201?key=$KEY
https://www.ligazo.app/demo/api/draft/ITA-Serie%20A?key=$KEY
https://www.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.export KEY=your-key and the
examples below work as-is.
curl "https://www.ligazo.app/demo/api/draft?key=$KEY"
curl "https://www.ligazo.app/demo/api/draft/ESP-La%20Liga/status?key=$KEY"
curl "https://www.ligazo.app/demo/api/rules?key=$KEY"
curl "https://www.ligazo.app/demo/api/standings?key=$KEY" curl "https://www.ligazo.app/demo/api/fixtures/ESP-La%20Liga?key=$KEY"
curl "https://www.ligazo.app/demo/api/lineup/ESP-La%20Liga?key=$KEY"
curl -X POST "https://www.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://www.ligazo.app/demo/api/lineup/ESP-La%20Liga/captain?key=$KEY" \
-H 'Content-Type: application/json' -d '{"player":"id1"}'
curl "https://www.ligazo.app/demo/api/wire/ESP-La%20Liga?key=$KEY"
curl -X POST "https://www.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://www.ligazo.app/demo/api/wire/ESP-La%20Liga/drop?key=$KEY" \
-H 'Content-Type: application/json' -d '{"drop":"oldguy"}'
curl -X POST "https://www.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://www.ligazo.app/demo/api/draft/ESP-La%20Liga?key=$KEY"
curl "https://www.ligazo.app/demo/api/draft/ESP-La%20Liga?key=$KEY"
curl -X POST "https://www.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://www.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))