Getting started with MusicAPI is a short sequence of setup steps, and the shape of what you build next has changed since this guide first ran. You create an account, copy a Client ID and Client Secret, whitelist a return URL, connect one music account as a test user, and read that user's playlists with one call. What moved is what you build next. MusicAPI is transfer infrastructure now, so the first useful thing to ship is a listener's library landing inside your service, not a read-only playlist viewer.
Two facts set expectations before step one. There is no SDK to install: it is plain REST from any language, and webhooks tell your backend what happened. And the sandbox is not a free tier. Every account starts in evaluation with 5 connected users, sandbox requests are not metered, and the EUR 500 monthly minimum starts on your first production call.
Decide between Core and Managed Transfers
The two products are different integrations with different bills. Pick before you write code.
| MusicAPI Core | MusicAPI Managed Transfers | |
|---|---|---|
| Where it runs | Inside your app, your screens and copy | Hosted white-label flow you theme |
| What you build | Sign-in handling, library reads, writes, retries | A link to /{slug}/transfer |
| First call | GET /api/{integrationUserUUID}/playlists | https://app.musicapi.com/{slug}/transfer |
| Metering | Source requests only | Source plus destination requests |
| Effort | Real engineering time | Integrate in a day |
Core puts the transfer in your own product: you connect users with the hosted sign-in, then read and write their libraries on the REST API. Managed Transfers moves a user's library from the service they use now into your service through a hosted, white-label flow. That flow is fixed and it is five steps: choose a source, connect both accounts, pick the content, confirm, then a live progress page and a results page with the match rate, the unmatched items and a CSV export.
Billing follows the same split. Core meters one side, Managed Transfers meters both, because with Core the destination is your own platform and MusicAPI never makes those calls.
You are not locked into one. Many customers ship Managed Transfers to get live, then move high-volume flows onto Core, and both feed one meter and one invoice. Read how Managed Transfers works before you commit to building a wizard, and model the engineering side with our playlist transfer build vs buy breakdown.
Step 1: Create the account and copy your keys
Sign up at app.musicapi.com/admin/signup. The dashboard asks what you build: integrating music services into your app, or transferring libraries into your service. The answer decides whether your integrations arrive ready-made or whether you register your own developer app per service first.
Then open API & webhooks and create a key. You get a Client ID and a Client Secret, the values show only once, and the secret stays on your server. Load both into your backend environment as MUSICAPI_CLIENT_ID and MUSICAPI_CLIENT_SECRET from your secret store. Create one key per backend, so revoking one does not take down the others.
Step 2: Whitelist a return URL and turn on one service
Open Settings and add an Accepted Return URL: the page of your app that the user comes back to after the sign-in. For a first test, any URL you control works, for example http://localhost:3000/callback. A returnUrl that is not on that list comes back as a 400 with internalCode: 12.
Then open Integrations and enable one service, for example Spotify. A transferring sign-up has ready-made integrations; an integrating sign-up adds its own developer app first.
Step 3: Send a listener through the hosted sign-in
Music SSO is a single redirect: your account slug, a whitelisted returnUrl, and optionally a service to pre-select.
# Let the listener pick from every service you have enabled:
https://app.musicapi.com/{slug}?returnUrl={returnUrl}
# Or pre-select one and skip the picker, e.g. for your own
# "Import from Apple Music" button:
https://app.musicapi.com/{slug}/appleMusic/auth?returnUrl={returnUrl}
Your account slug goes into the path, and the returnUrl must match one of your Accepted Return URLs. Swap the service in the URL and the redirect, the callback payload and every user endpoint stay identical.
This is the step teams underestimate when they build it themselves. MusicAPI holds the provider OAuth clients, the refresh, the scope changes and the revocations, held only to facilitate the transfers a listener authorises. On MusicAPI's ready-made integrations you register no developer app per provider, store no provider tokens, and write no refresh logic. If you want the shape of the work you are skipping, we covered OAuth and token management across streaming services in detail.
Step 4: Read the callback, then verify it on your backend
After the sign-in, the user's browser opens your returnUrl with one extra query parameter, data64. It is base64-encoded JSON.
const params = new URL(request.url, "https://example.com").searchParams;
const data = JSON.parse(Buffer.from(params.get("data64"), "base64").toString("utf8"));
if (data.authModel.status!== "success") {
throw new Error(`Sign-in failed: ${data.authModel.errorType?? data.authModel.error}`);
}
const integrationUserUUID = data.integrationUserUUID; // store it with your user
Failures arrive in authModel.errorType: accessDenied when the user said no, notAllowedInYourCountry, missingScopes when the user unticked a permission, and deniedSubscriptionTier when the service needs a paid plan. By default the hosted page shows the failure to the user, and forwardFailures=true sends them back to your returnUrl at once instead.
One thing the old version of this guide did not say. data64 is not signed, anybody can open your return URL with made-up data, and you confirm the result with your Client Secret before you link a connected user to your own user.
curl "https://api.musicapi.com/public/integrations/auth/{authModelUUID}" \
-u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET"
Store the integrationUserUUID with your own user, together with integration.type. It is the key for every library call, and it is a secret: keep it out of logs, URLs you share and third parties.
Step 5: Read the library
One call, and the credential is Basic from your backend.
curl "https://api.musicapi.com/api/3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11/playlists" \
-u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET"
{
"results": [
{
"id": "37i9dQZF1DXcBWIGoYBM5M",
"name": "Road trip",
"totalItems": 42,
"imageUrl": "https://images.example.com/road-trip.jpg",
"isOwner": true
}
],
"nextParam": "eyJvZmZzZXQiOjUwfQ==",
"totalItems": 87
}
Paging is cursor style. A nextParam that is not null means there is another page, and you send it back as the nextParam query parameter. Write that loop once: it is the same loop for playlists, liked tracks, albums and artists, and it is what carries a large library without you tracking offsets per provider.
Step 6: Run one transfer and read the per-item results
With a connected user you can run the real thing. For Managed Transfers, open https://app.musicapi.com/{slug}/transfer and walk it yourself. Every page of the wizard is anonymous, so the user needs no account with you or with us.
Matching is where a transfer earns or loses trust. For each source item, MusicAPI searches the destination, first by ISRC where the destination supports it, then by name, artists and album. A candidate must score at least the match threshold of your account, 80 out of 100 by default. Below it, the item is unmatched with the reason lowConfidence, and the closest candidate is reported as the best guess. Nothing is silently dropped, and our playlist transfer reconciliation post covers how to account for every source item in your own UI.
Your backend follows the result through webhooks on state changes, with the Match Results API for the unmatched items. The states worth handling are completedWithErrors, failed, and waitingForReauth: on the last one, nothing is lost, and the transfer continues after the user signs in again or after your backend pushes a new token. Transfers for one person also run one after the other, never side by side, so a second transfer waits until the first ends, then starts by itself.
Credentials: which one goes where
| Credential | Header | Where it lives |
|---|---|---|
| Client Secret (Basic) | Authorization: Basic base64(clientId:clientSecret) | Your backend only |
| Client ID token | Authorization: Token <clientId> | Anywhere, it is public |
| One-time token | Authorization: Token <oneTimeToken> | Your app, 30 seconds after a sign-in |
| DevToken | Authorization: DevToken <ES256 JWT> | Your frontend, signed by your backend |
The Client ID and Client Secret together give full access to your account, so send them only from your backend. For browser code, a DevToken is a short-lived JWT that your backend signs with your own key, so your frontend calls /api/{integrationUserUUID}/… directly and your Client Secret stays on the server. Give each user a token carrying their own sub: a call for any other integrationUserUUID then gets 403. A call with no credential gets 401, and a credential a route refuses gets 403.
The limits and errors to branch on
Each partner account has one rate limit across all its keys and credentials, 300 requests per minute by default. Over the limit the API answers 429 with Retry-After: 60, and that header is the window length in seconds, not the time left in it.
async function callMusicApi(url, init, attempt = 0) {
const response = await fetch(url, init);
if (response.status!== 429 || attempt >= 3) {
return response;
}
const waitSeconds = Number(response.headers.get("retry-after")?? "60");
await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000));
return callMusicApi(url, init, attempt + 1);
}
The other one is 412 with internalCode: 14. It means the music service refused the stored sign-in and MusicAPI could not renew it. Mark the connection broken, show a Reconnect button, and do not retry in a loop. When the same music account signs in again it keeps its integrationUserUUID, so your stored id stays valid. The full table of status codes and internal codes is in the rate limits and errors docs.
What production costs
One meter: requests MusicAPI makes to music streaming services on your behalf, with no per-seat fee, no per-user fee and no charge for transfers that fail to match. One request is one HTTP call out to a streaming service, and calls to the MusicAPI API itself are never metered.
| Bucket | Rate |
|---|---|
| First 5M requests a month | EUR 0.60 per 1,000 |
| 5M to 25M | EUR 0.45 per 1,000 |
| Above 25M | EUR 0.30 per 1,000 |
Buckets are cumulative inside a calendar month: your first five million requests bill at the top rate, the next twenty million at the middle rate, and everything beyond at the floor. For sizing, the pricing page models a typical listener at around 2,000 songs, roughly 60 source requests to read their library plus about 120 destination requests if MusicAPI writes it too.
Accounts on MusicAPI's managed credentials bill at least EUR 500 per month. Sandbox requests are not metered, and that minimum starts on your first production call. Check the numbers against your own volume on the pricing page before you plan a launch.
Next step
Skip months of OAuth and per-provider SDK work. Sandbox keys are free and instant, the whole quickstart is in the Core getting started guide, and one API moves playlists, albums, artists and liked tracks into your service. If the hosted flow is enough, start at how Managed Transfers works.
FAQ
Does a failed match cost anything? The read that found no match counts, because MusicAPI made it. A provider error that gets retried does not: those retries are on MusicAPI.
Does the hosted sign-in count against my rate limit? No. The limit counts calls to /public/* and /api/*, and the hosted sign-in pages and webhooks do not count.
What if the user closes the tab mid sign-in? You get no callback at all. Add uniqueId to the sign-in URL and poll the auth status server side if you need to know.
How long does a transfer session stay readable? A session lives in the user's browser for 24 hours. After the transfer starts, the browser keeps a read-only key, so the user can reload the progress and results pages for 30 days.
Can I set this up with a coding agent? Every docs page has a Markdown twin for your coding agent, and the MusicAPI MCP server lets an agent set up your account: integrations, return URLs, webhooks and alert rules.
