A Cloudflare Worker that fetches syllable-level TTML lyrics and editorial artwork for any Apple Music song — proxied through your own edge endpoint.
Given an Apple Music song ID, the worker returns:
ttml— The full TTML (Timed Text Markup Language) lyrics string, including syllable-level timing when availableartwork— Editorial video URLs and static preview frames in square (1:1) and tall (3:4) formats at multiple resolutions
Both the lyrics and metadata are fetched in parallel from Apple's internal API, so the response is fast.
am-lyrics-worker/
├── src/
│ └── index.js # Worker source code
├── wrangler.toml # Cloudflare Workers config
├── package.json
├── .gitignore
└── README.md
- Node.js v18 or later
- A Cloudflare account (free tier works)
- Wrangler CLI (installed automatically via
npm install)
npm installnpx wrangler loginSecrets are stored encrypted in Cloudflare — never committed to source control.
# Your API key (callers must send this to use the worker)
npx wrangler secret put API_KEY
# Apple Music bearer token (JWT from music.apple.com)
npx wrangler secret put AM_BEARER_TOKEN
# Apple Music media-user-token (from your Apple Music session cookie)
npx wrangler secret put AM_MEDIA_USER_TOKENAll three secrets are required. The worker returns a
500error if any are missing — there are no hardcoded fallbacks.
The default storefront is de (Germany). To change it, edit wrangler.toml:
[vars]
AM_STOREFRONT = "us" # or "gb", "fr", etc.npm run deployWrangler will print your worker URL, e.g.:
https://am-lyrics-worker.<your-subdomain>.workers.dev
npm run devThe worker runs locally at http://localhost:8787. You can test it right away without deploying.
To set secrets locally, create a .dev.vars file (never commit this):
API_KEY=I<3AMLyrics
AM_BEARER_TOKEN=eyJ...
AM_MEDIA_USER_TOKEN=0.Ai...
GET /?id=<APPLE_MUSIC_SONG_ID>
Pass your API key in one of two ways:
| Method | Example |
|---|---|
| Header | x-api-key: I<3AMLyrics |
| Query param | ?id=1871520999&api_key=I<3AMLyrics |
curl "https://am-lyrics-worker.<your-subdomain>.workers.dev/?id=1440857781" \
-H "x-api-key: I<3AMLyrics"{
"id": "1440857781",
"ttml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<tt xml:lang=\"de\">...</tt>",
"artwork": {
"animated": {
"square": "https://video-ssl.itunes.apple.com/...",
"tall": "https://video-ssl.itunes.apple.com/..."
},
"static": {
"square": {
"450x450": "https://is1-ssl.mzstatic.com/.../450x450.webp",
"1080x1080": "https://is1-ssl.mzstatic.com/.../1080x1080.webp"
},
"tall": {
"450x600": "https://is1-ssl.mzstatic.com/.../450x600.webp",
"1080x1440": "https://is1-ssl.mzstatic.com/.../1080x1440.webp"
}
}
}
}If a song has no lyrics or no editorial video, the corresponding fields will be
nullrather than omitted.
The song ID is the number at the end of any Apple Music share URL:
https://music.apple.com/de/album/gut-genug-mit-blumengarten-shirin-david/1871520597?i=1871520999
^^^^^^^^^^
| Status | Meaning |
|---|---|
400 |
Missing id query parameter |
401 |
Invalid or missing API key |
500 |
Upstream Apple Music API error |
Apple Music tokens expire periodically. When the worker starts returning empty ttml values or errors, update the secrets:
npx wrangler secret put AM_BEARER_TOKEN
npx wrangler secret put AM_MEDIA_USER_TOKENNo redeployment needed — secrets are picked up immediately.
Special thanks to Claude by Anthropic for assisting with the development, code optimization, and documentation of this project.