# How to Get Live Sports Scores for Free Using ESPN's Hidden API

If you've ever tried to build something that needs sports data — a score bot, a fantasy helper, a dashboard — you've probably run into the same wall. The official sports data APIs are expensive. SportsRadar starts at $9/month for hobbyists but quickly gets painful at scale. SportDataAPI, MySportsFeeds, same story. You're paying just to get data that ESPN is already serving to millions of people for free on their website.

Here's the thing: ESPN has an undocumented public API that's been sitting there for years. No authentication. No rate limit warnings. Clean JSON. Most developers don't know it exists because ESPN has never publicized it.

This post covers how it works, what you can pull from it, and how I wrapped it into a reusable tool so you don't have to deal with the sharp edges yourself.

* * *

## The ESPN API Endpoints

The base URL pattern looks like this:

```plaintext
https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard
```

So for NBA games today:

```plaintext
https://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard
```

For NFL:

```plaintext
https://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard
```

For MLB:

```plaintext
https://site.api.espn.com/apis/site/v2/sports/baseball/mlb/scoreboard
```

Hit any of those in your browser right now and you'll get a full JSON response with today's games — scores, game status, teams, venues, broadcast info.

### The sport/league mapping

| Sport | League | Endpoint segment |
| --- | --- | --- |
| Basketball | NBA | `basketball/nba` |
| Basketball | WNBA | `basketball/wnba` |
| Basketball | NCAA Men's | `basketball/mens-college-basketball` |
| Football | NFL | `football/nfl` |
| Football | NCAA | `football/college-football` |
| Baseball | MLB | `baseball/mlb` |
| Hockey | NHL | `hockey/nhl` |
| Soccer | MLS | `soccer/usa.1` |
| Soccer | EPL | `soccer/eng.1` |
| Soccer | La Liga | `soccer/esp.1` |
| Soccer | Champions League | `soccer/uefa.champions` |

### Fetching a specific date

By default the scoreboard endpoint returns today's games. To pull a different date, add a `dates` query parameter in `YYYYMMDD` format:

```plaintext
https://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard?dates=20260401
```

For a date range, pass both dates separated by a dash... except it doesn't actually support ranges natively. You have to call it once per date and stitch the results together yourself. More on that in a minute.

### What the response looks like

The raw response is nested and verbose. A single game object inside `events[]` looks roughly like this (simplified):

```json
{
  "id": "401584721",
  "date": "2026-01-23T00:00Z",
  "name": "Cleveland Cavaliers at Charlotte Hornets",
  "competitions": [
    {
      "status": {
        "clock": 0,
        "period": 0,
        "type": {
          "state": "pre",
          "description": "Scheduled",
          "detail": "7:00 PM ET"
        }
      },
      "competitors": [
        {
          "homeAway": "home",
          "team": {
            "id": "30",
            "displayName": "Charlotte Hornets",
            "abbreviation": "CHA"
          },
          "score": "0"
        },
        {
          "homeAway": "away",
          "team": {
            "id": "5",
            "displayName": "Cleveland Cavaliers",
            "abbreviation": "CLE"
          },
          "score": "0"
        }
      ],
      "venue": {
        "fullName": "Spectrum Center",
        "address": { "city": "Charlotte", "state": "NC" }
      },
      "broadcasts": [
        { "names": ["NBA TV"] }
      ]
    }
  ]
}
```

You have to dig through `competitions[0].competitors[]` to get home/away teams, navigate `competitions[0].status.type.state` to get game status, flatten broadcasts out of a nested array. It's not unusable but it's not clean either.

* * *

## Where It Gets Annoying

The API itself is solid, but using it directly at any real scale has a few friction points.

**No date ranges.** You want last week's NFL results? That's 7 separate HTTP calls, one per day, that you have to stitch together manually.

**Multi-league runs.** Want NBA + NHL + MLB in one shot? Three separate API calls, three different response shapes to normalize.

**Timezone handling.** Dates come back in UTC. If you're building something for US users you're converting everything yourself.

**The nested response shape.** Every time you use this you're writing the same flattening logic: dig into `competitions[0]`, separate home from away via `homeAway` field, pull status from `type.state`, flatten broadcasts. It's 20-30 lines of boilerplate every time.

None of this is hard but it's tedious, and it's the kind of thing that breaks your flow when you're trying to build the actual thing you care about.

* * *

## What I Built

I got tired of rewriting this logic across different projects, so I packaged it as an [Apify actor](https://apify.com/hgservices/apify-actor-espn?fpr=wfhpok).

You give it a list of leagues and a date range, it handles the multi-league calls, normalizes everything into a flat consistent schema, converts timezones, and outputs clean JSON you can actually use.

Input looks like this:

```json
{
  "leagues": ["nba", "nhl", "mlb"],
  "dateMode": "today",
  "timezone": "America/New_York"
}
```

And each game comes out flat:

```json
{
  "gameId": "401584721",
  "sport": "basketball",
  "league": "nba",
  "date": "2026-01-23T00:00:00Z",
  "dateLocal": "2026-01-22T19:00:00-05:00",
  "name": "Cleveland Cavaliers at Charlotte Hornets",
  "shortName": "CLE @ CHA",
  "status": {
    "state": "pre",
    "description": "Scheduled",
    "detail": "7:00 PM ET"
  },
  "homeTeam": {
    "name": "Charlotte Hornets",
    "abbreviation": "CHA",
    "score": null
  },
  "awayTeam": {
    "name": "Cleveland Cavaliers",
    "abbreviation": "CLE",
    "score": null
  },
  "venue": {
    "name": "Spectrum Center",
    "city": "Charlotte",
    "state": "NC"
  },
  "broadcasts": ["NBA TV"]
}
```

No nested `competitions[0]`, no manual timezone math, no stitching date loops.

* * *

## Using the Output

Once you have clean JSON coming out, plugging it into things is straightforward.

### Pull today's scores with Python

```python
import requests

run_url = "https://api.apify.com/v2/acts/hgservices~apify-actor-espn/run-sync-get-dataset-items"

params = {
    "token": "YOUR_APIFY_TOKEN"
}

payload = {
    "leagues": ["nba", "nfl"],
    "dateMode": "today",
    "timezone": "America/New_York"
}

response = requests.post(run_url, json=payload, params=params)
games = response.json()

for game in games:
    status = game["status"]["detail"]
    print(f"{game['shortName']} — {status}")
```

### Post live scores to a Discord webhook

```python
import requests

DISCORD_WEBHOOK = "https://discord.com/api/webhooks/YOUR_WEBHOOK"

def post_scores(games):
    lines = []
    for game in games:
        if game["status"]["state"] == "in":
            home = game["homeTeam"]
            away = game["awayTeam"]
            lines.append(
                f"**{away['abbreviation']} {away['score']} @ {home['abbreviation']} {home['score']}** — {game['status']['detail']}"
            )

    if lines:
        requests.post(DISCORD_WEBHOOK, json={"content": "\n".join(lines)})

# Call this on a schedule
```

### Filter by team across a date range

```json
{
  "leagues": ["nba"],
  "dateMode": "range",
  "startDate": "20260401",
  "endDate": "20260430",
  "teams": ["LAL"],
  "timezone": "America/Los_Angeles"
}
```

Returns every Lakers game in April — useful for building team-specific trackers or automating schedule reminders.

* * *

## Scheduling It

On Apify you can set cron schedules directly on the actor. A few useful patterns:

| Use case | Cron |
| --- | --- |
| Morning game preview | `0 8 * * *` |
| Live score updates | `*/5 * * * *` |
| Nightly results digest | `0 2 * * *` |
| Weekly schedule pull | `0 8 * * 1` |

The live score polling every 5 minutes is the most common pattern for Discord/Slack bots. Costs almost nothing — you're fetching maybe 10-20 game results per run at $0.001 each.

* * *

## Wrapping Up

ESPN's public API is genuinely good. The data is reliable, it covers all the major US leagues plus EPL and Champions League, and there's no auth to deal with. The main friction is the normalization work every time you use it — which is what the actor handles.

If you're just exploring the API directly, the endpoints above are enough to get started. If you want clean output across multiple leagues without writing the plumbing yourself, the [Apify actor](https://apify.com/hgservices/apify-actor-espn?fpr=wfhpok) is available at $0.001 per result — running today's full NBA schedule costs you about 2 cents.

Happy to answer questions in the comments about the ESPN API structure or how the actor handles edge cases like postponed games and timezone conversion.
