Uh oh!
There was an error while loading. Please reload this page.
docs(apple): GET /api/apple/songs — batch ISRC lookup contract - #298
Conversation
Documents the Apple Music catalog lookup endpoint ahead of the api implementation (recoupable/app#1959). Contract-first: the response shape returns one row per *requested* ISRC so a recording Apple does not carry surfaces as found: false rather than being omitted. Adds the OpenAPI path plus AppleSongsResponse / AppleIsrcResult / AppleSong / AppleSongAlbum / AppleErrorResponse schemas to social.json, the reference page, and the Apple Music nav group. Complements GET /api/spotify/search: Spotify reaches ISRCs only via a fuzzy isrc: search query, while Apple matches the identifier exactly and returns release-level rights metadata (upc, record_label, copyright) that Spotify does not expose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Caution Review failedAn error occurred during the review process. Please try again later. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
1 issue found across 3 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="api-reference/openapi/social.json">
<violation number="1" location="api-reference/openapi/social.json:1244">
P2: The new Apple response schemas (AppleSongsResponse, AppleIsrcResult, AppleSong, AppleSongAlbum, AppleErrorResponse) declare no `required` arrays, even though the endpoint contract states these fields are always present. The 200 description guarantees results is "one entry per requested ISRC, in the order requested", and each result always carries `isrc`, `found`, and `songs` (misses return `found: false` with `songs: []`). Other error-response schemas in this file (e.g. ArtistPostsErrorResponse) mark `status`/`error` required; the new AppleErrorResponse omits this. As a docs-first contract that the implementation will be built against, leaving these unconditionally-returned fields optional means consumers and the implementer cannot rely on them. Mark the guaranteed fields required (keeping genuinely conditional/nullable fields like `missing_fields`, `composer_name`, `track_number`, and the nullable `album`/`upc`/`record_label` optional).</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| @@ -480,6 +480,84 @@ | |||
| } | |||
There was a problem hiding this comment.
P2: The new Apple response schemas (AppleSongsResponse, AppleIsrcResult, AppleSong, AppleSongAlbum, AppleErrorResponse) declare no required arrays, even though the endpoint contract states these fields are always present. The 200 description guarantees results is "one entry per requested ISRC, in the order requested", and each result always carries isrc, found, and songs (misses return found: false with songs: []). Other error-response schemas in this file (e.g. ArtistPostsErrorResponse) mark status/error required; the new AppleErrorResponse omits this. As a docs-first contract that the implementation will be built against, leaving these unconditionally-returned fields optional means consumers and the implementer cannot rely on them. Mark the guaranteed fields required (keeping genuinely conditional/nullable fields like missing_fields, composer_name, track_number, and the nullable album/upc/record_label optional).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api-reference/openapi/social.json, line 1244:
<comment>The new Apple response schemas (AppleSongsResponse, AppleIsrcResult, AppleSong, AppleSongAlbum, AppleErrorResponse) declare no `required` arrays, even though the endpoint contract states these fields are always present. The 200 description guarantees results is "one entry per requested ISRC, in the order requested", and each result always carries `isrc`, `found`, and `songs` (misses return `found: false` with `songs: []`). Other error-response schemas in this file (e.g. ArtistPostsErrorResponse) mark `status`/`error` required; the new AppleErrorResponse omits this. As a docs-first contract that the implementation will be built against, leaving these unconditionally-returned fields optional means consumers and the implementer cannot rely on them. Mark the guaranteed fields required (keeping genuinely conditional/nullable fields like `missing_fields`, `composer_name`, `track_number`, and the nullable `album`/`upc`/`record_label` optional).</comment>
<file context>
@@ -1136,6 +1214,240 @@
+ }
+ }
+ },
+ "AppleSongsResponse": {
+ "type": "object",
+ "properties": {
</file context>
Two problems found while rendering the page locally with mintlify dev.
Drop `missing_fields` from AppleErrorResponse. The endpoint builds its
400s with errorResponse(), which emits { status, error } only — it
never uses validationErrorResponse, so the field was documented but
unreachable. Confirmed against the three live 400s (missing isrc,
malformed isrc, unknown storefront); none carried it.
Give 400, 401, and 500 their own response examples. All three shared
the schema-level example, so the rendered 401 and 500 tabs both
displayed an ISRC validation message. Each now shows the message the
endpoint actually returns for that status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
sweetmantech
commented
Aug 17, 2026
Local preview verification — 2026-08-17Rendered with The page rendersTitle, description, Defect 1 — |
| Tab | Rendered after fix | Matches live response |
|---|---|---|
| 400 | isrc must be a valid ISRC: NOTANISRC | yes — captured live |
| 401 | Exactly one of x-api-key or Authorization must be provided | yes — captured live |
| 500 | Failed to reach the Apple Music API | yes — matches getAppleSongsHandler |
The 200 example renders end to end
Full nesting, root → results[] → songs[] → album, with the real values captured from Apple on 2026-08-17. The ℗ in the copyright line survives rendering.
The nested schema expands correctly
29 fields render under results.songs.*, three levels deep, with types and nullability intact (string | null, string<date> | null, object | null). The rights metadata that motivates the endpoint — results.songs.album.upc, .record_label, .copyright — all present.
Two rendering quirks that are NOT from this PR
Worth recording so nobody mistakes them for regressions here:
The cURL sample omits required parameters. It shows ?storefront=us and no isrc, so copy-pasting it yields a 400. Mintlify's generator appears to include only params carrying a default, and isrc correctly has none.
The response panel preselects the 400 tab, so the first body a reader sees is an error.
Both reproduce on /api-reference/spotify/search:
and on /api-reference/songs/songs, which is generated from a different OpenAPI file — I checked its tab state directly and 400 carries aria-selected="true" there too. So this is site-wide Mintlify behavior, pre-existing, and out of scope for this PR. Worth its own issue if we want the cURL samples to be copy-pasteable.
Not verified
llms.txt— the dev server does not serve it (returns a Next error page); it is generated at deploy. Thedocs.jsonnav entry that feeds it is registered and renders, but the generated output itself needs a deployed preview to confirm.- Light mode — the local preview stayed dark through both a class override and a reload. Site-level theme config, not affected by this PR.
Files
| File | Change |
|---|---|
api-reference/openapi/social.json | /api/apple/songs path + 5 schemas; error contract corrected in b57a1b5 |
api-reference/apple/songs.mdx | reference page |
docs.json | Apple Music nav group |
Both files parse; all $refs resolve.
Uh oh!
There was an error while loading. Please reload this page.
* feat(apple): GET /api/apple/songs — batch ISRC lookup Implements recoupable/app#1959 against the contract in recoupable/docs#298. First Apple Music integration in api. Returns one row per *requested* ISRC, so a recording Apple does not carry surfaces as found: false rather than being omitted — built from meta.filters.isrc, which is the only place a requested-but-unmatched ISRC appears. Matching on data[].attributes.isrc instead (as the manual sweep script did) silently drops any song whose ISRC differs from the request. Rejects a malformed ISRC with a 400 rather than passing it upstream: Apple answers one with 200 and an empty result, indistinguishable from a genuine takedown, so without the format check a typo would be reported to a customer as their recording having gone dark. Chunks at Apple's hard 25-value filter cap. Requests include=albums so upc, record_label, and copyright arrive in the same round trip. Auth only, no per-artist scoping, matching validateGetSongsRequest. Adds APPLE_MUSIC_PRIVATE_KEY / _KEY_ID / _TEAM_ID to .env.example. The developer token is a self-signed ES256 JWT — signature must be raw R||S (IEEE P1363), not Node's default DER, or Apple returns a bare 401 with no error body. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(apple): derive found from meta hits, drop the ignored limit param Addresses three cubic review findings on #834. Derive `found` from the meta.filters hit count rather than from the songs resolved out of `data`. meta.filters is the authority on existence, so a hit that fails to resolve can no longer be reported as found: false — that would claim a live recording had gone dark, the one error this endpoint must never make. Drop the limit=100 query param. Apple ignores limit on identifier filters and returns every match regardless: verified 2026-08-17, limit=2 against 10 matching songs still returned all 10 with no `next`. The parameter was dead weight that invited the false belief that `data` could be truncated relative to meta.filters. Split for the sub-100-line house rule: fetchAppleSongsChunk.ts holds the per-chunk fetch, catalogTypes.ts holds the raw Apple shapes, and types.ts keeps only our documented contract. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(apple): accept a private key with escaped newlines or wrapping quotes A .p8 routed through a shell, a CI secret store, or a JSON blob commonly arrives with literal backslash-n rather than real line breaks, and sometimes with wrapping quotes. createPrivateKey rejects both, and the resulting 500 gives no hint why. This is the repo's first PEM-valued secret, so there was no existing normalization to inherit. Accept either form. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>





Contract for a new Apple Music catalog lookup endpoint. Implements the docs step of recoupable/app#1959.
Merge order: this PR, then the api implementation. Documentation-driven development — the response shape is settled here, in review, rather than discovered in a code diff.
What this documents
GET /api/apple/songs?isrc=A,B,C&storefront=us— up to 25 comma-separated ISRCs, one result row per requested ISRC.The shape decision worth reviewing: results are keyed on what the caller asked for, not on what Apple returned. A recording Apple does not carry comes back as
{ "isrc": "...", "found": false, "songs": [] }rather than being silently omitted. That is what makes the endpoint answerable for catalog-availability diligence — the absence is the answer, so it has to be in the response.Second shape decision:
songsis an array, not a single object. One ISRC legitimately maps to several Apple song ids when the same recording appears on multiple releases (compilations, anniversary editions). Verified against the live API — a test ISRC returned 6.Why this is not a mirror of the Spotify endpoints
Spotify reaches ISRCs only through a fuzzy search query (
q=isrc:X), which is whylib/spotify/getIsrc.tswrapsgetSearch. Apple'sfilter[isrc]is an exact identifier filter, and it echoes every requested identifier back — including the misses — inmeta.filters.isrc.Apple also returns release-level rights metadata Spotify does not expose:
upc,record_label, andcopyright(the ℗ line). Those are documented onAppleSongAlbum.Deliberately not documented here: Apple twins of
/spotify/artist,/artist/albums,/artist/topTracks, or/search. All four Apple counterparts respond 200 with our key, but Apple publishes no popularity or follower metric on any catalog object, so they would carry strictly less signal than the Spotify endpoints already shipped.Accuracy notes
Every documented field and example value was taken from a live Apple Music API response captured 2026-08-17, not from Apple's documentation. The documented status codes are the four the endpoint will actually emit — no 403 or 404, since auth is account-level with no per-artist scoping (matching the precedent in
validateGetSongsRequest.ts, whose comment records that ISRC-keyed song metadata is DSP-public) and a missing recording is a 200 withfound: false, never a 404.The
500covers an unreachable Apple API. There is no429: Apple surfaced no rate-limit headers across the probe run, and responses are Akamai-cached.Changes
api-reference/openapi/social.json— the/api/apple/songspath plusAppleSongsResponse,AppleIsrcResult,AppleSong,AppleSongAlbum,AppleErrorResponse. Purely additive; the file does not round-trip throughjson.dumpsbyte-for-byte, so the blocks were inserted as anchored text edits and the result re-validated as parsing with all$refs resolving.api-reference/apple/songs.mdx— reference pagedocs.json— new "Apple Music" nav group under the Social Media tab, beside Spotify🤖 Generated with Claude Code
Summary by cubic
Documents the contract for
GET /api/apple/songs(batch ISRC lookup) and corrects the error-response schema to match actual output. This enables catalog-availability checks and rights metadata ahead of implementation.found: false);songsis an array for one-to-many ISRC→song mappings.GET /api/apple/songs?isrc=A,B,...&storefront=us(max 25 ISRCs;storefrontoptional, defaultus).upc,record_label,copyright.AppleErrorResponseis{ status, error }only; separate examples for 400/401/500.Docs changes and rollout
AppleSongsResponse,AppleIsrcResult,AppleSong,AppleSongAlbum,AppleErrorResponseinapi-reference/openapi/social.json; updates per-status examples.api-reference/apple/songs.mdxand an “Apple Music” nav group indocs.json.Written for commit b57a1b5. Summary will update on new commits.