Demo You're touring a finished example league — everyone here is made up. Join for real →
API ACCESS
This is the DEMO instance. Keys minted here start with demo_ and only work under /demo. For your real league, use the API page at ligazo.app (no /demo). Every API response also carries "instance": "demo".
DEVELOPER API

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.

The whole reference below as Markdown — endpoints, response fields, examples. Paste it into a script, a doc, or an AI assistant. (Your key isn't embedded — set it once with the export line.)
YOUR KEY

The demo is read-only — sign in to your own league to mint a key.

ENDPOINTS

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.

GET /api/draft — every league at once (array under leagues).
GET /api/draft/<league> — one league.
GET /api/draft/<league>/status — cheap poll: picks_made, on_the_clock, your_next_pick. Poll this; fetch the big one on change.
GET /api/draft/<league>/queue — your current pick queue, in order.
POST /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.
GET /api/lineup/<league> — your six, plus the whole roster with each player's state, lock and armband eligibility. Add ?week=N.
POST /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.
POST /api/lineup/<league>/captain — set the armband. Body {"player": "<id>"}.
GET /api/wire/<league> — your pending bids, credits, droppable roster, and how the wire behaves right now (settles_week, adds_left).
POST /api/wire/<league> — sealed FAAB bid. Body {"add": "<id>", "drop": "<id>"|null, "bid": <credits>}.
POST /api/wire/<league>/drop — a pure drop, applied immediately (not a bid). Body {"drop": "<id>"}.
POST /api/wire/<league>/cancel/<intent_id> — cancel one of your pending bids. The id comes from pending[].intent_id.
GET /api/rules — the scoring matrix, roster/lineup/armband structure and the wire economy, straight from the engine so it never drifts.
GET /api/standings (or /<league>) — the table: cumulative points per manager, overall and per league, ranked (ties share a rank).
GET /api/fixtures (or /<league>) — real-match Score Center: each game's clubs, score, status (sched/live/ht/ft), minute, kickoff.
A refused write is a 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).
Rate limit: 60 reads and 20 writes per minute per key (bursts of 20 / 8). Over that: a 429 with a Retry-After header — pause and retry.
Conditional requests: every GET returns an 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.
Lazy clock: a read runs the draft clock, so polling advances expired and Auto-Draft turns. Reads are cheap but not side-effect-free; the draft also ticks on a cron, so it moves even if nobody polls.
LEAGUES

<league> is the exact, URL-encoded league name. All five, in draft order (append /queue or /status to any of these):

ESP-La Liga complete
https://www.ligazo.app/demo/api/draft/ESP-La%20Liga?key=$KEY
ENG-Premier League complete
https://www.ligazo.app/demo/api/draft/ENG-Premier%20League?key=$KEY
FRA-Ligue 1 complete
https://www.ligazo.app/demo/api/draft/FRA-Ligue%201?key=$KEY
ITA-Serie A complete
https://www.ligazo.app/demo/api/draft/ITA-Serie%20A?key=$KEY
GER-Bundesliga complete
https://www.ligazo.app/demo/api/draft/GER-Bundesliga?key=$KEY
RESPONSE FIELDS

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)"
Queue endpoint → { 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.
MANAGING YOUR TEAM

Three things run your season: the six you field, the armband, and the wire. Two rules govern all of them.

1 — One roster, locked by kickoff. A player locks at his club's first kickoff of the week and frees at the Monday reset. Locked means immovable in both directions: he can't leave your six, and he can't enter it. Nothing you do afterwards touches points he has already banked.
2 — The roster you get is the roster that scores. Every response is the squad as of that week — the same snapshot the scorer reads. Somebody who only joins you in a later week is not in 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"}
Both apply immediately — there is no submit step and no deadline beyond each player's own kickoff. Both return the same object as the GET, so one call gives you the write and the fresh state. Benching your captain clears the armband; dropping him does too.

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"
Both sides of a swap must be unlocked in the week it settles into. If either has played, the claim isn't refused — it is held: it stays pending and is retried at every clear until it's legal, which is Monday's run at the latest (that fires before any fixture, so nothing is locked then). Nothing waits longer than a week, and you can cancel a held bid at any time. Two things still end a held claim outright: somebody else signs the player, or you're outbid for him — a held bid is not a reservation.
The cap is on adds, not drops. In the opening window (weeks 1-3) adds are uncapped. From week 4 it's one successful add per Ligazo week — check 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.
Club-GK slots come back with the raw 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.
EXAMPLES
Generate a key above, then export KEY=your-key and the examples below work as-is.
Read every league
curl "https://www.ligazo.app/demo/api/draft?key=$KEY"
Cheap poll — has anything changed?
curl "https://www.ligazo.app/demo/api/draft/ESP-La%20Liga/status?key=$KEY"
Read the scoring rules
curl "https://www.ligazo.app/demo/api/rules?key=$KEY"
The table + real fixtures
curl "https://www.ligazo.app/demo/api/standings?key=$KEY"
curl "https://www.ligazo.app/demo/api/fixtures/ESP-La%20Liga?key=$KEY"
Your six, the armband, and what you can still move
curl "https://www.ligazo.app/demo/api/lineup/ESP-La%20Liga?key=$KEY"
Set the six, then the armband (both apply immediately)
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"}'
Work the wire — bid, pure-drop, cancel
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"
Conditional GET — 304 when nothing changed
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"
See who's left in ESP-La Liga
curl "https://www.ligazo.app/demo/api/draft/ESP-La%20Liga?key=$KEY"
Set your ESP-La Liga queue (top 3, in order)
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"]}'
Python: queue best-available forwards in every pending league
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))