Call an endpoint the clients don't cover
client-catalog and client-user have a function for nearly every endpoint Apple documents. A few are in neither, such as Apple's v1/test and the library relationship of a catalog resource. In this guide you call one by its path, read what the answer itself doesn't hold, walk its pages, and declare a named function for it.
Before you start, you need:
- a client from an integration, or from
createClientin@open-music-sdk/core. The code below calls itmusic, andmusicUserTokenis a listener's token.
Every client has request and paginate, the two functions the named ones are built on. They send each request to https://api.music.apple.com and nowhere else, with the developer token, and turn every failure into the errors Handle errors describes.
The code below imports types from @open-music-sdk/server. In a page, import them from @open-music-sdk/browser or @open-music-sdk/react, which export the same ones.
1. Make a request
request(path, init) sends one request and resolves to the answer's parsed JSON, or to undefined when the answer has no body. Apple's endpoint for testing a connection, v1/test, names no resource, so no function covers it.
On your server or in your page:
// Resolves when Apple accepts your developer token.
await music.request("v1/test");A path is v1/... or /v1/.... In init, you can pass:
method:GET, the default,POST,PUTorDELETE.params: the query. A list is joined with commas, andundefinedis left out.body: sent as JSON.user: whether to send the listener's Music User Token. By default it goes on paths under/v1/meonly.schema: a validator for the answer. See Validate Apple's responses.signal: to abort the request.
Type the answer with a type parameter, or let a schema type it:
import type { tStorefrontsResponse } from "@open-music-sdk/server";
const { data } = await music.request<tStorefrontsResponse>("v1/storefronts", {
params: { limit: 10 },
});2. Send the listener's token outside /v1/me
The library relationship of a catalog song, album, playlist or music video is the listener's copy of it, and needs their token. Its path is in the catalog, not under /v1/me, so pass user: true:
import type {
tAlbumRelationshipsAlbumLibraryRelationship as tAlbumLibrary,
} from "@open-music-sdk/server";
const listener = music.as(musicUserToken);
const storefront = await listener.storefront();
const page = await listener.request<tAlbumLibrary>(
`v1/catalog/${storefront}/albums/1613600183/library`,
{ user: true },
);storefront() gives the storefront the client was created with, or else the listener's, which the client asks Apple for once.
Apple answers 404 for an album the listener doesn't hold, as it did against the live API on 18 October 2026, and with their copy for one they hold a song of.
Build a path from values you trust. A value from outside your app, such as an id from a URL, can hold ../ and point the request somewhere else on Apple's API, where it carries the listener's token. segmentOf checks a value as one segment of a path, as the clients check every id.
segmentOf comes from @open-music-sdk/core, which the integrations don't re-export. Install core beside your integration, on the same version:
import { segmentOf } from "@open-music-sdk/core";
// `albumId` came from outside your app. "../me/library" is a TypeError;
// anything allowed comes back encoded.
const id = segmentOf("albumLibrary", "id", albumId);
const path = `v1/catalog/${storefront}/albums/${id}/library`;
await listener.request(path, { user: true });The first argument names your function in the error, and the second names the value.
The vanilla sample puts the two together, and turns Apple's answer into a yes or a no. Its listener is the client music.as(token) gave it at sign-in, and now.storefront is the storefront it reads the catalog in:
/**
* Whether the listener holds the album: its `library` relationship, which needs
* their token and which no client function asks for. Apple answers 404 for an
* album they do not hold, and counts one they hold a song of.
*/
async function inLibrary(
listener: tBrowserMusic,
albumId: string,
): Promise<boolean> {
// Each value is checked as one segment of the path, as the clients check
// every id: `..` would point the request, and the listener's token with it,
// somewhere else on Apple's API.
const storefront = segmentOf("inLibrary", "storefront", now.storefront);
const id = segmentOf("inLibrary", "id", albumId);
const path = `v1/catalog/${storefront}/albums/${id}/library`;
try {
const { data = [] } = await listener.request<tPage<tLibraryAlbum>>(path, {
user: true,
});
return data.length > 0;
} catch (e) {
if (isAppleMusicError(e, "ApiError") && e.status === 404) return false;
throw e;
}
}The React sample puts the two together, and turns Apple's answer into a yes or a no for a query. Its listener is the client useMusic() gives once the listener is signed in:
/**
* Whether the listener holds the album: its `library` relationship, which needs
* their token and which no client function asks for. Apple answers 404 for an
* album they do not hold, and counts one they hold a song of.
*/
async function inLibrary(
listener: tBrowserMusic,
inStorefront: string,
albumId: string,
): Promise<boolean> {
// Each value is checked as one segment of the path, as the clients check
// every id: `..` would point the request, and the listener's token with it,
// somewhere else on Apple's API.
const storefront = segmentOf("inLibrary", "storefront", inStorefront);
const id = segmentOf("inLibrary", "id", albumId);
const path = `v1/catalog/${storefront}/albums/${id}/library`;
try {
const { data = [] } = await listener.request<tPage<tLibraryAlbum>>(path, {
user: true,
});
return data.length > 0;
} catch (e) {
if (isAppleMusicError(e, "ApiError") && e.status === 404) return false;
throw e;
}
}The Next sample puts the two together on the server, as it draws the album page, and turns Apple's answer into a yes or a no. Its listener is the client music.forUser() gives for the signed-in listener:
/**
* Whether the listener holds the album: its `library` relationship, which needs
* their token and which no client function asks for. Apple answers 404 for an
* album they do not hold, and counts one they hold a song of.
*/
async function inLibrary(
listener: tMusic,
inStorefront: string,
albumId: string,
): Promise<boolean> {
// Each value is checked as one segment of the path, as the clients check
// every id: `..` would point the request, and the listener's token with it,
// somewhere else on Apple's API.
const storefront = segmentOf("inLibrary", "storefront", inStorefront);
const id = segmentOf("inLibrary", "id", albumId);
const path = `v1/catalog/${storefront}/albums/${id}/library`;
try {
const { data = [] } = await listener.request<tPage<tLibraryAlbum>>(path, {
user: true,
});
return data.length > 0;
} catch (e) {
if (isAppleMusicError(e, "ApiError") && e.status === 404) return false;
throw e;
}
}Every integration exports isAppleMusicError and the types the function names: tPage, tLibraryAlbum, and the client's, tBrowserMusic in a page or tMusic on a server.
3. Read the status or a header
request resolves to the body alone. To read the answer's status or one of its headers, pass onResponse in init:
let status: number | undefined;
let contentType: string | null = null;
await music.request("v1/test", {
onResponse: (res) => {
status = res.status;
contentType = res.headers.get("content-type");
},
});It's called once for each answer, after the client has read the body and decided what the answer means, so a request that's retried calls it more than once. Its third argument holds the parsed body as body, and the error the answer made, if any, as error. Read the body from there, not from res.json(), because the body has already been read. In a page, the browser lets you read only the headers Apple's answer exposes to other origins.
Never print the request
onResponse, like the client's onRequest, is handed the request as its second argument, with the developer token in its Authorization header and, where it's sent, the Music User Token in Music-User-Token. A hook that prints requests prints both tokens.
4. Walk pages
paginate(from, init) gives the items of every page, following each answer's next link. from is a path, or a page you already have. It asks for nothing until your loop starts, and stops asking when the loop ends:
import type { tLibraryAlbum } from "@open-music-sdk/server";
const path = `v1/catalog/${storefront}/albums/1613600183/library`;
const pages = listener.paginate<tLibraryAlbum>(path, { user: true });
for await (const album of pages) {
console.log(album.id);
}init takes what request takes, for each page it asks for, and maxPages, the most pages to ask for. Without maxPages, a walk goes on for as long as each page names a next one.
paginate follows a next link anywhere on Apple's API, and sends the listener's token where the link is under /v1/me. So a path or a page from outside your app is as trusted as you make it. To keep one to the catalog, pass user: false, which sends no Music User Token wherever the links point, and give the walk a maxPages:
// `next` came from outside your app, such as a query string your page sent.
for await (const genre of music.paginate(next, { user: false, maxPages: 5 })) {
console.log(genre);
}A search or a chart answers with its pages nested under results, so paginate can't walk it.
5. Declare a named function
To use an endpoint as the clients' functions are used, with its arguments checked and two ways to call it, declare it with the builders in @open-music-sdk/core. The integrations don't re-export them, so install core and types beside your integration, on the same version. Here, the library relationship of a catalog album:
import {
inStorefront,
relationshipGetter,
type tAppleMusicClient,
type tCollection,
type tStorefrontOption,
} from "@open-music-sdk/core";
import type { tAlbumRelationships } from "@open-music-sdk/types";
// Where the catalog's albums are. `user: true` sends the Music User Token,
// though the path isn't under /v1/me.
const albums: tCollection<tStorefrontOption> = Object.assign(
(fn: string, client: tAppleMusicClient, options: tStorefrontOption) =>
inStorefront(
fn,
client,
options.storefront,
(storefront) => `v1/catalog/${storefront}/albums`,
),
{ user: true },
);
export const getAlbumLibrary = relationshipGetter<
Pick<tAlbumRelationships, "library">,
tStorefrontOption
>("getAlbumLibrary", albums);
// Called with a client, it resolves to Apple's answer: the first page.
const page = await getAlbumLibrary(listener, "1613600183", "library");
console.log(page.data.length);
// Bound to a client, it walks every page.
const copies = getAlbumLibrary.bound(listener)("1613600183", "library");
for await (const copy of copies) console.log(copy.id);The function checks the id and the name as path segments, and the options as every client function does, before Apple is asked. The reference for endpoint, resourceGetter, relationshipGetter and tCollection lists the other builders and what each one checks. endpointNamespace binds several to one client at once.
Next steps
- Build your own client from the parts.
- Handle errors.
- The reference for
tAppleMusicClient, withrequestandpaginate, andtRequestInit.