Hi, I'm Brett

Marketing guru by day, FOSS dev by night.

Reverse Engineering the Apple Sports API


Apple Sports is fast. Like, annoyingly fast. If you’ve ever had your phone buzz a score update before the broadcast showed it, you know what I mean. That kind of latency implies something purpose-built, so I pointed mitmproxy at it.

Here’s what I found.


The Setup

Standard intercept setup: route iPhone traffic through mitmproxy on your machine, install the certificate, trust it under Settings > General > About > Certificate Trust Settings, and watch the HTTPS traffic roll in.

The app makes a lot of requests at startup, and they hit different hosts for different reasons. That split is the most interesting part of the whole thing.


Two Hosts, One Job Each

Almost everything routes to one of two places:

  • api.sports.apple.com: live, locale-aware data. Active scores, event details, live queries.
  • api-sports.cdn-apple.com: cached, static data. Game documents, league data, differential update payloads.

This is a classic edge-caching pattern. Pre-baked documents get pushed to CDN nodes globally; truly live data stays on origin. The CDN host was regularly responding in under 30ms. The origin host sat closer to 100-200ms. Still fast, but the gap matters when you’re polling every 5-10 seconds during a game.

A third host, register.sports.apple.com, only appears once at launch for push registration. Then you never see it again.


App Bootstrap: The Manifest

First real call out of the gate:

GET https://api.sports.apple.com/v3/en-us/manifest/3.0.0

This is the app’s map. CDN base URLs, image server addresses, available leagues, teams, versioning metadata. Everything the app needs to know before it starts doing real work.

The version segment is a floating pointer. 3.0.0 always resolves to the latest 3.0.x manifest. If you’re building something on top of this and don’t want it to break on Apple’s next manifest push, pin to a specific version like 3.0.73.

Swapping the locale (en-us, fr-fr, pt-br, etc.) gives you localized league priorities and team names. The app is live in 80 countries now, and each locale returns meaningfully different content.


Identifiers: UMC vs CDN

Two types of IDs show up everywhere and it’s worth knowing the difference.

UMC identifiers are Apple’s internal Universal Media Catalog format, the same system used across Apple’s other media platforms:

umc.csl.1fved7t8i1upckle2tprgkg3   # league
umc.cse.3lfvdkf1i6znda30fovansg07   # event/game

csl is a league entity, cse is a specific game. When registering for push on a specific game, they combine with a colon:

umc.csl.{league}:umc.cse.{event}

CDN identifiers are flat 32-character hex hashes:

a6411fd7708171d1e919b913989bfdee

These get generated server-side and returned inside manifest and live query responses. They’re how the CDN keys pre-baked game documents. No locale, no query params, just the hash. Fetch it directly from api-sports.cdn-apple.com and you get the full document back from the nearest edge node.

This is the secret to the speed. The server pre-computes rich JSON documents for every active game, stores them by hash, and CDN serves them globally.


The Polling Loop

Once a game is live, the app enters a tight polling loop against:

GET https://api-sports.cdn-apple.com/v3/query/updates/SPORTS

This returns a list of document IDs that have changed since the last check. The app then fetches only the changed documents, not the full game state every time. Smart.

For finer-grained updates, there’s a differential variant:

GET https://api-sports.cdn-apple.com/v3/query/updates/{CDN-ID}?timestamp={epoch}

Pass the Unix timestamp in milliseconds of your last successful poll and you only get back changes since that moment. During live games, this fires approximately every 5-10 seconds. Between games, it relaxes to 30-60 seconds.


Registration

Push registration hits register.sports.apple.com once at launch:

POST https://register.sports.apple.com/v3/register
Content-Type: application/json

Two modes: register for everything ("type": "ALL"), or target a specific game ("type": "EVENT" with a canonicalIds array). The PLAY_LEVEL parameter in the event payload controls granularity of the push updates.

Push and polling coexist. Push handles high-priority interrupts (game starting, overtime score), polling keeps the live ticker in sync. Neither relies on the other exclusively.


Images

Logos and imagery come from Apple’s shared mzstatic.com CDN, the same infrastructure behind the App Store and Apple Music:

https://is1-ssl.mzstatic.com/image/thumb/{token}/{width}x{height}.png

Size is specified in the URL itself and the CDN resizes on the fly. Use 512x0 instead of 512x512 to scale by width while preserving aspect ratio. Source images appear to be stored at max resolution and downscaled on request. Logo tokens come back in manifest and event responses.


Takeaways

Apple built this API to minimize time between a real-world event and a pixel changing on your screen. The CDN-first design, pre-baked documents, and differential polling all point to a team that took latency seriously from day one.

The locale system is also first-class, not bolted on. Regional content priorities differ meaningfully by locale, which makes sense for an app now running in 80 countries with very different sport preferences.

Worth noting: this is undocumented and unsupported. Apple can change endpoints, rotate tokens, or tighten auth at any time. Use it to build tools, not to scrape at scale.


Standard disclaimer: all of this was done on my own device using standard network interception. No auth was bypassed, nothing was scraped at scale.