GuidesESPN

ESPN API from JavaScript

ESPN's hidden API is free JSON with no key that browsers can call directly: in all 10 leagues we tested, scores, news, standings and schedules on site.api.espn.com sent Access-Control-Allow-Origin: *. Don't put a CORS proxy in front of it: of the 12,140 requests to site.api.espn.com through cors.dev in the 14 days to October 18, 2026, 11,237 got a 403 from ESPN and not one returned data. Two endpoints, /teams and /teams/{team}, never sent a CORS header and depth charts only sometimes did; those need your own server.

Call ESPN directly, not through a proxy

ESPN closed its official developer API in 2014. What developers now call the hidden ESPN API is a set of undocumented JSON endpoints, mostly under https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}. They take no key, no signup and no auth header, and most of them answer every origin with Access-Control-Allow-Origin: *, so a plain fetch() works from any page, including localhost and GitHub Pages.

We sent each request with four different Origin headers, including http://localhost:5173 and a github.io domain, and got the header every time.

Why a CORS proxy gets 403 Access Denied

ESPN's CDN refuses requests from cors.dev's servers on Cloudflare. In our tests, every request through https://proxy.cors.dev/ to site.api.espn.com came back as an Akamai error page instead of JSON:

What ESPN returns to a request through cors.dev
GET https://proxy.cors.dev/https://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard

403
content-type: text/html
x-cors-source: upstream

<HTML><HEAD>
<TITLE>Access Denied</TITLE>
</HEAD><BODY>
<H1>Access Denied</H1>

You don't have permission to access "http&#58;&#47;&#47;site&#46;api&#46;espn&#46;com&#47;apis&#47;site&#47;v2&#47;sports&#47;football&#47;nfl&#47;scoreboard" on this server.<P>
Reference&#32;&#35;18&#46;8ccf3617&#46;1792365497&#46;19b29ac9
<P>https&#58;&#47;&#47;errors&#46;edgesuite&#46;net&#47;18&#46;8ccf3617&#46;1792365497&#46;19b29ac9</P>
</BODY>
</HTML>

X-Cors-Source: upstream means the 403 came from ESPN, not from the proxy. site.web.api.espn.com and sports.core.api.espn.com answered with a shorter 403 Forbidden page. In our logs from October 4 to 18, 2026, site.api.espn.com answered every proxied request with 403, while sports.core.api.espn.com still worked through cors.dev on October 10 and 11 and returned 403 by October 18.

If your page loads cors.dev Auto, its default native-first mode keeps ESPN calls direct: Auto sends each request natively first and tries the proxy only when the browser blocks it. See Auto routing modes.

Live scores from the scoreboard

/scoreboard returns the current games for a league with teams, scores, status, leaders and broadcasts. On October 18, 2026 the NFL scoreboard held 14 games in 224 KB. Add ?dates=20261018 for one day or ?seasontype=2&week=7 for one NFL week.

Live scores from the ESPN scoreboard, no proxy
// Markup: <ul id="scores"></ul>
// Other leagues: basketball/nba, baseball/mlb, hockey/nhl, soccer/eng.1
const league = 'football/nfl';

async function espn(path) {
  const response = await fetch(`https://site.api.espn.com/apis/site/v2/sports/${league}${path}`);
  if (!response.ok) {
    throw new Error(`ESPN request failed: ${response.status}`);
  }
  return response.json();
}

function describe(event) {
  const { competitors } = event.competitions[0];
  const away = competitors.find((side) => side.homeAway === 'away');
  const home = competitors.find((side) => side.homeAway === 'home');
  const { state, shortDetail } = event.status.type;
  if (state === 'pre') {
    return `${away.team.abbreviation} at ${home.team.abbreviation}, ${shortDetail}`;
  }
  return `${away.team.abbreviation} ${away.score}, ${home.team.abbreviation} ${home.score} (${shortDetail})`;
}

const list = document.querySelector('#scores');
espn('/scoreboard')
  .then(({ events }) => {
    if (events.length === 0) {
      list.textContent = 'No games scheduled';
      return;
    }
    list.replaceChildren(
      ...events.map((event) => {
        const item = document.createElement('li');
        item.textContent = describe(event);
        return item;
      }),
    );
  })
  .catch((error) => {
    list.textContent = error.message;
  });

status.type.state is pre, in or post, and shortDetail is a ready-made label such as Final, FT, 3:52 - 4th or a start time in US Eastern time. Scores are strings on the scoreboard. For one game in depth, /summary?event=401873004 adds the box score, drives, scoring plays, odds and win probability; the event id is events[].id on the scoreboard.

A team's schedule and results

/teams/{team}/schedule returns one team's season with dates, opponents and final scores. Abbreviations work in place of ids: teams/kc/schedule and teams/12/schedule both return the same 277 KB for the Kansas City Chiefs.

A team's schedule and results
// Uses espn() from the example above, in a module script or the DevTools console
// Ids and abbreviations both work: teams/12 and teams/kc are the Kansas City Chiefs
const { team, events } = await espn('/teams/kc/schedule');

console.log(team.displayName, team.recordSummary, team.standingSummary);
console.table(
  events.map((event) => {
    const competition = event.competitions[0];
    const us = competition.competitors.find((side) => side.team.id === team.id);
    const them = competition.competitors.find((side) => side.team.id !== team.id);
    const { completed, shortDetail } = competition.status.type;
    const outcome = us.winner ? 'W' : them.winner ? 'L' : 'T';
    return {
      week: event.week?.text,
      date: new Date(event.date).toLocaleDateString(),
      opponent: (us.homeAway === 'home' ? 'vs ' : 'at ') + them.team.abbreviation,
      result: completed ? `${outcome} ${us.score.displayValue}-${them.score.displayValue}` : shortDetail,
    };
  }),
);

Unlike the scoreboard, scores here are objects with value and displayValue, and games in progress or not yet played have no score. Rosters work the same way: /teams/kc/roster lists players in groups such as offense, defense, special teams and injured reserve.

ESPN API endpoints and response sizes

Paths are on https://site.api.espn.com/apis/site/v2/sports/ unless a host is given. Event 401873004 is the Cardinals at Rams game of October 18, 2026, and athlete 4373626 is Tyler Allgeier. Every endpoint in the table returned 403 through cors.dev.

ESPN API endpoints called from the browser, checked October 2026
PathReturnsSizeBrowser
football/nfl/scoreboardThis week's games: teams, scores, status, leaders224 KB, 14 gamesWorks directly
football/nfl/scoreboard?dates=20261018Games on one date195 KB, 12 gamesWorks directly
football/nfl/summary?event=401873004One game: box score, drives, scoring plays, win probability570 KBWorks directly
football/nfl/newsLatest headlines23 KBWorks directly
football/nfl/teams/kc/scheduleOne team's season with results277 KB, 17 gamesWorks directly
football/nfl/teams/kc/rosterPlayers by unit, plus injured reserve and practice squad354 KBWorks directly
football/nfl/injuriesInjury report for every team8.7 MBWorks directly
football/college-football/rankingsAP Top 25 and coaches poll289 KBWorks directly
site.api.espn.com/apis/v2/sports/football/nfl/standingsAFC and NFC standings158 KBWorks directly
site.web.api.espn.com/apis/common/v3/sports/football/nfl/athletes/4373626One player's profile and stats83 KBWorks directly
sports.core.api.espn.com/v2/sports/football/leagues/nfl/teams/12Team record with links to related data10 KBWorks directly
football/nfl/teamsAll 32 teams with ids, abbreviations and logos149 KBBlocked: no CORS header
football/nfl/teams/kcOne team: record, logos, next game23 KBBlocked: no CORS header
football/nfl/teams/kc/depthchartsDepth chart by position216 KBBlocked: no CORS header

The split held across all 10 leagues: scoreboards, news, schedules, rosters and standings always sent the header, and /teams and /teams/{team} never did.

League paths and scoreboard sizes on October 18, 2026
LeaguePathScoreboard
NFLfootball/nfl224 KB, 14 games
NBAbasketball/nba41 KB, 3 games
WNBAbasketball/wnba25 KB, 1 game
MLBbaseball/mlb19 KB, 1 game
NHLhockey/nhl42 KB, 2 games
College footballfootball/college-football262 KB, 16 games
Men's college basketballbasketball/mens-college-basketball15 KB, 2 games
Premier Leaguesoccer/eng.146 KB, 4 games
MLSsoccer/usa.114 KB, 1 game
Champions Leaguesoccer/uefa.champions113 KB, 9 games

Endpoints without CORS headers need a server

/teams and /teams/{team} send no Access-Control-Allow-Origin header. Depth charts are unreliable: the NFL's never sent it in our checks, while NBA and MLB depth charts sent it on some requests and not on others. The browser blocks any response without the header, and a CORS proxy does not help, because ESPN refuses the proxy too. Fetch these from your own backend and pass the JSON to your page. cors.dev runs on Cloudflare Workers and gets the 403, so a Cloudflare Worker may too; from our test server outside Cloudflare, all three answered 200.

Team ids, names, abbreviations and logos also come inside the scoreboard, schedule and roster responses, which do send the header, so many apps never need /teams.

MLB Stats API and the NHL

The MLB Stats API at statsapi.mlb.com also sends Access-Control-Allow-Origin: *, so call it directly too. /api/v1/schedule?sportId=1 lists today's games, /api/v1/standings?leagueId=103,104 returns both leagues in 81 KB and /api/v1/teams/147/roster is the New York Yankees roster in 10 KB. Responses carry a copyright notice from MLB Advanced Media that points to its terms of use.

The NHL's own API is different: api-web.nhle.com sends no CORS header, so it does need a proxy; see NHL API from JavaScript. ESPN's hockey/nhl endpoints send the header and work directly.

Limits and polling

  • No documentation: ESPN publishes no docs or rate limits for these endpoints, and nothing guarantees they stay the same. Check response.ok and show a fallback when a request fails.
  • Polling: the scoreboard is cached for 1 to 8 seconds (Cache-Control max-age), so polling every 10 to 30 seconds is enough for live games.
  • Size: a scoreboard is 14 KB to 262 KB, but the NFL injury report is 8.7 MB and the men's college basketball standings 6.2 MB, so request the narrowest endpoint you need.

Good questions.

Is there an official ESPN API?

Not anymore. ESPN stopped issuing public API keys in August 2014 and switched off the existing keys in December 2014. The hidden ESPN API is the set of undocumented JSON endpoints on site.api.espn.com, site.web.api.espn.com and sports.core.api.espn.com, which work without a key.

Does the ESPN API need an API key?

No. None of the endpoints on this page take a key, a token or a login. A plain GET from the browser returns JSON.

Does the ESPN API support CORS?

Mostly. Scoreboard, summary, news, standings, rankings, roster, schedule, injuries and athlete endpoints send Access-Control-Allow-Origin: *. The /teams list and the single-team endpoint send no CORS header, and depth charts send it only some of the time, so read those from a server.

Why does the ESPN API return 403 Access Denied?

ESPN's CDN answers requests from cors.dev, which runs on Cloudflare, with a 403 page instead of JSON. Call ESPN directly from the browser, where the same request returns 200 with CORS headers.

What is the ESPN API endpoint for NBA scores?

https://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard. Swap basketball/nba for football/nfl, baseball/mlb, hockey/nhl or soccer/eng.1 for other leagues. It sends CORS headers, so fetch it directly.

How do I get ESPN team ids?

Scoreboard, schedule and roster responses include team ids, and abbreviations such as kc or bos work in their place in team URLs. The /teams list has the full set but sends no CORS header, so read it from a server.

Is the MLB Stats API free to call from the browser?

It needs no key, and statsapi.mlb.com sends Access-Control-Allow-Origin: *, so fetch it directly without a proxy. Its responses include a copyright notice pointing to MLB's terms for using the data.