# LOSGL API — guide for agents (League of Sharpe Gets Last, ESPN fantasy football league 466207, since 2015) Base URL: https://losgl.thebatcave.cloud/api/v1/ Human docs: https://losgl.thebatcave.cloud/api/ OpenAPI: https://losgl.thebatcave.cloud/api/openapi.json Everything is JSON. Read endpoints are static files (no auth, CORS *, cache 5 min). The ONLY write endpoints are under /api/v1/submit/ and need `Authorization: Bearer ` (ask the commissioner for a token; it names you as a source). Nothing else accepts writes. Never send the token anywhere but this host. ## Freshness GET published.json -> {"published_at": ISO8601, "published_human": str, "content_hash": str} Poll this (no-store). If content_hash changed, re-fetch what you need. Data republishes within ~15 minutes of any change (ESPN sync, admin edit, or your submission). ## Vocabulary - season_team_id: stable integer id of one team in one season. Use it in submissions. Find them in seasons/{year}.json -> standings[].season_team_id, or GET submit/teams/{year} with your token. - manager_slug / franchise_slug: stable string ids for people / team lineages. - matchup status: final (counted everywhere) | provisional (decided, stat corrections may still land; counted in standings, not records) | live (in progress, never counted) | scheduled | missing (ESPN has no score; never counted) | bye. ALWAYS check status before using points. - tier: NONE (regular season) | WINNERS_BRACKET (playoffs) | *_CONSOLATION_LADDER (excluded from all stats). - NFL team codes: ARI ATL BAL BUF CAR CHI CIN CLE DAL DEN DET GB HOU IND JAX KC LV LAC LAR MIA MIN NE NO NYG NYJ PHI PIT SEA SF TB TEN WSH (WAS/JAC/OAK/SD/STL/LA are accepted on input and normalised). ## Read endpoints GET site.json {name, league_name, host, current_season, last_complete_season, first_season, seasons: [{year, available, complete, champion, champion_manager, last_place, team_count, latest_week, gaps}], sync: {ok, auth_expired, status}, nav, bugs, empty} GET seasons/{year}.json {year, complete, reg_weeks, playoff_team_count, team_count, current_week, latest_week, standings: [{season_team_id, name, manager_name, manager_slug, franchise_slug, wins, losses, ties, pf, pa, ppg, allplay_w, allplay_l, allplay_t, allplay_pct, win_pct, streak, rank, final_rank, playoff_seed, logo, weekly: [{week, pf, pa, result, opp, status, matchup_id}]}], weeks: [{week, label, is_playoff_week, status, tickets: [TICKET]}], playoffs: [{week, games: [TICKET]}], consolation: [TICKET], byes: [{week, name}], draft: {available, rounds: [{round, picks: [{overall, pick, team, manager, manager_slug, player, pos, nfl, keeper}]}]}, draft_report: {teams: [{rank, team, points, best, bust, reach, steal}], late_gems, reaches, steals, coverage, rules} | null, outcomes: {complete, champion, runner_up, last_place, champion_verification, conflict, final_matchup_id}, gaps: [str], rule_change: bool} TICKET = {matchup_id, year, week, tier, is_playoff, is_consolation, status, winner (HOME|AWAY|TIE|UNDECIDED), margin, multiweek, line, home/away: {name, manager, manager_slug, franchise_slug, points, result (W|L|T|null), projected, logo, box: {starters_points, bench_points, top: {player, points}, worst_starter, bench_top, projected} | null}} GET managers.json [{id, name, slug, aliases, years, active, seasons: [...], career: {wins, losses, ties, pf, pa, games, win_pct, ppg, high, low, season_count}, playoffs: {wins, losses, games}, titles: [year], runner_ups, last_places, playoff_appearances, rivals: [{name, slug, games, record: [W,L,T], line}]}] GET franchises.json [{id, name, slug, names, owners: [{manager, manager_slug, start, end}], seasons, career, titles, last_places}] GET h2h.json {manager: {"a-slug|b-slug": H2H}, franchise: {...}} H2H = {record: {all|regular|playoff: [W,L,T] from A's side}, pf_a, pf_b, streak_a, biggest, closest, line, games: [{matchup_id, year, week, tier, is_playoff, status, a_team, b_team, a_points, b_points, result, margin}]} GET records.json {rules, categories: [{key, title, blurb, scope, items: [...]}]} every item links matchup_id/year/week GET rankings/{year}.json {year, reg_weeks, statistical: [{week, formula (v1|v2), weights, provisional, entries: [{rank, season_team_id, name, manager_name, manager_slug, score, components: {ppg, allplay, win_pct, form, roster?}, normalized, delta, prev_rank, games, injured}]}], commissioner: [{week, title, intro, published_at, entries: [{rank, season_team_id, name, manager, manager_slug, comment, delta}]}], external: [{source, editions: [same shape as commissioner]}], <- submissions from tokens, one set per token name formula: {version, weights, text}, roster_context} GET draft/{year}.json the draft_report object above GET notes.json [{slug, title, year, week, published_at, published_human, excerpt}] (full HTML at /notes/{slug}/) GET theory.json {rules_year, method, seasons: [{year, rules_identical, coverage, actual_champion, theory_champion, title_flips, reg_flips, replay: [{matchup_id, week, home, away, home_adj, away_adj, home_points, away_points, actual_winner, theory_winner, flip}], standings: [{season_team_id, name, manager_slug, rank, arank, w, l, t, aw, al, at, ppg, appg, delta}]}]} aPPG also appears on seasons/{year}.json standings[] and managers.json career.appg (re-scored under the current rules). GET scoring.json {history: [{year, vs, cosmetic, summary, items: [{stat, id, kind (added|removed|changed), from, to, inferred}]}], tables: {"": [{stat, points}]}} GET sleepers.json {rule, board: [{manager, manager_slug, sleepers, firsts, points (value over replacement), hits: [{year, player, pos, points, value, rank, how, live}]}], seasons: {"": {through_week, replacement: {QB..DST}, top: [{player, pos, points, value, replacement, drafted, overall_pick, round, found_by: {season_team_id, team, manager, manager_slug, how}, weeks_rostered, starts}]}}} Sleeper = drafted pick 133+ (round 12+) or undrafted, on a league roster; ranked by points over positional replacement. GET transactions/{year}.json {year, budget, count, faab: [{season_team_id, name, manager_slug, remaining, spent, acquisitions, trades}], weeks: [{week, top_bid, waivers_won: [TX], waivers_failed: [TX + reason], free_agents: [TX], trades: [{outcome (executed|vetoed|declined|withdrawn), parties: [team, team], veto_votes, veto_required, versions, pieces_inferred, trade_items: [{player, pos, nfl, from_team, to_team}]}]}]} TX = {id, week, type, status, team: {season_team_id, name, manager, manager_slug}, bid, proposed_at, processed_at, adds: [{player, pos, nfl}], drops: [...]} Archive begins 2026-09-14; ESPN does not serve prior seasons' transactions. Pending claims and open trade proposals are never published. GET survivor/{year}.json {year, title, notes, weeks_played, alive_count, total, winner, source, updated_at, participants: [{name, alive, eliminated_week, picks: {"": {team, result (win|loss|pending|push|none)}}}]} GET nfl/schedule/{year}.json {year, source, fetched_at, weeks: [{week, games: [{id, kickoff (ISO8601 UTC), week, home, away, home_name, away_name, status, completed, home_score, away_score, venue, neutral, line: {provider, details ("BUF -4.5"), spread (home-relative, negative = home favored), over_under, favorite} | null}]}]} Lines are whatever ESPN attaches (usually DraftKings) and can be null early in the week. ## Write endpoints (bearer token) GET submit/whoami -> {name, scope, kind} GET submit/teams/{year} -> {year, teams: [{season_team_id, name, espn_team_id}]} POST submit/rankings -> 201 {kind, year, week, entries, public_within, read_back} body: {"year": 2026, "week": 3, "title": "optional", "intro": "optional", "entries": [{"season_team_id": 145, "rank": 1, "comment": "optional, <=1200 chars"}, ...]} rules: one entry per team of that year; ranks are a permutation of 1..N; re-POST same year+week to replace; stored as kind "external:" (never touches commissioner/statistical editions). POST submit/survivor -> 201 {year, participants, alive, weeks_played, updated_at, source, public_within, read_back} body: {"year": 2026, "title": "optional", "notes": "optional", "weeks_played": 3, "participants": [{"name": "Josh", "alive": true, "eliminated_week": null, "picks": {"1": {"team": "KC", "result": "win"}, "2": "BUF"}}]} rules: whole-season snapshot each time (replaces the previous one); names <= 40 chars, unique; team must be an NFL code; result defaults to pending; alive is inferred from losses if omitted; max 200 participants, 25 weeks. Errors: 400 malformed JSON · 401 no/bad token · 403 wrong scope · 413 body > 256 KB · 422 {"error": ""} · 429 > 30 req/min. ## Recipes Google Apps Script (sheet -> survivor), run from a time trigger: function syncSurvivor() { const sh = SpreadsheetApp.getActive().getSheetByName('Picks'); // row 1: Name, Wk1, Wk2, ...; cells like "KC" or "KC W" / "KC L" const rows = sh.getDataRange().getValues(); const header = rows[0]; const participants = rows.slice(1).filter(r => r[0]).map(r => { const picks = {}; let alive = true, elim = null; for (let i = 1; i < header.length; i++) { const v = String(r[i] || '').trim(); if (!v) continue; const [team, res] = v.split(/\s+/); const result = res === 'W' ? 'win' : res === 'L' ? 'loss' : 'pending'; picks[String(i)] = {team, result}; if (result === 'loss' && alive) { alive = false; elim = i; } } return {name: String(r[0]), alive, eliminated_week: elim, picks}; }); UrlFetchApp.fetch('https://losgl.thebatcave.cloud/api/v1/submit/survivor', {method: 'post', contentType: 'application/json', headers: {Authorization: 'Bearer ' + PropertiesService.getScriptProperties().getProperty('LOSGL_TOKEN')}, payload: JSON.stringify({year: 2026, title: 'Survivor 2026', participants})}); } Survivor picker: use nfl/schedule/{year}.json for the week's games, kickoff times and lines; use survivor/{year}.json to know which teams each participant has already burned (picks[*].team) and who is alive; post the new state back with submit/survivor. Deadline = earliest kickoff of the week. Power rankings from your own model: read seasons/{year}.json + rankings/{year}.json (statistical entries have the components), compute, then POST submit/rankings. Your edition shows under Power Rankings as its own tab. ## Privacy / limits No ESPN ids, emails, or real names are published — only display handles and team names. Do not scrape ESPN or FantasyPros through this site; it does not proxy them. Cache reads for >= 5 minutes. Be nice: 30 writes/min. Content hash: 1db45f7e7ee1ba35b15854f852d1a81dc6ea759e7277155ba78a0f6eb179263e