Skip to content

docs(apple): GET /api/apple/songs — batch ISRC lookup contract - #298

Merged
sweetmantech merged 2 commits into
mainfrom
feature/apple-songs-isrc-endpoint
Aug 17, 2026
Merged

docs(apple): GET /api/apple/songs — batch ISRC lookup contract#298
sweetmantech merged 2 commits into
mainfrom
feature/apple-songs-isrc-endpoint

Conversation

@sweetmantech

@sweetmantechsweetmantech commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

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: songs is 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 why lib/spotify/getIsrc.ts wraps getSearch. Apple's filter[isrc] is an exact identifier filter, and it echoes every requested identifier back — including the misses — in meta.filters.isrc.

Apple also returns release-level rights metadata Spotify does not expose: upc, record_label, and copyright (the ℗ line). Those are documented on AppleSongAlbum.

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 with found: false, never a 404.

The 500 covers an unreachable Apple API. There is no 429: Apple surfaced no rate-limit headers across the probe run, and responses are Akamai-cached.

Changes

  • api-reference/openapi/social.json — the /api/apple/songs path plus AppleSongsResponse, AppleIsrcResult, AppleSong, AppleSongAlbum, AppleErrorResponse. Purely additive; the file does not round-trip through json.dumps byte-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 page
  • docs.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.

  • One result per requested ISRC (misses return found: false); songs is an array for one-to-many ISRC→song mappings.
  • Endpoint and params: GET /api/apple/songs?isrc=A,B,...&storefront=us (max 25 ISRCs; storefront optional, default us).
  • Album object includes rights fields Apple exposes: upc, record_label, copyright.
  • Status codes: 200, 400, 401, 500. No 404/403/429.
  • Error contract fixed: AppleErrorResponse is { status, error } only; separate examples for 400/401/500.

Docs changes and rollout

  • Adds OpenAPI path and schemas AppleSongsResponse, AppleIsrcResult, AppleSong, AppleSongAlbum, AppleErrorResponse in api-reference/openapi/social.json; updates per-status examples.
  • Adds api-reference/apple/songs.mdx and an “Apple Music” nav group in docs.json.
  • Merge this PR first; implement the API against this contract next. No runtime changes.

Written for commit b57a1b5. Summary will update on new commits.

Review in cubic

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>
@coderabbitai

Copy link
Copy Markdown

Caution

Review failed

An 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @@
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@cursor

cursorBot commented Aug 17, 2026

Copy link
Copy Markdown

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

Copy link
Copy Markdown
CollaboratorAuthor

Local preview verification — 2026-08-17

Rendered with npx mintlify@latest dev on localhost:3333 at commit 6dbdfa3, driven through Chrome DevTools. The pass found two real defects in this PR, both now fixed in b57a1b5 and re-verified.

The page renders

Songs by ISRC page

Title, description, GET /api/apple/songs path, and the new Apple Music nav group sitting between Spotify and Apify — all as intended. Console is clean: one Connected to Socket.io notice from the dev server, zero errors or warnings.

Defect 1 — missing_fields was documented but is unreachable

AppleErrorResponse declared a missing_fields array. The endpoint builds all of its 400s with errorResponse(), which emits { status, error } only — it never calls validationErrorResponse, so the field can never appear. All three live 400s I captured against the running api (missing isrc, malformed isrc, unknown storefront) confirm it: none carried the field.

My earlier reconciliation on api#834 compared only the 200 body field-by-field, which is how this slipped through. Removed.

Defect 2 — 401 and 500 both rendered an ISRC validation message

All three error responses pointed at the same schema, so Mintlify rendered the schema-level example on every tab. The 401 tab told the reader that authentication had failed because of a malformed ISRC.

Each status now carries its own example, matching what the endpoint actually returns:

401 tab showing the correct auth message

TabRendered after fixMatches live response
400isrc must be a valid ISRC: NOTANISRCyes — captured live
401Exactly one of x-api-key or Authorization must be providedyes — captured live
500Failed to reach the Apple Music APIyes — matches getAppleSongsHandler

The 200 example renders end to end

200 response example

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

album schema fields

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:

Spotify sibling showing the same behavior

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. The docs.json nav 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

FileChange
api-reference/openapi/social.json/api/apple/songs path + 5 schemas; error contract corrected in b57a1b5
api-reference/apple/songs.mdxreference page
docs.jsonApple Music nav group

Both files parse; all $refs resolve.

@sweetmantech
sweetmantech merged commit 55bf433 into mainAug 17, 2026
2 checks passed
sweetmantech added a commit to recoupable/api that referenced this pull request Aug 17, 2026
* 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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@sweetmantech