# Overview URL: https://musicapi.com/docs/get-started/overview.md > What MusicAPI is, how MusicAPI Core and MusicAPI Managed Transfers differ, and which one you need. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Get started. MusicAPI connects your users to their music services. One API reads and writes playlists, liked tracks, albums and artists on Spotify, Apple Music, YouTube, Tidal, Amazon Music, Deezer and the other [supported services](https://musicapi.com/docs/get-started/services-and-features.md). It also searches and matches the catalogs of those services, with no connected user. You use MusicAPI in one of two ways. ## Two products | | MusicAPI Core | MusicAPI Managed Transfers | | ---------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | What you get | A REST API. You build the screens and the transfer logic. | A hosted, white-label transfer flow. You send users to a link. | | Who signs users in | The hosted sign-in, or your own UI that links to it | The hosted wizard, both sides | | Who matches tracks | You, with the catalog search and match endpoints | MusicAPI | | What your backend does | Calls `/api/{integrationUserUUID}/…` for each read and write | Listens to webhooks, reads the Match Results API | | Branding | Your app | Your logo, colours, fonts and custom domain on our pages | | Start here | [Quickstart: Core](https://musicapi.com/docs/get-started/quickstart-core.md) | [Quickstart: Managed Transfers](https://musicapi.com/docs/get-started/quickstart-managed-transfers.md) | Both products share one account, one pair of API keys and one hosted sign-in. ## Which one do I need - **You want users to move their library into your service, and you do not want to build a wizard.** Use Managed Transfers. The flow reads the source library, matches every item, writes it to your service and reports what it could not match. - **You want library data inside your own product**: show a user's playlists, save tracks to their account, build your own transfer or sync feature. Use Core. - **You only need catalog data**: search a track on several services, resolve a shared link, read a public playlist. Use the [catalog endpoints](https://musicapi.com/docs/catalog/overview.md) of Core. They need no connected user. You can start with one and add the other later. ## How a Core integration works 1. **Sign in**: Your user connects a music service on the hosted sign-in. 2. **Callback**: You get the `integrationUserUUID` on your return URL. 3. **API calls**: Your backend calls `/api/{integrationUserUUID}/playlists` and the other library routes. ## How Managed Transfers works 1. **Link**: You send the user to your transfer URL. 2. **Wizard**: The user connects both services and picks the content. 3. **Transfer**: MusicAPI matches and writes every item. 4. **Webhooks**: Your backend gets the result and the unmatched items. ## The base URLs | Host | What it serves | | -------------------------- | ------------------------------------------------------------------------------------------------------ | | `https://api.musicapi.com` | The API. Every endpoint in the [API reference](https://musicapi.com/docs/reference.md). | | `https://app.musicapi.com` | The dashboard (`/admin`), the hosted sign-in (`/{slug}`) and the transfer wizard (`/{slug}/transfer`). | ## Next steps - [Get your keys and connect a test user](https://musicapi.com/docs/get-started/quickstart-core.md). - [Read how the credentials work](https://musicapi.com/docs/get-started/authentication.md). - Building with an AI coding agent? Give it [the agent guide](https://musicapi.com/docs/ai/ai-coding-agents.md). # Quickstart: MusicAPI Core URL: https://musicapi.com/docs/get-started/quickstart-core.md > Get your API keys, connect one test user through the hosted sign-in, and read their playlists. About five minutes. Product: MusicAPI Core. Section: Get started. You get your keys, connect your own music account as a test user, and read its playlists with one API call. > **Signed in to the docs?:** Sign in with your dashboard account (top right). The examples on this page then show your own slug, Client ID and return URL. Your Client Secret never shows on this site. 1. **Create an account** Sign up at [app.musicapi.com/admin/signup](https://app.musicapi.com/admin/signup). Pick what you build: **integrating** music services into your app, or **transferring** libraries into your service. See [Sandbox and going live](https://musicapi.com/docs/get-started/sandbox-and-going-live.md) for what each choice turns on. 2. **Copy your API keys** Open **API & webhooks** in the dashboard. Under **API keys**, create a key and copy its Client ID and Client Secret. The values show only once. Keep the secret on your server only. 3. **Add a return URL** 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`. 4. **Turn on one service** Open **Integrations** and enable a service, for example Spotify. A `transferring` sign-up has ready-made integrations. An `integrating` sign-up adds its own developer app first: see [Set up Spotify](https://musicapi.com/docs/connect-users/configure-spotify.md). 5. **Connect yourself** Open the hosted sign-in in your browser, pick the service and sign in. 6. **Read your playlists** Take the `integrationUserUUID` from the callback and call the API. ## 1. Store your keys Put the two values into the environment of your backend as `MUSICAPI_CLIENT_ID` and `MUSICAPI_CLIENT_SECRET`, from your secret store. Every example on this site reads them from there. Your Client ID is `$MUSICAPI_CLIENT_ID`. ## 2. Open the hosted sign-in Your account slug goes into the path. The `returnUrl` must match one of your Accepted Return URLs. ```url https://app.musicapi.com/{slug}?returnUrl={returnUrl} ``` The user picks a service, signs in and comes back to your return URL. The full list of query parameters is in [Start a sign-in](https://musicapi.com/docs/connect-users/start-sign-in.md). ## 3. Read the callback The return URL gets a `data64` query parameter: base64-encoded JSON. Decode it on your server. ```js 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 ``` ```python import base64, json from urllib.parse import parse_qs, urlparse query = parse_qs(urlparse(request_url).query) data = json.loads(base64.b64decode(query["data64"][0])) if data["authModel"]["status"] != "success": raise RuntimeError(f"Sign-in failed: {data['authModel']['errorType'] or data['authModel']['error']}") integration_user_uuid = data["integrationUserUUID"] # store it with your user ``` ```json { "integration": { "type": "spotify", "isSuperPrivate": false, "returnUrl": "https://api.musicapi.com/callback/spotify" }, "authModel": { "uuid": "a1869581-38b2-4268-aee2-a4c2355efeb0", "status": "success", "error": null, "errorType": null }, "integrationUserUUID": "3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11" } ``` The `integrationUserUUID` is the id of this connected account. Store it next to your own user. ## 4. Read the playlists ```bash curl "https://api.musicapi.com/api/3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11/playlists" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` ```js const auth = Buffer.from(`${process.env.MUSICAPI_CLIENT_ID}:${process.env.MUSICAPI_CLIENT_SECRET}`).toString("base64"); const response = await fetch(`https://api.musicapi.com/api/${integrationUserUUID}/playlists`, { headers: { Authorization: `Basic ${auth}` }, }); const { results, nextParam } = await response.json(); ``` ```python import os, requests response = requests.get( f"https://api.musicapi.com/api/{integration_user_uuid}/playlists", auth=(os.environ["MUSICAPI_CLIENT_ID"], os.environ["MUSICAPI_CLIENT_SECRET"]), ) page = response.json() ``` ```json { "results": [ { "id": "37i9dQZF1DXcBWIGoYBM5M", "name": "Road trip", "totalItems": 42, "imageUrl": "https://images.example.com/road-trip.jpg", "isOwner": true } ], "nextParam": "eyJvZmZzZXQiOjUwfQ==", "totalItems": 87 } ``` A `nextParam` that is not `null` means there is another page. Send it back as the `nextParam` query parameter. See [Pagination](https://musicapi.com/docs/user-library/overview-and-pagination.md). > **No account yet?:** Use **Try it** on any endpoint page of the [API reference](https://musicapi.com/docs/reference.md) while signed out. The docs give you a sandbox session for the catalog calls. Choose **Connect your music account** to run the library calls on your own Spotify, Deezer or other account, so you can see real answers before you sign up. ## Next steps - [Handle the callback](https://musicapi.com/docs/connect-users/handle-callback.md) in production, including failures. - [Read and write the library](https://musicapi.com/docs/user-library/overview-and-pagination.md): playlists, liked tracks, albums, artists. - [Handle a 412](https://musicapi.com/docs/get-started/rate-limits-and-errors.md#the-user-must-sign-in-again-412): the user must connect again. # Quickstart: MusicAPI Managed Transfers URL: https://musicapi.com/docs/get-started/quickstart-managed-transfers.md > Turn on Managed Transfers, open your hosted transfer flow, and watch a demo transfer land in Demoify. Product: MusicAPI Managed Transfers. Section: Get started. You open your own hosted transfer flow, move a few playlists into Demoify, and get the result as a webhook. You write no code until the last step. 1. **Sign up as transferring** Sign up at [app.musicapi.com/admin/signup](https://app.musicapi.com/admin/signup) and pick **transferring** libraries into your service. This turns Managed Transfers on, gives you ready-made source integrations, and sets the destination to Demoify. 2. **Open your transfer URL** Open `https://app.musicapi.com/{slug}/transfer` in a browser. Your slug is `{slug}`. 3. **Run a transfer** Pick a source service, sign in to it, pick a few playlists and confirm. Demoify, the destination, needs no sign-in. 4. **Watch the result** The progress page is live. The results page shows the match rate and every item that did not match. 5. **Get the webhook** Add a webhook endpoint and run a second transfer. Your backend gets `transfer.completed` and `match.report_ready`. > **Signed up as integrating?:** Then Managed Transfers is off for your account, and the transfer URL opens no session. [Contact us](https://schedule.musicapi.com/) to turn it on. ## Your transfer URL ```url https://app.musicapi.com/{slug}/transfer ``` Link your users to it from your app: a button, an email, a web view. With a [custom domain](https://musicapi.com/docs/managed-transfers/turn-on-and-brand.md#custom-domain) the same page answers on your own host. ## What Demoify is Demoify is a pretend music service. It needs no account, keeps what the transfer writes, and lets you open the result like a real library. Use it to see the whole flow before your own service is connected. Read more in [Demoify](https://musicapi.com/docs/managed-transfers/demoify.md). ## Add a webhook Open **API & webhooks** in the dashboard, add your endpoint URL, tick the events and copy the signing secret. The secret shows once. Then press **Send test event**: your endpoint gets a signed `transfer.completed` with `"test": true`. ```json { "event": "transfer.completed", "createdAt": "2026-10-16T10:15:42.101Z", "data": { "transferUuid": "6a002d4b-f95b-471f-a27d-609a42e10cdb", "status": "completedWithErrors", "totalItems": 1204, "matchedItems": 1131, "unmatchedItems": 31, "resultsUrl": "https://api.musicapi.com/public/transfers/6a002d4b-f95b-471f-a27d-609a42e10cdb/unmatched" } } ``` Verify the signature before you trust a delivery: [Set up and verify webhooks](https://musicapi.com/docs/webhooks/set-up-and-verify.md). ## Go to your own destination 1. Set up the integration of your own service under **Integrations**, with your developer app: see the [service setup pages](https://musicapi.com/docs/connect-users/how-sign-in-works.md#set-up-each-service). 2. Open **Transfer Branding** and pick it as the **Transfer destination**. 3. Brand the pages: logo, colours, fonts, custom domain. See [Turn it on and brand it](https://musicapi.com/docs/managed-transfers/turn-on-and-brand.md). Your users already signed in to your service in your app? Skip the destination sign-in with a [pre-authorized session](https://musicapi.com/docs/managed-transfers/preauthorized-sessions.md). ## Next steps - [How Managed Transfers works](https://musicapi.com/docs/managed-transfers/how-it-works.md): the wizard, sessions and limits. - [Transfer results and unmatched items](https://musicapi.com/docs/managed-transfers/results-and-unmatched.md). - [Billing](https://musicapi.com/docs/billing/billing-and-usage.md): how a transfer is metered. # Sandbox and going live URL: https://musicapi.com/docs/get-started/sandbox-and-going-live.md > Evaluation accounts and their cap of 5 connected users, managed integrations versus your own developer apps, the docs sandbox, and what changes for production. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Get started. Your sign-up choice decides how your account starts. Production needs a short check by us. ## The sign-up choice The sign-up form asks what you build. | | Integrating | Transferring | | ------------------------- | -------------------------------------------------- | ------------------------------------------------------------ | | Form text | "Integrating music streaming services into my app" | "Move listener libraries into my service, matching included" | | Managed Transfers | Off | On | | Evaluation account | No | Yes: at most 5 connected users | | Integrations at the start | None. You add your own developer apps. | Managed integrations, ready to use | | Transfer destination | None | Demoify | You get a Client ID and a Client Secret in both cases. ## Evaluation accounts A `transferring` sign-up is an evaluation account. The dashboard shows a **Sandbox** badge. - At most **5 connected users** in total, across all services. A new sign-in past the cap fails with `403` and the message "Evaluation accounts are limited to 5 connected accounts. Contact us to move to production." - Each new connected account counts, on both sides of a transfer. Demoify accounts count too. - The same music account can sign in again: that updates the existing connected user and needs no new slot. - A deleted connected user still counts. [Disconnecting](https://musicapi.com/docs/connect-users/disconnect-users.md) erases the tokens and personal data, but the record stays. Everything else works as in production: the API, the rate limit, the webhooks, the metering. ## Managed integrations or your own developer apps An integration is your account's setup of one music service: its OAuth client, its scopes, and whether users can pick it. - **Managed integrations.** A `transferring` sign-up gets a copy of the integrations that MusicAPI runs. They work at once, and the users see your business name and logo on the sign-in pages. The credentials stay hidden from you and from the API. - **Your own developer app.** You register an app at the music service and paste its credentials into the integration. Users then grant access to your app, under your name at the service. An `integrating` sign-up starts here. Each service page under **Set up each service** says what the service asks for. Start with [Spotify](https://musicapi.com/docs/connect-users/configure-spotify.md). > **Some services need a partner agreement:** Amazon Music library access, Boomplay and Audiomack give API access only to approved partners. Their setup pages say how to ask for it. ## The docs sandbox You can try the API on this site with no account. - Signed out, **Try it** on an endpoint page gets a sandbox session for 1 hour. It acts as our `demo` account and allows 30 calls per minute. It runs the catalog calls: search, match and inspect. - Signed out, **Connect your music account** on an endpoint page opens our hosted sign-in. Pick your service and sign in with your own account. For one hour, **Try it** then runs every library call (playlists, liked songs, albums, artists, the profile and search) on that account, and on no other account. A call that changes the library asks you first, because it changes your real library. **Disconnect** ends the session. Erasing the user, the token refresh and transfers stay closed to the sandbox. - For every other endpoint, sign in. - Signed in, **Try it** acts as your own account, with your rate limit, on your connected users. Each call counts like any other call. A docs session lasts 8 hours or until the browser tab closes. - A docs session never acts as your Client Secret. It cannot delete users, read raw service tokens or create transfer sessions. Revoke a session in the dashboard settings. ## Going live 1. Finish the onboarding steps the dashboard shows. 2. Set up your own developer apps where your users must see your app at the service, and check each service with its **Test** button. 3. Point the Accepted Return URLs at your production hosts. 4. [Contact us](https://schedule.musicapi.com/). We review the account and lift the evaluation cap. 5. Set up [billing](https://musicapi.com/docs/billing/billing-and-usage.md). Production accounts pay per request, with a monthly minimum. What changes in production: the 5-user cap goes away. Your rate limit stays 300 requests per minute unless we agree a different one. # Authentication URL: https://musicapi.com/docs/get-started/authentication.md > The credentials MusicAPI accepts, which route takes which, the Client-Secret-only routes, DevTokens with and without sub, one-time tokens, and how to keep the secret on your server. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Get started. Every API call carries one credential in the `Authorization` header. Which credentials a route accepts depends on its prefix. ## The credentials | Credential | Header | Where it may live | | --------------------- | ---------------------------------------------------- | ------------------------------------------- | | Client Secret (Basic) | `Authorization: Basic base64(clientId:clientSecret)` | Your backend only | | Client ID token | `Authorization: Token ` | Anywhere. It is public. | | One-time token | `Authorization: Token ` | Your app, for 30 seconds after a sign-in | | DevToken | `Authorization: DevToken ` | Your frontend. Your backend signs it. | | Docs session | `Authorization: DocsSession ` | This docs site. You never send it yourself. | Make API keys under **API & webhooks** in the dashboard. Each key is a Client ID and a Client Secret. The values show only once, when you create the key. Make one key per backend, so you can revoke one without touching the others. ## Which route takes which | Route | Basic | Client ID token | One-time token | DevToken | Docs session | | --------------------------------------------------- | -------------------- | --------------- | -------------- | ------------------ | ------------ | | `/public/*` (catalog, connected users) | Yes | Yes | Yes | Yes | Yes | | `/api/{integrationUserUUID}/*` (the user's library) | Yes | **No** | **No** | Yes | Yes | | `DELETE /public/integrations/user/{uuid}` | Yes | No | No | No | No | | `/public/transfer-sessions/*` | Yes | No | No | Only without `sub` | No | | `GET /public/transfers/*` (Match Results) | Yes | No | No | No | Yes | | `/app/*` (hosted sign-in state) | No credential needed | | | | | A call without any credential gets `401`. A credential that a route refuses gets `403`. > **Token works on /public only:** `Authorization: Token ` is for the catalog and the connected-user reads under `/public`. A call to `/api/{integrationUserUUID}/…` with a Client ID token gets `401`. Use Basic from your backend, or a DevToken from your frontend. ## Client Secret (Basic) The Client ID and the Client Secret together give full access to your account. Send them only from your backend. ```bash curl "https://api.musicapi.com/public/search/introspection" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` ```js const auth = Buffer.from(`${process.env.MUSICAPI_CLIENT_ID}:${process.env.MUSICAPI_CLIENT_SECRET}`).toString("base64"); await fetch("https://api.musicapi.com/public/search/introspection", { headers: { Authorization: `Basic ${auth}` }, }); ``` ```python import os, requests requests.get( "https://api.musicapi.com/public/search/introspection", auth=(os.environ["MUSICAPI_CLIENT_ID"], os.environ["MUSICAPI_CLIENT_SECRET"]), ) ``` Only Basic sees the raw music-service tokens (`authData`) and only Basic can delete a connected user. ## Client ID token The Client ID is public. Use it from a browser or an app for the catalog endpoints. ```bash curl -X POST "https://api.musicapi.com/public/search" \ -H "Authorization: Token $MUSICAPI_CLIENT_ID" \ -H "Content-Type: application/json" \ -d '{"type": "track", "track": "Numb", "artist": "Linkin Park", "sources": ["spotify", "appleMusic"]}' ``` ## DevToken A DevToken is a short-lived JWT that your backend signs with your own key. Your frontend then calls `/api/{integrationUserUUID}/…` directly, and your Client Secret stays on the server. 1. **Make a key pair** Make a P-256 key pair with OpenSSL (the commands are below). Keep `private.ec.key` on your backend. 2. **Upload the public key** Open **Settings** in the dashboard and add `public.pem` under **Dev Token JWT Public EC Keys**. Save, and note the **Key ID** of the key. 3. **Sign tokens on your backend** Algorithm `ES256`, the Key ID in `kid`, your Client ID in `iss`, and a short `exp`. ```bash openssl ecparam -name prime256v1 -genkey -noout -out private.ec.key openssl ec -in private.ec.key -pubout -out public.pem ``` ```js import jwt from "jsonwebtoken"; export function signDevToken({ privateKey, keyId, integrationUserUUID }) { const now = Math.floor(Date.now() / 1000); return jwt.sign( { iss: process.env.MUSICAPI_CLIENT_ID, sub: integrationUserUUID, // optional: limits the token to this one user iat: now, exp: now + 60 * 60, }, privateKey, { algorithm: "ES256", keyid: keyId } ); } ``` ```bash curl "https://api.musicapi.com/api/3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11/playlists" \ -H "Authorization: DevToken $DEV_TOKEN" ``` ### With and without `sub` - **With `sub`**: the token works only for the connected user in `sub`. A call for any other `integrationUserUUID` gets `403`. Give the browser of each user a token with their own `sub`. - **Without `sub`**: the token works for every connected user of your account. Keep such a token on your servers, for example for `POST /public/transfer-sessions`, which refuses a token with `sub`. A DevToken never deletes a connected user and never reads raw service tokens. ## One-time tokens Add `requestOneTimeToken=true` to the [sign-in URL](https://musicapi.com/docs/connect-users/start-sign-in.md). The callback then carries a `oneTimeToken`. It lives for **30 seconds** and is bound to that one sign-in. Use it as `Authorization: Token ` to read the connected user once, with the raw service tokens. This lets an app without a backend get the tokens. See [Raw tokens and refresh](https://musicapi.com/docs/connect-users/raw-tokens-and-refresh.md). ## Keep the secret on your server - Never put the Client Secret in a browser bundle, a mobile app, a URL, a log or a public repository. - Read it from your secret store at run time: `MUSICAPI_CLIENT_SECRET`. - Frontend code uses a DevToken with `sub`, or the Client ID token for the catalog. - Lost a secret, or leaked one? Create a new key under **API & webhooks**, deploy it, then revoke the old key. Both halves of a revoked key stop working at once. ## Tenant isolation Every lookup by `integrationUserUUID`, sign-in id or transfer id stays inside your account. A uuid of another account answers `404`, the same answer as a uuid that does not exist. # Rate limits and errors URL: https://musicapi.com/docs/get-started/rate-limits-and-errors.md > The rate limit of a partner account, the error shape, the internal error codes, and what to do on a 412 or a 429. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Get started. ## Rate limit Each partner account has one rate limit for all its keys and credentials together. - **300 requests per minute** by default. - One **fixed 60-second window**. The first request starts the window, and the count resets 60 seconds later. - Over the limit, the API answers `429 Too Many Requests` with `Retry-After: 60`. The header gives the window length in seconds, not the time left in the window. - The limit counts calls to `/public/*` and `/api/*`. The hosted sign-in pages and webhooks do not count. - Need more? [Contact us](https://schedule.musicapi.com/). We can raise the limit of your account or remove it. A docs sandbox session has its own limit: 30 calls per minute. ```json { "message": "MusicAPI rate limit. You have exceeded the rate limit for this endpoint or server is currently unable to handle the request. Please wait and retry after the indicated time." } ``` ### Handle a 429 ```js 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); } ``` ```python import time, requests def call_music_api(method, url, attempt=0, **kwargs): response = requests.request(method, url, **kwargs) if response.status_code != 429 or attempt >= 3: return response time.sleep(int(response.headers.get("Retry-After", "60"))) return call_music_api(method, url, attempt + 1, **kwargs) ``` Spread bulk work over time instead of sending it in bursts. A music service can also refuse a call because of its own limits. Retry such a call later, with a growing pause. ## The error shape Every error answer is JSON. ```json { "internalCode": 14, "message": "Needs reauthentication", "sentryId": "b6f1c2d4e5a64f0f9a1b2c3d4e5f6a7b" } ``` | Field | What it is | | -------------- | --------------------------------------------------------------------------------- | | `message` | A short text. Show it in your logs, not to your users. | | `internalCode` | A number for the cases worth branching on (table below). Not every error has one. | | `details` | More detail, when there is any. | | `sentryId` | Our error id. Send it to us when you report a problem. | A body or query that fails validation answers `400` with the field that is wrong: ```json { "type": "body", "message": "\"itemIds\" must contain less than or equal to 20 items", "details": [{ "message": "\"itemIds\" must contain less than or equal to 20 items", "path": ["itemIds"] }] } ``` ## Status codes | Status | Meaning | What to do | | ------ | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `400` | The request is not valid. | Fix the request. Do not retry it unchanged. | | `401` | No credential, or a credential this route does not read. | Check the `Authorization` header and the [route table](https://musicapi.com/docs/get-started/authentication.md#which-route-takes-which). | | `403` | The credential is valid but not allowed here, or an evaluation account reached its cap. | Use a Client Secret where the route needs one. | | `404` | The connected user, item or transfer does not exist in your account. | Check the id. Another account's id also answers `404`. | | `412` | The user must sign in again. | Send the user through the hosted sign-in. | | `418` | The service does not support this operation. | Check [Services and features](https://musicapi.com/docs/get-started/services-and-features.md). | | `422` | The item is already in the collection. | Treat it as done. | | `429` | Rate limit. | Wait `Retry-After` seconds. | | `5xx` | Our error, or the music service failed. | Retry later with a growing pause. | ## Internal error codes | `internalCode` | Name | Status | | -------------- | ------------------------------------------------------------------------------------------------------------- | ------ | | 12 | `InvalidReturnUrl`: the `returnUrl` is not one of your Accepted Return URLs | 400 | | 14 | `IntegrationUserNotAuthenticated`: the stored sign-in no longer works | 412 | | 15 | `NotSupportedMethod`: the service has no such operation | 418 | | 18 | `ItemAlreadyExistsInCollection` | 422 | | 19 | `CollectionDoesNotExists`: the playlist or album is gone, or cannot be read | 404 | | 20 | `ManualLoginRequired`: the user must sign in on the service's own site first | 403 | | 21 | `ActionInMusicServiceRequired`: the user must do something in the service first, for example accept new terms | 403 | ## The user must sign in again (412) A `412` with `internalCode: 14` means the music service refused the stored sign-in, and MusicAPI could not renew it. The user changed their password, removed your app, or the refresh token expired. ```json { "internalCode": 14, "message": "Needs reauthentication" } ``` What to do: 1. Mark the connection as broken in your app. 2. Show the user a "Reconnect" button that opens the [hosted sign-in](https://musicapi.com/docs/connect-users/start-sign-in.md) for the same service. 3. When the same music account signs in again, it keeps its `integrationUserUUID`. Your stored id stays valid. Before it answers `412`, MusicAPI already tried to renew the tokens once. Do not retry the call in a loop. # Services and features URL: https://musicapi.com/docs/get-started/services-and-features.md > The music services a user can connect, and what the API can do on each one. Generated from the same service metadata as the API. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Get started. Music services differ in what their APIs allow. The table shows what MusicAPI can do on each service for a connected user. It is built from the same service metadata that the API uses, so it changes when a service changes. | Service | Id | Status | Update playlists | Remove tracks | Library albums | Library artists | Search by ISRC | Returns ISRC | Page size max | Tracks per playlist | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | Spotify | spotify | live | yes | yes | yes | yes | yes | yes | 50 | 10000 | | Apple Music | appleMusic | live | no | no | yes | yes | yes | yes | 25 | — | | YouTube | youtube | live | yes | yes | no | no | no | no | 50 | — | | Tidal | tidal | live | yes | no | yes | yes | no | yes | 20 | — | | Amazon Music | amazonMusic | live | yes | yes | yes | yes | yes | yes | 100 | 2500 | | Deezer | deezer | live | yes | yes | yes | yes | no | no | 50 | 2000 | | SoundCloud | soundCloud | live | yes | yes | no | yes | no | yes | 50 | 500 | | Boomplay | boomplay | live | yes | yes | yes | no | yes | yes | 50 | — | | Audiomack | audiomack | live | yes | yes | yes | yes | no | yes | 50 | — | | Audius | audius | live | yes | yes | no | no | no | no | 50 | — | | Qobuz | qobuz | live | yes | yes | yes | yes | no | yes | 500 | 1000 | | Resso | resso | no longer available | no | no | no | no | no | no | — | — | Setup guides: [Spotify](https://musicapi.com/docs/connect-users/configure-spotify.md), [Apple Music](https://musicapi.com/docs/connect-users/configure-apple-music.md), [YouTube](https://musicapi.com/docs/connect-users/configure-youtube.md), [Tidal](https://musicapi.com/docs/connect-users/configure-tidal.md), [Amazon Music](https://musicapi.com/docs/connect-users/configure-amazon-music.md), [Deezer](https://musicapi.com/docs/connect-users/configure-deezer.md), [SoundCloud](https://musicapi.com/docs/connect-users/configure-soundcloud.md), [Boomplay](https://musicapi.com/docs/connect-users/configure-boomplay.md), [Audiomack](https://musicapi.com/docs/connect-users/configure-audiomack.md), [Audius](https://musicapi.com/docs/connect-users/configure-audius.md), [Qobuz](https://musicapi.com/docs/connect-users/configure-qobuz.md) ## Read the table - **Update playlists**: rename a playlist and change its description. Creating a playlist and adding tracks work on every live service. - **Remove tracks**: remove items from a playlist or from the liked tracks. - **Library albums** and **Library artists**: read the saved albums and the followed artists. - **Search by ISRC**: the user search (`POST /api/{integrationUserUUID}/search`) takes an `isrc`. - **Returns ISRC**: tracks in answers carry an `isrc`. Matching across services is better when both sides have it. - **Page size max**: the highest `limitParam` the service takes. A higher value is cut to this. - **Tracks per playlist**: the service's own cap on playlist length. A call that a service does not support answers `418` with `internalCode: 15` (`NotSupportedMethod`). ## Services that are not available **Resso** shut down, and MusicAPI no longer supports it. See [Resso](https://musicapi.com/docs/connect-users/configure-resso.md). **Napster** is no longer supported. Napster closed its music streaming service in January 2026 ([Digital Music News](https://www.digitalmusicnews.com/2026/01/02/napster-music-streaming-shut-down-ai-pivot/)), so its public and authenticated endpoints stopped working. ## Demo services Two services exist only for tests and demos. They need no sign-in and never show in a public service list. | Service | Integration type | What it does | | -------------- | ---------------- | ------------------------------------------------------- | | Demoify | `demo` | A pretend destination. It keeps what a transfer writes. | | Sample library | `sample` | A fixed sample library to read from. | The docs sandbox and [Demoify](https://musicapi.com/docs/managed-transfers/demoify.md) use them. ## Catalog search sources The catalog endpoints (no connected user) cover their own list of services. Ask the API for the live list: ```bash curl "https://api.musicapi.com/public/search/introspection" \ -H "Authorization: Token $MUSICAPI_CLIENT_ID" ``` ```json { "sources": ["spotify", "appleMusic", "deezer", "tidal", "youtube"], "authSources": ["spotify", "appleMusic", "youtube", "tidal"], "types": ["track", "album", "artist"] } ``` `sources` are the values that `POST /public/search` and `POST /public/search/match` take. `authSources` are the services a user can connect. # How the hosted sign-in works URL: https://musicapi.com/docs/connect-users/how-sign-in-works.md > The hosted sign-in connects a user's music account to your MusicAPI account and gives you an integrationUserUUID. The flow, the ids, and the setup each service needs. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. Before MusicAPI can read a user's library, the user connects their music account. The hosted sign-in does that for every service with one link. You never handle the music service's OAuth flow yourself. 1. **Your app**: Opens `app.musicapi.com/{slug}` with your `returnUrl`. 2. **Service picker**: The user picks one of the services you turned on. 3. **Music service**: The user signs in and allows access. 4. **MusicAPI callback**: The service returns to `api.musicapi.com/callback/…`. MusicAPI stores the tokens. 5. **Your return URL**: The user comes back with `data64`: the result and the `integrationUserUUID`. ## The ids | Id | What it names | Where you get it | | --------------------- | -------------------------------------------------------- | ------------------------------------------------- | | `slug` | Your MusicAPI account, in the sign-in URL | **Settings** in the dashboard. Yours is `{slug}`. | | `authModel.uuid` | One sign-in attempt | The callback | | `integrationUserUUID` | One connected music account. Every library call uses it. | The callback | | `integrationUserId` | The user's own id at the music service | The connected-user answers | Store the `integrationUserUUID` with your own user. One person can connect several services. Each one is its own connected user with its own uuid. When the same music account signs in again, it gets the same uuid back. ## What the user sees - Your business name and logo on the picker and the result pages. Set both under **Settings** in the dashboard. - Only the services you turned on under **Integrations**. A [direct service link](https://musicapi.com/docs/connect-users/start-sign-in.md#skip-the-picker) skips the picker. - The consent screen of the music service, for your developer app or for the MusicAPI managed app. ## Where the tokens go MusicAPI stores the music-service tokens encrypted, and renews them when they expire. You call the API with the `integrationUserUUID`, never with the service's token. If you need the raw tokens to call the service yourself, read [Raw tokens and refresh](https://musicapi.com/docs/connect-users/raw-tokens-and-refresh.md). ## The pages of this section 1. [Start a sign-in](https://musicapi.com/docs/connect-users/start-sign-in.md): build the URL and its options. 2. [Handle the callback](https://musicapi.com/docs/connect-users/handle-callback.md): read `data64` and check the result. 3. [Poll the sign-in status](https://musicapi.com/docs/connect-users/poll-sign-in-status.md): for apps that cannot receive a redirect. 4. [Connected users](https://musicapi.com/docs/connect-users/connected-users.md): read a user and their service profile. 5. [Raw tokens and refresh](https://musicapi.com/docs/connect-users/raw-tokens-and-refresh.md). 6. [Disconnect and erase a user](https://musicapi.com/docs/connect-users/disconnect-users.md). ## Set up each service Each service needs an integration on your account. A `transferring` sign-up gets managed integrations. Otherwise, register a developer app at the service. Every page lists the callback URL to register, the scopes and the quirks. | Service | Setup | | ------------ | ---------------------------------------------------------------------------------------- | | Spotify | [Set up Spotify](https://musicapi.com/docs/connect-users/configure-spotify.md) | | Apple Music | [Set up Apple Music](https://musicapi.com/docs/connect-users/configure-apple-music.md) | | YouTube | [Set up YouTube](https://musicapi.com/docs/connect-users/configure-youtube.md) | | Tidal | [Set up Tidal](https://musicapi.com/docs/connect-users/configure-tidal.md) | | Amazon Music | [Set up Amazon Music](https://musicapi.com/docs/connect-users/configure-amazon-music.md) | | Deezer | [Set up Deezer](https://musicapi.com/docs/connect-users/configure-deezer.md) | | SoundCloud | [Set up SoundCloud](https://musicapi.com/docs/connect-users/configure-soundcloud.md) | | Boomplay | [Set up Boomplay](https://musicapi.com/docs/connect-users/configure-boomplay.md) | | Audiomack | [Set up Audiomack](https://musicapi.com/docs/connect-users/configure-audiomack.md) | | Audius | [Set up Audius](https://musicapi.com/docs/connect-users/configure-audius.md) | | Qobuz | [Set up Qobuz](https://musicapi.com/docs/connect-users/configure-qobuz.md) | | Resso | [No longer available](https://musicapi.com/docs/connect-users/configure-resso.md) | # Start a sign-in URL: https://musicapi.com/docs/connect-users/start-sign-in.md > Send the user to your hosted sign-in URL. They pick a music service, sign in, and come back to your return URL. All query parameters. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. 1. **Your app**: Links to the sign-in URL 2. **Hosted sign-in**: The user picks a service 3. **Music service**: The user signs in 4. **Your return URL**: You get the connected-user id ## Build the URL Use your account slug in the path. Signed in, the examples show your slug and your first return URL. ```url https://app.musicapi.com/{slug}?returnUrl={returnUrl} ``` ```js const url = new URL("https://app.musicapi.com/{slug}"); url.searchParams.set("returnUrl", "{returnUrl}"); url.searchParams.set("merchantUserId", currentUser.id); // optional window.location.assign(url.toString()); ``` ```python from urllib.parse import urlencode url = "https://app.musicapi.com/{slug}?" + urlencode({ "returnUrl": "{returnUrl}", "merchantUserId": current_user_id, # optional }) ``` The sign-in URL is a page, not an API call. Open it in the user's browser, a new tab, a popup, or a web view. ## Skip the picker Add the service id to the path to send the user straight to one service: ```url https://app.musicapi.com/{slug}/spotify/auth?returnUrl={returnUrl} ``` Use the integration type as the id: `spotify`, `appleMusic`, `youtube`, `tidal`, `amazonMusic`, `deezer`, `soundCloud`, `boomplay`, `audiomack`, `audius`, `qobuz`. > **A direct link ignores the on/off switch:** A direct service link works even for a service that is turned off under **Integrations**. The switch only hides the service from the picker. ## Query parameters - `returnUrl` (string, required): Where the user comes back to, success or failure. It must match one of your **Accepted Return URLs** (rules below). - `requestOneTimeToken` (boolean): Add a `oneTimeToken` to the callback. It lives 30 seconds and lets an app without a backend read the raw service tokens once. See [Raw tokens and refresh](https://musicapi.com/docs/connect-users/raw-tokens-and-refresh.md). - `uniqueId` (string (UUID v4)): A UUID v4 that you make. Poll the sign-in state with it while the user signs in. See [Poll the sign-in status](https://musicapi.com/docs/connect-users/poll-sign-in-status.md). - `forwardFailures` (boolean): Send the user back to `returnUrl` at once when the sign-in fails. Without it, the hosted page shows the failure first. - `merchantUserId` (string): Your own id for this user. It is added to the connected user's list of merchant ids (one connected user can carry several, if several of your users connect the same music account) and written on every usage record of that user, so you can match usage to your users. ## Return URL rules Add return URLs under **Settings → Accepted Return URLs** in the dashboard. A `returnUrl` is accepted when one entry matches all of these: - The same scheme: `https` matches only `https`. A custom app scheme such as `myapp://` works for mobile apps. - The same host. An entry `*.example.com` matches every subdomain of `example.com`, but not `example.com` itself. - The path starts with the entry's path. The entry `https://example.com/music` accepts `https://example.com/music/callback`. Your verified custom domain counts as an accepted return URL on its own. A `returnUrl` that matches nothing fails with `400` and `internalCode: 12` (`InvalidReturnUrl`). Check a URL before you use it with [`GET /app/users/{slug}`](https://musicapi.com/docs/reference/get-partner-profile.md): with `returnUrl` it answers `400` when the URL does not match. ## What happens next The user comes back to `returnUrl` with a `data64` query parameter. Read it in [Handle the callback](https://musicapi.com/docs/connect-users/handle-callback.md). # Handle the callback URL: https://musicapi.com/docs/connect-users/handle-callback.md > Read the data64 parameter on your return URL, check the result on your backend, and store the integrationUserUUID. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. After the sign-in, the user's browser opens your `returnUrl` with one extra query parameter, `data64`. ```url https://app.example.com/callback?data64=eyJpbnRlZ3JhdGlvbiI6eyJ0eXBlIjoic3BvdGlmeSIs... ``` ## Decode data64 `data64` is base64-encoded JSON. ```json { "integration": { "type": "spotify", "isSuperPrivate": false, "returnUrl": "https://api.musicapi.com/callback/spotify" }, "authModel": { "uuid": "a1869581-38b2-4268-aee2-a4c2355efeb0", "status": "success", "error": null, "errorType": null }, "integrationUserUUID": "3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11", "oneTimeToken": "e50cc5e7-254a-4cff-a000-457ae53a059d" } ``` - `integration.type` (string): The service the user connected, for example `spotify`. - `integration.returnUrl` (string): The MusicAPI callback URL of this service. You register it at the service when you use your own developer app. - `authModel.uuid` (string): The id of this sign-in attempt. - `authModel.status` (string): `success`, `error`, or `initialised` (started but never finished; rare in a callback). - `authModel.errorType` (string | null): Why it failed: `accessDenied` (the user said no), `notAllowedInYourCountry`, `missingScopes` (the user unticked a permission), `deniedSubscriptionTier` (the service needs a paid plan). `null` on success or for other failures. - `authModel.error` (string | null): A text for your logs. - `integrationUserUUID` (string | null): The connected user. `null` when the sign-in failed. - `oneTimeToken` (string): Only when you asked for one with `requestOneTimeToken=true`. It lives 30 seconds. ## Check it on your backend `data64` is not signed. Anybody can open your return URL with made-up data. Before you link a connected user to your own user, confirm the result with your Client Secret: ```bash curl "https://api.musicapi.com/public/integrations/auth/a1869581-38b2-4268-aee2-a4c2355efeb0" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` ```js export async function confirmSignIn(data64) { const data = JSON.parse(Buffer.from(data64, "base64").toString("utf8")); const auth = Buffer.from(`${process.env.MUSICAPI_CLIENT_ID}:${process.env.MUSICAPI_CLIENT_SECRET}`).toString("base64"); const response = await fetch(`https://api.musicapi.com/public/integrations/auth/${data.authModel.uuid}`, { headers: { Authorization: `Basic ${auth}` }, }); if (!response.ok) { throw new Error(`Unknown sign-in: ${response.status}`); // 404: not a sign-in of your account } const { authModel, integrationUser } = await response.json(); if (authModel.status !== "success" || integrationUser === null) { return { ok: false, reason: authModel.errorType ?? "unknown" }; } return { ok: true, integrationUserUUID: integrationUser.uuid, profile: integrationUser }; } ``` ```python import base64, json, os, requests def confirm_sign_in(data64: str): data = json.loads(base64.b64decode(data64)) response = requests.get( f"https://api.musicapi.com/public/integrations/auth/{data['authModel']['uuid']}", auth=(os.environ["MUSICAPI_CLIENT_ID"], os.environ["MUSICAPI_CLIENT_SECRET"]), ) response.raise_for_status() # 404: not a sign-in of your account result = response.json() if result["authModel"]["status"] != "success" or result["integrationUser"] is None: return None return result["integrationUser"] ``` ```json { "authModel": { "uuid": "a1869581-38b2-4268-aee2-a4c2355efeb0", "status": "success", "error": null, "errorType": null }, "integrationUser": { "uuid": "3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11", "integrationUserId": "mica.nowak", "name": "Mica Nowak", "email": "mica@example.com", "imageUrl": "https://images.example.com/mica.jpg", "country": "PL", "authData": { "accessToken": "BQD…" }, "authDataExpiresAt": 1792137600000 } } ``` With Basic the answer also holds `authData`, the raw service tokens without the refresh token. Leave them alone unless you call the service yourself. The other credentials get the profile only. The profile fields depend on the service. Some services give no name or no email. ## Store the result Store the `integrationUserUUID` with your own user, together with `integration.type`. It is the key for every library call: ```url https://api.musicapi.com/api/3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11/playlists ``` ## Failures - `status: "error"` with `errorType: "accessDenied"`: the user cancelled. Offer the button again. - `missingScopes`: the user unticked a permission. Ask them to sign in again and keep every box ticked. - `deniedSubscriptionTier`: the service needs a paid plan for API access. - No callback at all: the user closed the tab. Use [`uniqueId` polling](https://musicapi.com/docs/connect-users/poll-sign-in-status.md) if you need to know. By default the hosted page shows the failure to the user. Add `forwardFailures=true` to the sign-in URL to send the user back to your `returnUrl` at once, with `status: "error"` in `data64`. ## No credentials in the browser [`GET /app/integrations/{authModelUUID}`](https://musicapi.com/docs/reference/get-hosted-sign-in.md) reads the same attempt with no credential at all: the `authModelUUID` is the secret. It never returns `authData`. Keep the `authModelUUID` away from logs and third parties. # Poll the sign-in status URL: https://musicapi.com/docs/connect-users/poll-sign-in-status.md > Start a sign-in with your own uniqueId and poll its state, for desktop apps, TVs and other clients that cannot receive a redirect. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. Some clients cannot catch a redirect: a desktop app that opens the system browser, a TV app that shows a QR code, a CLI. Give the sign-in your own id and ask for its state until it ends. 1. **Make a UUID v4** Make it on the client, for example with `crypto.randomUUID()`. Use a new one for every sign-in. 2. **Open the sign-in with it** Add it as `uniqueId` to the sign-in URL, and open the URL in the browser. 3. **Poll** Call `GET /app/integrations/byUniqueId/{uniqueId}` every 2 seconds until `authModel.status` is `success` or `error`. ```url https://app.musicapi.com/{slug}?returnUrl={returnUrl}&uniqueId=7f1d2b9a-3c4e-4f5a-8b6c-9d0e1f2a3b4c ``` ```js const uniqueId = crypto.randomUUID(); openBrowser(`https://app.musicapi.com/{slug}?returnUrl=${encodeURIComponent("{returnUrl}")}&uniqueId=${uniqueId}`); async function waitForSignIn() { for (let i = 0; i < 150; i += 1) { const response = await fetch(`https://api.musicapi.com/app/integrations/byUniqueId/${uniqueId}?tokenOnly=1`); if (response.ok) { const { authModel, integrationUser } = await response.json(); if (authModel.status === "success") return integrationUser.uuid; if (authModel.status === "error") throw new Error(authModel.errorType ?? "Sign-in failed"); } await new Promise((resolve) => setTimeout(resolve, 2000)); } throw new Error("Timed out"); } ``` ```bash curl "https://api.musicapi.com/app/integrations/byUniqueId/7f1d2b9a-3c4e-4f5a-8b6c-9d0e1f2a3b4c?tokenOnly=1" ``` ## The answer - `404` until the user picks a service: the attempt does not exist yet. Keep polling. - `200` with `authModel.status: "initialised"` while the user signs in. - `200` with `success` or `error` at the end. ```json { "authModel": { "uuid": "a1869581-38b2-4268-aee2-a4c2355efeb0", "status": "success", "error": null, "errorType": null }, "oneTimeToken": null, "integrationUser": { "uuid": "3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11", "integrationUserId": "mica.nowak", "name": "Mica Nowak", "email": "mica@example.com", "imageUrl": "https://images.example.com/mica.jpg", "country": "PL" } } ``` ## Security The call needs no credential: the `uniqueId` you chose is the secret. Treat it like a password for the length of the sign-in. - Always make the `uniqueId` with a secure random generator, never from a user id. - Send `tokenOnly=1` from a browser or any client you do not control. The answer then never holds music-service credentials. - Without `tokenOnly`, a sign-in started with `requestOneTimeToken=true` whose `returnUrl` is not an `http(s)` URL (a native app scheme) also returns `authData`, the raw service tokens. A sign-in that returns to an `http(s)` URL never does. - With `requestOneTimeToken=true`, `oneTimeToken` carries a token that lives 30 seconds. # Get a connected user URL: https://musicapi.com/docs/connect-users/connected-users.md > Read a connected user of your account, and the user's own profile on the music service ("Me"). Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. A connected user is one music account that signed in through your hosted sign-in. Two calls read it. | Call | What it returns | Calls the music service | | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------- | | [`GET /public/integrations/user/{integrationUserUUID}`](https://musicapi.com/docs/reference/get-integration-user.md) | What MusicAPI stored at the sign-in: name, email, image, country. With Basic, also the raw tokens. | Only to renew expiring tokens | | [`GET /api/{integrationUserUUID}/users/profile`](https://musicapi.com/docs/reference/get-user-profile.md) | The profile as the service returns it now ("Me") | Yes | ## The stored user ```bash curl "https://api.musicapi.com/public/integrations/user/3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` ```bash curl "https://api.musicapi.com/public/integrations/user/3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11" \ -H "Authorization: Token $MUSICAPI_CLIENT_ID" ``` ```json { "integrationUser": { "uuid": "3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11", "integrationUserId": "mica.nowak", "name": "Mica Nowak", "email": "mica@example.com", "imageUrl": "https://images.example.com/mica.jpg", "country": "PL" }, "integration": { "type": "spotify" } } ``` `integration.type` tells you the service of this user. `country` is the ISO 3166-1 alpha-2 code, when the service gives one. ## The service profile ("Me") ```bash curl "https://api.musicapi.com/api/3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11/users/profile" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` This is a library call: it goes to the music service, it counts as one request, and it answers `412` when the user must sign in again. Use it to check that a connection still works. ## List all connected users The **Music Users** page of the dashboard lists every connected user with search and a service filter. The API has no list endpoint: store the `integrationUserUUID` of each user when the callback comes in. # Raw service tokens and refresh URL: https://musicapi.com/docs/connect-users/raw-tokens-and-refresh.md > Read a connected user's own music-service access token to call the service yourself, from your backend or once from an app, and force a token refresh. Product: MusicAPI Core. Section: Connect your users. MusicAPI keeps each user's music-service tokens and renews them for you. Most integrations never touch them. Read them only to call a service endpoint that MusicAPI does not cover. > **Raw tokens are personal data:** An access token opens the user's music account. Read it on your backend, keep it out of logs, and do not store it longer than you need it. ## From your backend (Client Secret) [`GET /public/integrations/user/{integrationUserUUID}`](https://musicapi.com/docs/reference/get-integration-user.md) with Basic authentication returns `authData`. ```bash curl "https://api.musicapi.com/public/integrations/user/3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` ```json { "integrationUser": { "uuid": "3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11", "integrationUserId": "mica.nowak", "name": "Mica Nowak", "email": "mica@example.com", "imageUrl": "https://images.example.com/mica.jpg", "country": "PL", "authData": { "accessToken": "BQD…" }, "authDataExpiresAt": 1792137600000 }, "integration": { "type": "spotify" } } ``` - `authData` holds the service's credentials **without the refresh token**. The fields depend on the service. - `authDataExpiresAt` is when the access token expires, in milliseconds since the epoch. - When the token expires within one minute, MusicAPI renews it before it answers. ## From an app without a backend (one-time token) 1. **Ask for a token** Add `requestOneTimeToken=true` to the [sign-in URL](https://musicapi.com/docs/connect-users/start-sign-in.md). 2. **Read it from the callback** The decoded `data64` holds `oneTimeToken`. 3. **Spend it at once** Call the connected-user endpoint with `Authorization: Token ` within 30 seconds. ```url https://app.musicapi.com/{slug}?returnUrl={returnUrl}&requestOneTimeToken=true ``` ```bash curl "https://api.musicapi.com/public/integrations/user/3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11" \ -H "Authorization: Token e50cc5e7-254a-4cff-a000-457ae53a059d" ``` The one-time token is bound to the sign-in it came from. For another connected user it answers `403`. You can also pass it as `?oneTimeToken=` to [`GET /public/integrations/auth/{authModelUUID}`](https://musicapi.com/docs/reference/get-sign-in-result.md). ### Attach-only tokens for browser sign-ins You can keep the music-service tokens out of the browser. In the dashboard, open **API & webhooks** and turn on **Browser logins can only attach**. The setting is off by default. When it is on, the `oneTimeToken` of a sign-in that returns to a web page (an `http` or `https` `returnUrl`) does not read `authData`. [`GET /public/integrations/user/{integrationUserUUID}`](https://musicapi.com/docs/reference/get-integration-user.md) and [`GET /public/integrations/auth/{authModelUUID}`](https://musicapi.com/docs/reference/get-sign-in-result.md) answer that token without `authData` and `authDataExpiresAt`. The token still proves the sign-in. Read the raw tokens from your backend with your Client Secret. A sign-in that returns to a native app (a custom scheme such as `myapp://`) keeps the full token. ## Force a refresh [`POST /public/integrations/refreshAuthToken`](https://musicapi.com/docs/reference/refresh-integration-user-token.md) renews the tokens of one user now, for example after the service told you the token is no longer valid. ```bash curl -X POST "https://api.musicapi.com/public/integrations/refreshAuthToken" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"integrationUserUUID": "3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11"}' ``` - With Basic the answer holds the new `authData`. Other credentials get the profile only. - When the service refuses the renewal, the answer is `401`. The user must [sign in again](https://musicapi.com/docs/get-started/rate-limits-and-errors.md#the-user-must-sign-in-again-412). You never need this for MusicAPI's own library calls: they renew an expired token by themselves. # Disconnect and erase a user URL: https://musicapi.com/docs/connect-users/disconnect-users.md > Delete a connected user when your own user deletes their account. What gets erased, what stays, and why the call needs your Client Secret. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. When one of your users deletes their account, or disconnects a music service in your app, delete the connected user in MusicAPI too. One call from your backend does it. ```bash curl -X DELETE "https://api.musicapi.com/public/integrations/user/3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` ## Answers | Status | Meaning | | ------ | -------------------------------------------------- | | `204` | The user is erased. No body. | | `400` | The uuid is not a valid UUID. | | `401` | The request has no credential. | | `403` | The credential is not your Client Secret. | | `404` | Your account has no connected user with this uuid. | The call is idempotent: a second delete of the same user answers `204` again, so you can retry it safely. > **Client Secret only:** Only Basic authentication can delete a user. A Client ID token, a DevToken, a one-time token and a docs session get `403`, because each of them can sit in a browser or an app. ## What gets erased - The music-service access and refresh tokens. MusicAPI can no longer act for this user. - The user's name, email and profile image. - The copy of the user's library that Managed Transfers keeps. ## What stays MusicAPI keeps the uuid, the service's own user id and the country code, so your usage numbers and transfer history stay correct. None of them names a person on its own. If the same person connects the same music account again later, they get the same `integrationUserUUID` back. ## At the music service Deleting the user in MusicAPI does not remove the permission at the music service. The user can remove the app in the "connected apps" settings of their music account. # Set up Spotify URL: https://musicapi.com/docs/connect-users/configure-spotify.md > What you need from the Spotify developer dashboard, the callback URL and scopes to register, and the Spotify quirks. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. > **Managed integration:** A `transferring` sign-up gets copies of the integrations that MusicAPI runs. When your **Integrations** page already lists Spotify, users can sign in with no setup on your side. Set up your own Spotify app when your users must see your app's name at Spotify, or when your account has no Spotify integration. ## What you need from Spotify An app in the [Spotify for Developers dashboard](https://developer.spotify.com/dashboard). From its **Settings** you copy: | Spotify field | MusicAPI field | | ------------- | -------------- | | Client ID | Client ID | | Client secret | Client Secret | ## Callback URL Add this exact **Redirect URI** to the Spotify app: ```url https://api.musicapi.com/callback/spotify ``` ## Scopes MusicAPI asks for these scopes by default. You can change the list on the integration. ```text playlist-read-collaborative playlist-modify-public playlist-read-private playlist-modify-private user-read-private user-read-email user-library-modify user-library-read user-top-read user-read-recently-played user-read-currently-playing user-follow-modify user-follow-read ugc-image-upload ``` A user who signed in before a scope was added does not have it. Such a user must sign in again. ## Add it to MusicAPI 1. Open **Integrations** in the dashboard and pick Spotify. 2. Paste the Client ID and the Client Secret, and save. 3. Turn **Is Enabled** on, so the picker shows Spotify. 4. Press **Test Spotify** and sign in once with your own account. ## Quirks - Spotify starts a new app in Development Mode, where only users you allow-list can sign in. Apply at Spotify for extended access before you launch. - Every library operation of the API works on Spotify. Tracks carry an ISRC, and the user search takes one. - Spotify returns no preview URL. - At most 50 items per page (`limitParam`), at most 10,000 tracks per playlist. ## Direct sign-in link ```url https://app.musicapi.com/{slug}/spotify/auth?returnUrl={returnUrl} ``` # Set up Apple Music URL: https://musicapi.com/docs/connect-users/configure-apple-music.md > What you need from the Apple Developer portal (Team ID, Key ID, private key, Services ID), the Sign in with Apple return URL, and the Apple Music limits. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. > **Managed integration:** A `transferring` sign-up gets copies of the integrations that MusicAPI runs. When your **Integrations** page already lists Apple Music, users can sign in with no setup on your side. Set up your own Apple keys when your account has no Apple Music integration, or when the developer token must be in your company's name. ## What you need from Apple A paid Apple Developer Program membership, then: | Apple value | Where you find it | MusicAPI field | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | Team ID | [developer.apple.com/account](https://developer.apple.com/account), membership details | Team ID | | Key ID and private key (`.p8`) | Certificates, Identifiers & Profiles → Keys. [Create a media identifier and private key](https://developer.apple.com/help/account/configure-app-capabilities/create-a-media-identifier-and-private-key/). | Key ID, Private Key | | Services ID | Identifiers → Services IDs. [Register a Services ID](https://developer.apple.com/help/account/manage-identifiers/register-a-services-id). | Client ID | The key must have both **Media Services (MusicKit)** and **Sign in with Apple** turned on. A key without both fails with `{"message": "Invalid token specified"}`. ## Return URL and domains On the Services ID, configure **Sign in with Apple**: - **Domains and subdomains**: `api.musicapi.com` and `app.musicapi.com` - **Return URL**: ```url https://api.musicapi.com/callback/apple_sign_in ``` ## How the Apple sign-in works The user signs in with Apple, then MusicKit asks for access to their library. MusicAPI signs the developer token with your private key, stores the MusicKit user token, and handles the storefront and the library-versus-catalog ids for you. Apple Music has no OAuth scopes to pick. ## Add it to MusicAPI 1. Open **Integrations** in the dashboard and pick Apple Music. 2. Paste the Team ID, the Key ID, the Client ID (Services ID) and the private key, and save. 3. Turn **Is Enabled** on. 4. Press **Test Apple Music** and sign in once with your own Apple account. ## Quirks Apple's API allows less than most services. With Apple Music you **cannot**: - rename or re-describe a playlist, or delete one; - remove or reorder tracks in a playlist, or remove liked tracks; - remove a saved album, or follow and unfollow artists. Creating playlists, adding tracks, liking tracks and saving albums work. The email comes from the Sign in with Apple id token of each sign-in; an account that signed in before has it after its next sign-in. At most 25 items per page (`limitParam`). Tracks carry an ISRC, and the user search takes one. ## Direct sign-in link ```url https://app.musicapi.com/{slug}/appleMusic/auth?returnUrl={returnUrl} ``` # Set up YouTube URL: https://musicapi.com/docs/connect-users/configure-youtube.md > What you need from the Google Cloud console (YouTube Data API v3, consent screen, OAuth client), the redirect URI and scopes, and the YouTube limits. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. The `youtube` integration connects a Google account and works on its YouTube and YouTube Music library through the YouTube Data API. YouTube Music playlists and liked songs are YouTube playlists. > **Managed integration:** A `transferring` sign-up gets copies of the integrations that MusicAPI runs. When your **Integrations** page already lists YouTube, users can sign in with no setup on your side. Set up your own Google project when your account has no YouTube integration, or when your users must see your app on the Google consent screen. ## What you need from Google 1. **Create a project** In the [Google Cloud console](https://console.cloud.google.com/apis/credentials), create a project. 2. **Turn on the API** Under **Enabled APIs and services**, enable **YouTube Data API v3**. 3. **Set up the consent screen** Pick the user type, set the app name and a developer contact email, and add the scopes below. While the app is not verified, add your testers as **Test users**. 4. **Create the OAuth client** Under **Credentials**, create an **OAuth client ID** of type **Web application** and add the redirect URI below. 5. **Copy the credentials** Copy the **Client ID** and the **Client secret**. ## Redirect URI Add this exact URI under **Authorized redirect URIs**: ```url https://api.musicapi.com/callback/youtube ``` ## Scopes ```text https://www.googleapis.com/auth/youtube https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile ``` The user must allow every scope. When the user unticks one, the sign-in fails with `errorType: "missingScopes"`. The `youtube` scope is a sensitive scope. Google must verify your app before people outside your test users can sign in. ## Add it to MusicAPI 1. Open **Integrations** in the dashboard and pick YouTube. 2. Paste the Client ID and the Client Secret, and save. 3. Turn **Is Enabled** on. 4. Press **Test YouTube** and sign in once with your own Google account. ## Quirks - Playlists work: read, create, rename, delete, add, remove and reorder items. - There are no saved albums or followed artists, and no liked-track writes. - YouTube gives no ISRC, so matching into YouTube uses the title and the artist. YouTube titles often mix both. - Each Google project has a daily YouTube Data API quota. A busy project can run out before the day ends. - At most 50 items per page (`limitParam`). ## Direct sign-in link ```url https://app.musicapi.com/{slug}/youtube/auth?returnUrl={returnUrl} ``` # Set up Tidal URL: https://musicapi.com/docs/connect-users/configure-tidal.md > What you need from the Tidal developer dashboard, the redirect URI and scopes to register, and the Tidal limits. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. > **Managed integration:** A `transferring` sign-up gets copies of the integrations that MusicAPI runs. When your **Integrations** page already lists Tidal, users can sign in with no setup on your side. Set up your own Tidal app when your account has no Tidal integration, or when your users must see your app at Tidal. ## What you need from Tidal An app in the [Tidal developer dashboard](https://developer.tidal.com/dashboard). Create it with **Create App**, then copy: | Tidal field | MusicAPI field | | ------------- | -------------- | | Client ID | Client ID | | Client Secret | Client Secret | ## Redirect URI ```url https://api.musicapi.com/callback/tidal ``` ## Scopes ```text user.read collection.read search.read playlists.write playlists.read collection.write playback recommendations.read search.write ``` ## Add it to MusicAPI 1. Open **Integrations** in the dashboard and pick Tidal. 2. Paste the Client ID and the Client Secret, and save. 3. Turn **Is Enabled** on. 4. Press **Test Tidal** and sign in once. ## Quirks - Tidal cannot remove or reorder tracks in a playlist through the API. - Tracks carry an ISRC, but the user search does not take one. - At most 20 items per page (`limitParam`), a low cap. Expect more pages. ## Direct sign-in link ```url https://app.musicapi.com/{slug}/tidal/auth?returnUrl={returnUrl} ``` # Set up Amazon Music URL: https://musicapi.com/docs/connect-users/configure-amazon-music.md > What you need from the Amazon Developer Portal (a Security Profile), the allowed return URL, the scopes, and the business agreement that library access needs. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. > **Managed integration:** A `transferring` sign-up gets copies of the integrations that MusicAPI runs. When your **Integrations** page already lists Amazon Music, users can sign in with no setup on your side. > **Library access needs an Amazon agreement:** Your own Security Profile lets users sign in with Amazon. To read and write their music library, you also need an agreement with Amazon: contact your Amazon business development representative. ## What you need from Amazon A Security Profile in the [Amazon Developer Portal](https://developer.amazon.com/dashboard): 1. Click **Create New Security Profile** and fill it in. 2. Save, then open **Web Settings**. 3. Add the return URL below to **Allowed Return URLs**. 4. Copy the **Client ID** and the **Client Secret** from **Web Settings**, and the **Security Profile ID** from the **General** tab. | Amazon field | MusicAPI field | | -------------------------------------------- | ------------------- | | Security Profile ID (`amzn1.application.…`) | Security Profile ID | | Client ID (`amzn1.application-oa2-client.…`) | Client ID | | Client Secret | Client Secret | ## Allowed return URL ```url https://api.musicapi.com/callback/amazonMusic ``` ## Scopes ```text profile music::playlists ``` ## Add it to MusicAPI 1. Open **Integrations** in the dashboard and pick Amazon Music. 2. Paste the three values, and save. 3. Turn **Is Enabled** on. 4. Press **Test Amazon** and sign in once. ## Quirks - Amazon returns owned and followed playlists from two sources. MusicAPI joins them, so the list of playlists has no reliable order. - Amazon gives no user country. - At most 100 items per page (`limitParam`), at most 2,500 tracks per playlist. ## Direct sign-in link ```url https://app.musicapi.com/{slug}/amazonMusic/auth?returnUrl={returnUrl} ``` # Set up Deezer URL: https://musicapi.com/docs/connect-users/configure-deezer.md > What you need from Deezer for developers, the redirect URI and permissions, and the Deezer limits. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. > **Managed integration:** A `transferring` sign-up gets copies of the integrations that MusicAPI runs. When your **Integrations** page already lists Deezer, users can sign in with no setup on your side. Set up your own Deezer app when your account has no Deezer integration, or when your users must see your app at Deezer. ## What you need from Deezer An application at [Deezer for developers](https://developers.deezer.com). Click **Create a new application**, then copy: | Deezer field | MusicAPI field | | -------------- | -------------- | | Application ID | Application ID | | Secret Key | Secret Key | ## Redirect URI ```url https://api.musicapi.com/callback/deezer ``` ## Permissions MusicAPI asks Deezer for these permissions: ```text basic_access email offline_access manage_library delete_library listening_history ``` ## Add it to MusicAPI 1. Open **Integrations** in the dashboard and pick Deezer. 2. Paste the Application ID and the Secret Key, and save. 3. Turn **Is Enabled** on. 4. Press **Test Deezer** and sign in once. ## Quirks - Deezer gives no ISRC in its answers, and the user search does not take one. Matching into Deezer uses the title, the artist and the album. - At most 50 items per page (`limitParam`), at most 2,000 tracks per playlist. ## Direct sign-in link ```url https://app.musicapi.com/{slug}/deezer/auth?returnUrl={returnUrl} ``` # Set up SoundCloud URL: https://musicapi.com/docs/connect-users/configure-soundcloud.md > What you need from SoundCloud for developers, the one redirect URI SoundCloud allows, and the SoundCloud limits. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. > **Managed integration:** A `transferring` sign-up gets copies of the integrations that MusicAPI runs. When your **Integrations** page already lists SoundCloud, users can sign in with no setup on your side. Set up your own app only when SoundCloud already approved one for you. ## What you need from SoundCloud An application at [SoundCloud for developers](https://soundcloud.com/you/apps). Copy: | SoundCloud field | MusicAPI field | | ---------------- | -------------- | | Client ID | Client ID | | Client Secret | Client Secret | ## Redirect URI SoundCloud stores only one redirect URI per app. Register exactly this one. Note the hyphen: it is not the integration type `soundCloud`. ```url https://api.musicapi.com/callback/sound-cloud ``` ## Add it to MusicAPI 1. Open **Integrations** in the dashboard and pick SoundCloud. 2. Paste the Client ID and the Client Secret, and save. 3. Turn **Is Enabled** on. 4. Press **Test SoundCloud** and sign in once. ## Quirks - SoundCloud has no saved albums. - Tracks can carry an ISRC, but the user search does not take one. - Many uploads exist only on SoundCloud, so a match into other services often fails for them. - SoundCloud gives no user email. - At most 50 items per page (`limitParam`), at most 500 tracks per playlist. ## Direct sign-in link ```url https://app.musicapi.com/{slug}/soundCloud/auth?returnUrl={returnUrl} ``` # Set up Boomplay URL: https://musicapi.com/docs/connect-users/configure-boomplay.md > Boomplay API access needs an agreement with Boomplay. What you need, the redirect URI, and the Boomplay limits. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. > **Managed integration:** A `transferring` sign-up gets copies of the integrations that MusicAPI runs. When your **Integrations** page already lists Boomplay, users can sign in with no setup on your side. ## What you need from Boomplay Boomplay has no open developer program. Access needs a developer agreement with Boomplay: [contact Boomplay](https://www.boomplay.com/contact-us/) directly. With the agreement you get: | Boomplay value | MusicAPI field | | -------------- | -------------- | | Client ID | Client ID | | Client Secret | Client Secret | ## Redirect URI ```url https://api.musicapi.com/callback/boomplay ``` ## Add it to MusicAPI 1. Open **Integrations** in the dashboard and pick Boomplay. 2. Paste the Client ID and the Client Secret, and save. 3. Turn **Is Enabled** on. 4. Press **Test Boomplay** and sign in once. ## Quirks - Boomplay returns owned and liked playlists from two sources. MusicAPI joins them, so the list of playlists has no reliable order. - You cannot delete a playlist, reorder its tracks, or remove liked tracks. - Boomplay has no followed artists in the API. - Boomplay gives no user email and no user country. - At most 50 items per page (`limitParam`). ## Direct sign-in link ```url https://app.musicapi.com/{slug}/boomplay/auth?returnUrl={returnUrl} ``` # Set up Audiomack URL: https://musicapi.com/docs/connect-users/configure-audiomack.md > Audiomack grants API access per partner. What you need, the callback URL, and the Audiomack limits. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. > **Managed integration:** A `transferring` sign-up gets copies of the integrations that MusicAPI runs. When your **Integrations** page already lists Audiomack, users can sign in with no setup on your side. ## What you need from Audiomack Audiomack creates API apps on request. Contact Audiomack, and read the [Audiomack API documentation](https://audiomack.com/data-api/docs). You get: | Audiomack value | MusicAPI field | | --------------- | -------------- | | Key | Key | | Secret | Secret | ## Callback URL Audiomack uses OAuth 1.0a. MusicAPI sends this callback with each sign-in: ```url https://api.musicapi.com/callback/audiomack ``` ## Add it to MusicAPI 1. Open **Integrations** in the dashboard and pick Audiomack. 2. Paste the two values, and save. 3. Turn **Is Enabled** on. 4. Press **Test Audiomack** and sign in once. ## Quirks - You cannot reorder tracks in a playlist. - Tracks carry an ISRC, but the user search does not take one. - At most 50 items per page (`limitParam`). ## Direct sign-in link ```url https://app.musicapi.com/{slug}/audiomack/auth?returnUrl={returnUrl} ``` # Set up Audius URL: https://musicapi.com/docs/connect-users/configure-audius.md > What you need from Audius (a developer app key and secret), the redirect URI and the write scope, and the Audius limits. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. > **Managed integration:** A `transferring` sign-up gets copies of the integrations that MusicAPI runs. When your **Integrations** page already lists Audius, users can sign in with no setup on your side. Set up your own Audius app when your account has no Audius integration, or when users must see your app on the Audius consent screen. ## What you need from Audius A developer app on Audius. It gives you two values: | Audius value | MusicAPI field | | ------------ | -------------- | | API key | Key | | API secret | Secret | ## Redirect URI ```url https://api.musicapi.com/callback/audius ``` ## Scope MusicAPI asks for the `write` scope, so it can create playlists and save tracks for the user. The Audius consent screen shows your account slug as the origin: `MusicAPI [your-slug]`. ## Add it to MusicAPI 1. Open **Integrations** in the dashboard and pick Audius. 2. Paste the key and the secret, and save. 3. Turn **Is Enabled** on. 4. Press **Test Audius** and sign in once. ## Quirks - Audius has no albums or followed artists in the API. - You cannot reorder tracks in a playlist. - Audius gives no ISRC, and the user search does not take one. - An Audius sign-in has no refresh step. When it stops working, the API answers `412` and the user signs in again. - At most 50 items per page (`limitParam`). ## Direct sign-in link ```url https://app.musicapi.com/{slug}/audius/auth?returnUrl={returnUrl} ``` # Set up Qobuz URL: https://musicapi.com/docs/connect-users/configure-qobuz.md > Qobuz needs four values from your Qobuz developer account (OAuth and app credentials), one redirect URI, and three scopes. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. > **Managed integration:** A `transferring` sign-up gets copies of the integrations that MusicAPI runs. When your **Integrations** page already lists Qobuz, users can sign in with no setup on your side. ## What you need from Qobuz Qobuz uses two pairs of credentials from your Qobuz developer account: an OAuth pair for the user sign-in, and an app pair for the catalog API. | Qobuz value | MusicAPI field | Used for | | ------------------- | -------------- | ------------------------------------------------------ | | OAuth Client ID | Client ID | The user sign-in | | OAuth Client Secret | Client Secret | The user sign-in | | App ID | App ID | Catalog requests. It can be the same as the Client ID. | | App Secret | App Secret | Catalog requests | ## Redirect URI Register this redirect URI in the Qobuz developer portal: ```url https://api.musicapi.com/callback/qobuz ``` ## Scopes ```text streaming library playlists ``` ## Add it to MusicAPI 1. Open **Integrations** in the dashboard and pick Qobuz. 2. Paste the four values, and save. 3. Turn **Is Enabled** on. 4. Press **Test Qobuz** and sign in once. ## Quirks - Tracks carry an ISRC, but the user search does not take one. - At most 500 items per page (`limitParam`), the highest cap of all services. At most 1,000 tracks per playlist. ## Direct sign-in link ```url https://app.musicapi.com/{slug}/qobuz/auth?returnUrl={returnUrl} ``` # Resso is no longer available URL: https://musicapi.com/docs/connect-users/configure-resso.md > Resso shut down. Its sign-in no longer works, and you cannot set it up. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Connect your users. > **No longer available:** Resso shut down its service, and MusicAPI no longer supports it. Do not count on a new Resso sign-in, or on library calls for a Resso user you connected before. What to do: - Remove Resso from your service pickers and from your marketing pages. - Ask users who connected Resso to connect another service. - [Delete](https://musicapi.com/docs/connect-users/disconnect-users.md) the Resso connected users that you still store. The integration type `resso` stays in the API and in old data, so old records still read correctly. See the services that work today in [Services and features](https://musicapi.com/docs/get-started/services-and-features.md). # Read and write a user's library URL: https://musicapi.com/docs/user-library/overview-and-pagination.md > Playlists, playlist items, liked tracks, albums, artists and search in the user's own service. One URL shape, pagination with nextParam and limitParam, and at most 20 items per write. Product: MusicAPI Core. Section: Read and write a user's library. Every library call names one connected user in the path. The answer has the same shape on every service. ```url https://api.musicapi.com/api/{integrationUserUUID}/ ``` Authenticate with Basic from your backend, or with a [DevToken](https://musicapi.com/docs/get-started/authentication.md#devtoken) from your frontend. A Client ID token does not work here. ## The endpoints | What | Read | Write | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Playlists | [`GET /api/{integrationUserUUID}/playlists`](https://musicapi.com/docs/reference/list-playlists.md), [`GET /api/{integrationUserUUID}/playlists/{playlistId}`](https://musicapi.com/docs/reference/get-playlist.md) | [`POST /api/{integrationUserUUID}/playlists`](https://musicapi.com/docs/reference/create-playlist.md), [`PUT /api/{integrationUserUUID}/playlists/{playlistId}`](https://musicapi.com/docs/reference/update-playlist.md), [`DELETE /api/{integrationUserUUID}/playlists/{playlistId}`](https://musicapi.com/docs/reference/delete-playlist.md) | | Playlist items | [`GET /api/{integrationUserUUID}/playlists/{playlistId}/items`](https://musicapi.com/docs/reference/list-playlist-items.md) | [`POST /api/{integrationUserUUID}/playlists/{playlistId}/items`](https://musicapi.com/docs/reference/add-playlist-items.md), [`PUT /api/{integrationUserUUID}/playlists/{playlistId}/items`](https://musicapi.com/docs/reference/move-playlist-items.md), [`DELETE /api/{integrationUserUUID}/playlists/{playlistId}/items`](https://musicapi.com/docs/reference/remove-playlist-items.md) | | Liked tracks | [`GET /api/{integrationUserUUID}/liked/tracks`](https://musicapi.com/docs/reference/list-liked-tracks.md) | [`POST /api/{integrationUserUUID}/liked/tracks`](https://musicapi.com/docs/reference/add-liked-tracks.md), [`DELETE /api/{integrationUserUUID}/liked/tracks`](https://musicapi.com/docs/reference/remove-liked-tracks.md) | | Albums | [`GET /api/{integrationUserUUID}/albums`](https://musicapi.com/docs/reference/list-albums.md), [`GET /api/{integrationUserUUID}/albums/{albumId}`](https://musicapi.com/docs/reference/get-album.md), [`GET /api/{integrationUserUUID}/albums/{albumId}/items`](https://musicapi.com/docs/reference/list-album-items.md) | [`POST /api/{integrationUserUUID}/albums`](https://musicapi.com/docs/reference/save-album.md), [`DELETE /api/{integrationUserUUID}/albums/{albumId}`](https://musicapi.com/docs/reference/remove-album.md) | | Artists | [`GET /api/{integrationUserUUID}/artists`](https://musicapi.com/docs/reference/list-artists.md), [`GET /api/{integrationUserUUID}/artists/{artistId}`](https://musicapi.com/docs/reference/get-artist.md) | [`POST /api/{integrationUserUUID}/artists`](https://musicapi.com/docs/reference/follow-artist.md), [`DELETE /api/{integrationUserUUID}/artists/{artistId}`](https://musicapi.com/docs/reference/unfollow-artist.md) | | Search the user's service | [`POST /api/{integrationUserUUID}/search`](https://musicapi.com/docs/reference/search-user-service.md) | | | Profile ("Me") | [`GET /api/{integrationUserUUID}/users/profile`](https://musicapi.com/docs/reference/get-user-profile.md) | | Not every service supports every write. Check [Services and features](https://musicapi.com/docs/get-started/services-and-features.md). An unsupported call answers `418` with `internalCode: 15`. ## Pagination Every list answer has the same shape: ```json { "results": [{ "id": "1", "name": "Road trip", "totalItems": 42, "imageUrl": null, "isOwner": true }], "nextParam": "eyJvZmZzZXQiOjUwfQ==", "totalItems": 87 } ``` - `nextParam`: send it back as the `nextParam` query parameter to get the next page. `null` on the last page. It is opaque: do not build or change it. - `limitParam`: the page size you want. The service's own maximum caps it (Spotify 50, Tidal 20, Amazon Music 100, Qobuz 500; see the **Page size max** column in [Services and features](https://musicapi.com/docs/get-started/services-and-features.md)). Some services ignore it. - `totalItems`: the total, when the service tells it. It can be `null`. ```js async function listAllPlaylists(integrationUserUUID) { const auth = Buffer.from(`${process.env.MUSICAPI_CLIENT_ID}:${process.env.MUSICAPI_CLIENT_SECRET}`).toString("base64"); const playlists = []; let nextParam = null; do { const url = new URL(`https://api.musicapi.com/api/${integrationUserUUID}/playlists`); url.searchParams.set("limitParam", "50"); if (nextParam !== null) url.searchParams.set("nextParam", nextParam); const response = await fetch(url, { headers: { Authorization: `Basic ${auth}` } }); if (response.status === 412) throw new Error("The user must sign in again"); if (!response.ok) throw new Error(`MusicAPI ${response.status}`); const page = await response.json(); playlists.push(...page.results); nextParam = page.nextParam; } while (nextParam !== null); return playlists; } ``` ```python import os, requests def list_all_playlists(integration_user_uuid): auth = (os.environ["MUSICAPI_CLIENT_ID"], os.environ["MUSICAPI_CLIENT_SECRET"]) playlists, next_param = [], None while True: params = {"limitParam": 50} if next_param is not None: params["nextParam"] = next_param response = requests.get( f"https://api.musicapi.com/api/{integration_user_uuid}/playlists", params=params, auth=auth ) response.raise_for_status() page = response.json() playlists.extend(page["results"]) next_param = page["nextParam"] if next_param is None: return playlists ``` ```bash curl "https://api.musicapi.com/api/3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11/playlists?limitParam=50&nextParam=eyJvZmZzZXQiOjUwfQ==" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` ## Writes: at most 20 items per call `itemIds` and `entryIds` take **at most 20 ids** per request. A longer list fails with `400`. Split it: ```js for (let i = 0; i < trackIds.length; i += 20) { const response = await fetch(`https://api.musicapi.com/api/${integrationUserUUID}/playlists/${playlistId}/items`, { method: "POST", headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" }, body: JSON.stringify({ itemIds: trackIds.slice(i, i + 20) }), }); if (!response.ok) throw new Error(`MusicAPI ${response.status}`); } ``` ## Ids per service Ids are the music service's own ids. A Spotify track id works only for Spotify users. - `id`: the track, album, artist or playlist on that service. Add and like calls take `itemIds` of track ids. - `entryId`: one entry of a track inside a playlist or the liked tracks. A playlist can hold the same track twice, with two entry ids. Remove calls take `entryIds`. Read them from the list calls. - To move a track to another service, find its id there with the [catalog match](https://musicapi.com/docs/catalog/overview.md) or with the user search. ## Search inside the user's service [`POST /api/{integrationUserUUID}/search`](https://musicapi.com/docs/reference/search-user-service.md) searches the service of this user, with their account and their country: ```bash curl -X POST "https://api.musicapi.com/api/3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11/search" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"type": "track", "track": "Numb", "artist": "Linkin Park", "isrc": "USWB10300474"}' ``` `type` is `track`, `album` or `artist`. `isrc` works where the service supports a search by ISRC. ## When a call fails - `412`: the user must [sign in again](https://musicapi.com/docs/get-started/rate-limits-and-errors.md#the-user-must-sign-in-again-412). - `404` with `internalCode: 19`: the playlist or album is gone. - `422` with `internalCode: 18`: the item is already in the collection. - `429`: wait `Retry-After` seconds. See [Rate limits](https://musicapi.com/docs/get-started/rate-limits-and-errors.md). Each library call that reaches the music service counts as one billed request, failed calls included. See [Billing and usage](https://musicapi.com/docs/billing/billing-and-usage.md). # Search and match the catalog URL: https://musicapi.com/docs/catalog/overview.md > Search tracks, albums and artists across services, match items into other services, resolve a shared link, read a public playlist, and embed a player. No connected user needed. Product: MusicAPI Core. Section: Search and match the catalog. The catalog endpoints work on the public catalogs of the music services. They need no connected user, so they work before anyone signs in. | Task | Endpoint | | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | Which services and types can I search? | [`GET /public/search/introspection`](https://musicapi.com/docs/reference/get-search-introspection.md) | | Find a track, album or artist on several services | [`POST /public/search`](https://musicapi.com/docs/reference/search-catalog.md) | | Find the same items on other services | [`POST /public/search/match`](https://musicapi.com/docs/reference/match-catalog-items.md) | | What does this shared link point at? | [`POST /public/inspect/url`](https://musicapi.com/docs/reference/inspect-url.md) | | Read the tracks of a public playlist | [`POST /public/inspect/playlistItems`](https://musicapi.com/docs/reference/get-public-playlist-items.md) | | Embed a player for a link | [`POST /public/embed/url`](https://musicapi.com/docs/reference/create-embed-player.md) | ## Authentication Use `Authorization: Token `. The Client ID is public, so these calls may run in a browser or an app. Basic and DevToken work too. The embed endpoint needs no credential. ## Which services The catalog covers its own list of services, which differs from the list of services a user can connect. Ask for it: ```bash curl "https://api.musicapi.com/public/search/introspection" \ -H "Authorization: Token $MUSICAPI_CLIENT_ID" ``` `sources` lists the values that `sources`, `destinations` and `source` take below. `types` lists `track`, `album` and `artist`. ## Search across services ```bash curl -X POST "https://api.musicapi.com/public/search" \ -H "Authorization: Token $MUSICAPI_CLIENT_ID" \ -H "Content-Type: application/json" \ -d '{ "type": "track", "track": "Bezos I", "artist": "Bo Burnham", "album": "INSIDE", "sources": ["spotify", "appleMusic", "deezer"] }' ``` ```json { "tracks": [ { "source": "spotify", "status": "success", "type": "track", "data": { "externalId": "5aszL9hl6SBzFNsOvw8u8w", "name": "Bezos I", "artistNames": ["Bo Burnham"], "albumName": "INSIDE", "imageUrl": "https://i.scdn.co/image/ab67616d0000b27388fed14b936c38007a302413", "previewUrl": null, "isrc": "", "duration": 58149, "url": "https://open.spotify.com/track/5aszL9hl6SBzFNsOvw8u8w" } } ] } ``` Each source answers on its own, with its own `status`: `success`, `not_found` or `error`. One failing service does not fail the others. ## Match items into other services You have ids on one service and want the same items on others. Send the ids and the destinations: ```bash curl -X POST "https://api.musicapi.com/public/search/match" \ -H "Authorization: Token $MUSICAPI_CLIENT_ID" \ -H "Content-Type: application/json" \ -d '{ "destinations": ["appleMusic", "tidal"], "objects": [{ "id": "5aszL9hl6SBzFNsOvw8u8w", "source": "spotify" }] }' ``` The answer has one entry per object: the object with its catalog data, and one match per destination that has it. Use the matched ids in the [library writes](https://musicapi.com/docs/user-library/overview-and-pagination.md) of a user on that service. ## Resolve a shared link ```bash curl -X POST "https://api.musicapi.com/public/inspect/url" \ -H "Authorization: Token $MUSICAPI_CLIENT_ID" \ -H "Content-Type: application/json" \ -d '{"url": "https://tidal.com/browse/track/131696293"}' ``` The answer says what the link points at (a track, an album or a playlist), on which service, with its metadata. ## Read a public playlist ```bash curl -X POST "https://api.musicapi.com/public/inspect/playlistItems" \ -H "Authorization: Token $MUSICAPI_CLIENT_ID" \ -H "Content-Type: application/json" \ -d '{"serviceId": "spotify", "playlistId": "37i9dQZF1DXcBWIGoYBM5M"}' ``` Up to 50 tracks per page. Send the answer's `nextParam` back in the body for the next page, until it is `null`. ## Embed a player ```bash curl -X POST "https://api.musicapi.com/public/embed/url" \ -H "Content-Type: application/json" \ -d '{"url": "https://open.spotify.com/track/0pqnGHJpmpxLKifKRmU6WP"}' ``` The answer holds `embedCode` (an `iframe`), `embedUrl`, the available `sizes` and the `defaultSize`. To change the height, set it in two places: the `height` of the iframe and the `height` query parameter of its `src`. See the [embed player page](https://musicapi.com/embed) for a live example. # How Managed Transfers works URL: https://musicapi.com/docs/managed-transfers/how-it-works.md > The hosted, white-label transfer wizard, its sessions, the library snapshots, matching, and the rule of one transfer at a time per person. Product: MusicAPI Managed Transfers. Section: Managed Transfers. MusicAPI Managed Transfers moves a user's library from the service they use now into your service. It is a hosted, white-label flow: your users open a page, connect both services, pick what to move and watch it happen. You write no wizard, no matching and no retry logic. Your backend follows the result through webhooks. ## The entry URL ```url https://app.musicapi.com/{slug}/transfer ``` With a [custom domain](https://musicapi.com/docs/managed-transfers/turn-on-and-brand.md#custom-domain), the same path answers on your host: `https://transfer.example.com/{slug}/transfer`. ## The wizard 1. **Choose a source** The user picks the service they leave. The list holds the music integrations that are on in your account, minus your destination. 2. **Connect both accounts** The user signs in to the source, then to your destination service, through the [hosted sign-in](https://musicapi.com/docs/connect-users/how-sign-in-works.md). A [pre-authorized session](https://musicapi.com/docs/managed-transfers/preauthorized-sessions.md) skips the destination sign-in. 3. **Pick the content** Playlists, followed playlists, liked tracks, albums and artists, as far as you allow them for that source and the services support them. 4. **Confirm** The user reviews the selection and starts the transfer. For a playlist that already exists on the destination, the user picks how to handle it. 5. **Progress and results** A live progress page, then a results page with the match rate, the unmatched items and a CSV export. We do not email your users unless you turn on [transfer emails](https://musicapi.com/docs/managed-transfers/turn-on-and-brand.md#transfer-emails). ## Sessions - Every page of the wizard is anonymous. The user needs no account with you or with us. - A session lives in the user's browser for **24 hours**. - After the transfer starts, the browser also keeps a read-only key to it. The user can reload the progress and results pages for **30 days**. - The browser never holds your API keys or the user's service tokens. ## Library snapshots When a user connects an account, MusicAPI reads the library once and keeps a snapshot. Every wizard screen reads that snapshot, so picking content is fast and costs no extra calls to the service. A **Refresh** in the wizard reads the library again. A transfer reads the source as it was in the snapshot that the user picked from. ## Matching 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. Unmatched items and their reasons: [Transfer results and unmatched items](https://musicapi.com/docs/managed-transfers/results-and-unmatched.md). ## One transfer at a time per person Transfers of one person run one after the other, never side by side. A second transfer waits until the first one ends, then starts by itself. When a music service asks MusicAPI to slow down, the transfer waits and continues by itself. When a service keeps refusing for too long, the transfer fails with `errorType: "rateLimited"` and `failureReason: "rateLimited"`. ## Transfer states | Status | Meaning | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `created`, `fetching`, `running` | The transfer reads the source and writes the destination. | | `paused` | The transfer is paused. | | `waitingForReauth` | One account needs a new sign-in. Nothing is lost. The transfer continues after the user signs in again, or after your backend [pushes a new token](https://musicapi.com/docs/managed-transfers/preauthorized-sessions.md#push-a-new-token). | | `completed` | Every item reached the destination or was already there. | | `completedWithErrors` | The transfer ended, and some items did not match or failed. | | `failed` | The transfer stopped and cannot continue. | | `cancelled` | The transfer was cancelled. | ## What you see - **Transfers** in the dashboard: every transfer with its status and match rate, with a CSV export. - **Analytics → Funnel**: where users drop out, from the first page view to a finished transfer. - **Analytics → Reliability**: error rates per service, and the [alert rules](https://musicapi.com/docs/webhooks/alert-rules.md). - [Webhooks](https://musicapi.com/docs/webhooks/set-up-and-verify.md) for your backend, and the [Match Results API](https://musicapi.com/docs/managed-transfers/results-and-unmatched.md) for the unmatched items. > **Do not poll:** Webhooks tell your backend what happened. Fetch the unmatched items after `match.report_ready`, or once a day with the bulk endpoint. Do not call the API in a loop to follow a running transfer. # Turn on Managed Transfers and brand it URL: https://musicapi.com/docs/managed-transfers/turn-on-and-brand.md > Turn the hosted flow on, pick the transfer destination, choose the content types, and brand the pages with your colours, fonts, CSS and your own domain. Product: MusicAPI Managed Transfers. Section: Managed Transfers. ## Turn it on Managed Transfers is on or off per account. - A **transferring** sign-up has it on from the start, with Demoify as the destination. - An **integrating** account has it off. [Contact us](https://schedule.musicapi.com/) to turn it on. While it is off, the transfer pages stay hidden in the dashboard, the hosted flow refuses to start, and the transfer APIs answer `403`. ## Set the destination Open **Transfer Branding** in the account menu. The **Transfer destination** is the service every transfer moves into. The hosted flow cannot start without one. 1. Set up the integration of your service under **Integrations** first, with your developer app (see [Set up each service](https://musicapi.com/docs/connect-users/how-sign-in-works.md#set-up-each-service)). 2. Pick it as the **Transfer destination**. Until then, Demoify stays the destination of a transferring sign-up, so you can test the whole flow on the first day. ## Choose the content Each integration has an **Entity types offered to users** setting: playlists, followed playlists, liked tracks, albums, artists. The wizard offers only the types you turn on, and only the types that the source service can read. ## Brand the pages The **Theme tokens** of the Transfer Branding page skin every hosted page. Every token is optional. | Token | What it sets | | ----------------- | ----------------------------------------------------------------------------- | | Mode | Light or dark | | Accent colour | Buttons, links, progress | | Background colour | The page | | Surface colour | Cards and panels | | Button radius | A pixel length, for example `8px` | | Heading font | The font family of the headings. Load the font yourself in the custom CSS. | | Custom CSS | Added to every hosted page. Markup is refused, so no tag can be opened there. | The **Live preview** shows your changes before you press **Publish theme**. Your logo and business name come from the account settings. ## Custom domain Run the flow on your own host, for example `transfer.example.com`. 1. **Save the domain** Enter it as the **Custom domain** on the Transfer Branding page. The status shows "pending setup on our side". 2. **Contact us** Our servers accept traffic only through Cloudflare, so we add your domain to our configuration and tell you the DNS record to create. [Contact us](https://schedule.musicapi.com/). 3. **Link to the full path** The page still lives under your slug: `https://transfer.example.com/{slug}/transfer`. The root of the domain is not a transfer entry point. Your custom domain counts as an accepted return URL by itself, for exactly that host over `https`. Once we verified the domain, the `launchUrl` of a [pre-authorized session](https://musicapi.com/docs/managed-transfers/preauthorized-sessions.md) uses it too. ## Transfer emails We do not email your users about their transfers. This setting is off by default for every account. When you turn it on, we send one email when a transfer ends, to the address that the source service gave us. The email has the MusicAPI brand, not yours. It shows the results and links to the results page. To turn it on, open **API & webhooks** in the dashboard and switch on **Email my users when a transfer ends (MusicAPI-branded)**. > **Use webhooks for your own email:** We recommend [webhooks](https://musicapi.com/docs/webhooks/set-up-and-verify.md). `transfer.completed` and `transfer.failed` tell your backend that a transfer ended. Then you send your users an email in your own brand. ## Set it up with an AI agent The [MusicAPI MCP server](https://musicapi.com/docs/ai/mcp-server.md) has `update_managed_transfers_settings`: the theme, the custom domain, the destination and the transfer emails, in one call from Claude, Cursor or ChatGPT. # Pre-authorized sessions URL: https://musicapi.com/docs/managed-transfers/preauthorized-sessions.md > Your users are already signed in to your service in your app. Send us their token, open the launchUrl, and they connect only the source. Create, push a new token, end. Product: MusicAPI Managed Transfers. Section: Managed Transfers. Use a pre-authorized session when your users are already signed in to your music service inside your own app. Your backend sends us the user's access token, we connect that account as the destination, and the user connects only the source. The wizard shows the destination as already connected. 1. **Your backend**: `POST /public/transfer-sessions` with the user's token 2. **MusicAPI**: Checks the token at the service and answers a `launchUrl` 3. **Your app**: Opens the `launchUrl`. The user connects the source. 4. **Transfer**: Runs. You push a new token when `user.auth_failed` asks for one. ## Requirements - Managed Transfers is on for your account, and your **Transfer destination** is set. - The destination is one of: **Spotify, Deezer, Tidal, YouTube, SoundCloud, Boomplay**. These services take a plain OAuth access token. For any other destination the call answers `400`. - The token has the scopes your destination integration asks for in a normal sign-in: read the library, create and edit playlists, save tracks, albums and artists. - Call the three endpoints from your backend only. They take **Basic** (Client Secret) or a **DevToken without `sub`**. A Client ID token, a one-time token, a DevToken with `sub` and a docs session get `403`. ## Create a session ```bash curl -X POST "https://api.musicapi.com/public/transfer-sessions" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{ "target": { "authData": { "accessToken": "BQD...user-access-token", "refreshToken": "AQB...user-refresh-token", "expiresAt": "2026-10-16T15:00:00Z" } }, "externalUserId": "your-user-8841", "returnUrl": "https://app.example.com/library" }' ``` - `target.authData.accessToken` (string, required): The user's access token for your destination service. - `target.authData.refreshToken` (string): The refresh token. We use it only when your destination integration has OAuth client credentials that can renew this token. - `target.authData.expiresAt` (string | number): When the access token expires: an ISO 8601 date, or a Unix time in seconds or in milliseconds. - `externalUserId` (string): Your own id for the user, up to 255 characters. We send it back in the webhooks of this session. - `returnUrl` (string): The results page shows a "Back to …" button with this URL. It must match your Accepted Return URLs. With no return URLs on your account, it must be an `https` URL. - `locale` (string): The user's language, such as `en` or `pt-BR`. Accepted now so your call does not change when the wizard gets more languages. ```json { "sessionUuid": "0f1e2d3c-4b5a-4968-8776-655443322110", "launchUrl": "https://app.musicapi.com/acme/transfer#session=0f1e2d3c-4b5a-4968-8776-655443322110&launch=8b1Xv...", "launchExpiresAt": "2026-10-16T14:15:00.000Z", "expiresAt": "2026-10-17T14:00:00.000Z", "externalUserId": "your-user-8841", "target": { "integrationType": "spotify", "integrationUserUuid": "9c8b7a65-4321-4f0e-9d8c-7b6a59483726", "displayName": "Mica Nowak" } } ``` A second call for the same music account updates the stored token of that account. It makes no second account. Each call makes a new session, and sends `user.authorized` with `"preauthorized": true`. | Status | Why | | ------ | -------------------------------------------------------------------------------------------- | | `400` | The body is not valid, no destination is set, or the destination does not support this flow. | | `401` | No credential. | | `403` | The credential is not secret, or Managed Transfers is off for your account. | | `422` | The music service refused the access token. We store nothing. | | `503` | We could not reach the music service to check the token. Try again later. We store nothing. | ## The launch URL Open the `launchUrl` in your app as it is: a web view or the browser. It works **once**, and only for **15 minutes** (`launchExpiresAt`). Create the session when the user taps "Transfer", not in advance. The session itself lasts 24 hours (`expiresAt`). The single-use code sits in the URL fragment, after `#`. A browser never sends the fragment to a server, so the code stays out of access logs and `Referer` headers. The wizard trades the code for a session and removes it from the address bar. With a verified [custom domain](https://musicapi.com/docs/managed-transfers/turn-on-and-brand.md#custom-domain), the `launchUrl` uses your domain. ## Token lifetime We store the token encrypted, and never send it back in an answer or a webhook. When the destination refuses it during a transfer, we try to renew it once: - When your destination integration has OAuth client credentials that can renew this token, we renew it with `refreshToken` and the transfer continues. - Otherwise, or when the renewal fails, the transfer stops with status `waitingForReauth` and we send `user.auth_failed`. The transfer keeps everything it already moved. ## Push a new token ```bash curl -X PUT "https://api.musicapi.com/public/transfer-sessions/0f1e2d3c-4b5a-4968-8776-655443322110/target-auth" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{ "authData": { "accessToken": "BQE...new-access-token", "refreshToken": "AQB...user-refresh-token", "expiresAt": 1792170000 } }' ``` ```json { "sessionUuid": "0f1e2d3c-4b5a-4968-8776-655443322110", "target": { "integrationType": "spotify", "integrationUserUuid": "9c8b7a65-4321-4f0e-9d8c-7b6a59483726", "displayName": "Mica Nowak" }, "transferResumed": true } ``` - The token must open the **same** account as the session's destination. A token of another account answers `409`. - We replace the stored token. Leave out `refreshToken`, and we store none. - A transfer of this session that waits for the destination continues at once: `transferResumed: true`. - You can push a token at any time, also before the old one expires. - A session of another partner answers `404`. A session you ended answers `409`. The loop for a long transfer: 1. You get `user.auth_failed` with `"side": "target"`, `"preauthorized": true` and the `sessionUuid`. 2. Your backend gets a fresh token for that user and calls `PUT …/target-auth`. 3. The answer has `"transferResumed": true`. The user's progress page continues without any action from the user. While the transfer waits, the progress page tells the user that your app renews the connection. It does not ask the user to sign in to the destination. ## End a session ```bash curl -X DELETE "https://api.musicapi.com/public/transfer-sessions/0f1e2d3c-4b5a-4968-8776-655443322110" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` ```json { "sessionUuid": "0f1e2d3c-4b5a-4968-8776-655443322110", "transferCancelled": true, "credentialsErased": true } ``` Ending a session: - closes the wizard of this session. A browser that started a transfer can still read its progress and results for 30 days; - cancels its transfer when it still runs (you get `transfer.cancelled`); - erases the stored token, name, email and library copy of the destination account, unless another live session or a running transfer still uses that account. Then `credentialsErased` is `false`. A second call answers the same way and changes nothing. ## Security - The access token and your Client Secret never go into your app's code. - The `launchUrl` is a credential for 15 minutes. Do not log it, and do not send it by email. - We check every token with the music service before we store it. A token of another account cannot replace the destination of a session. - The wizard cannot detach or replace a pre-authorized destination. - Call `DELETE` when the user signs out of your app or deletes their account. Outside a session, use [Disconnect users](https://musicapi.com/docs/connect-users/disconnect-users.md). # Transfer results and unmatched items URL: https://musicapi.com/docs/managed-transfers/results-and-unmatched.md > The Match Results API gives your backend the summary of a transfer and every item that did not move, with the reason and the best guess. Per transfer or as a daily bulk pull. Product: MusicAPI Managed Transfers. Section: Managed Transfers. The Match Results API gives your backend the items that a transfer could not move: the tracks, albums and artists that found no match, or that the destination refused. Use it to show the user what is missing, to retry with your own catalog search, or to measure your catalog gaps. | Call | What it returns | | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | | [`GET /public/transfers/{transferUuid}`](https://musicapi.com/docs/reference/get-transfer-result.md) | The summary: status, counters, source and destination, timestamps. | | [`GET /public/transfers/{transferUuid}/unmatched`](https://musicapi.com/docs/reference/list-transfer-unmatched-items.md) | The unmatched and failed items of one transfer, one page at a time. | | [`GET /public/transfers/unmatched`](https://musicapi.com/docs/reference/list-unmatched-items.md) | The unmatched and failed items of every transfer that finished in a time window. For a daily pull. | ## When to call it After `match.report_ready`. The webhook carries `resultsUrl`, the address of the unmatched items of that transfer. Or once a day, with the bulk endpoint. Do not poll a running transfer: [webhooks](https://musicapi.com/docs/webhooks/set-up-and-verify.md) tell you when it ends. ## Authentication Server to server, with **Basic** (Client ID and Client Secret). The report holds your users' libraries. - No credential: `401`. - A Client ID token, a DevToken or a one-time token: `403`. - Managed Transfers off for your account: `403`. - A transfer of another account: `404`, the same answer as a uuid that does not exist. A signed-in docs session may also read it, so **Try it** works here. These calls are not billed. ## Fetch the unmatched items ```bash curl "https://api.musicapi.com/public/transfers/6a002d4b-f95b-471f-a27d-609a42e10cdb/unmatched?limit=500" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` ```bash curl "https://api.musicapi.com/public/transfers/unmatched?from=2026-10-15T00:00:00Z&to=2026-10-16T00:00:00Z&limit=500" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` ```js async function allUnmatched(transferUuid) { const auth = Buffer.from(`${process.env.MUSICAPI_CLIENT_ID}:${process.env.MUSICAPI_CLIENT_SECRET}`).toString("base64"); const rows = []; let cursor = null; do { const url = new URL(`https://api.musicapi.com/public/transfers/${transferUuid}/unmatched`); url.searchParams.set("limit", "500"); if (cursor !== null) url.searchParams.set("cursor", cursor); const response = await fetch(url, { headers: { Authorization: `Basic ${auth}` } }); if (!response.ok) throw new Error(`MusicAPI ${response.status}`); const page = await response.json(); rows.push(...page.unmatched); cursor = page.nextCursor; } while (cursor !== null); return rows; } ``` ## Query parameters - `reason` (string): Return only the rows with this reason (table below). Both lists. - `limit` (integer): Rows per page, at most 500. Both lists. - `cursor` (string): The `nextCursor` of the previous page. Leave it out for the first page. Both lists. - `from` (ISO 8601, required): Bulk only. A transfer is in the window when it finished at or after `from`. - `to` (ISO 8601): Bulk only. A transfer is in the window when it finished before `to`. The window is at most 31 days. ## Pagination Each list answers `{ "unmatched": [...], "nextCursor": "..." }`. Send `nextCursor` back as `cursor`, and stop when it is `null`. The cursor is opaque: do not build or change it. The rows of one transfer come in a stable order, so a page never repeats or skips a row. The bulk endpoint orders by transfer, then by item. A bulk page can hold fewer rows than `limit` and still have a `nextCursor`: keep going until it is `null`. ## One row ```json { "transferUuid": "6a002d4b-f95b-471f-a27d-609a42e10cdb", "title": "Low", "artist": "Artist A, Artist B", "playlistName": "Road trip", "reason": "lowConfidence", "bestGuess": "Low (Live) — Artist A", "bestGuessUrl": "https://tidal.com/track/123456789", "status": "unmatched", "itemType": "track", "matchingPercentage": 55, "source": { "id": "4uLU6hMCjMI75M1A2tKUQC", "entryId": null, "name": "Low", "artists": ["Artist A", "Artist B"], "album": "The Album", "isrc": "USRC17607839", "durationMs": 201000 }, "group": { "type": "playlist", "name": "Road trip", "sourceId": "37i9dQZF1DXcBWIGoYBM5M" }, "bestGuessCandidate": { "id": "123456789", "name": "Low (Live)", "artists": ["Artist A"], "album": "Live", "isrc": null, "url": "https://tidal.com/track/123456789" } } ``` | Field | Meaning | | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `title`, `artist`, `playlistName`, `reason`, `bestGuess` | The same values as the results page and its CSV export. | | `status` | `unmatched`: no good match. `failed`: the destination refused the item. | | `itemType` | `track`, `album` or `artist`. | | `matchingPercentage` | The score of the best guess, 0 to 100. `null` without a guess. | | `source` | The item on the source service. A field is `null` when the source does not give it. | | `group` | The collection that held the item: `playlist`, `likedTracks`, `albums`, `artists` or `podcasts`. `sourceId` is the source playlist id, `null` for the other types. | | `bestGuessCandidate` | The closest item on the destination, which we did not add. `null` when there is none. | | `bestGuessUrl` | The web address of the best guess. `null` without a guess, or when the service gives no address. | ## Reasons | `reason` | What happened | | --------------- | -------------------------------------------------------------------------------- | | `notFound` | The destination has no item that matches. | | `lowConfidence` | The best match scored below your match threshold. `bestGuessCandidate` holds it. | | `regionLocked` | The match exists, but not in the user's country. | | `error` | The item failed on the way to the destination (`status` is `failed`). | | `cannotMatch` | The user looked at the candidates and refused all of them. | | `limitReached` | The user's plan allows no more items. | ## The summary ```bash curl "https://api.musicapi.com/public/transfers/6a002d4b-f95b-471f-a27d-609a42e10cdb" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` The answer has the `status`, the `counters` (`total`, `processed`, `matched`, `unmatched`, `skipped`, `failed`), `matchRatePct`, `durationSeconds`, the `source` and `target` sides and the timestamps. `matched` counts what we **added**, and `skipped` what the destination already had. A second run of a finished transfer reports almost everything as skipped and almost nothing as matched: it changed nothing. # Demoify: try Managed Transfers with a demo destination URL: https://musicapi.com/docs/managed-transfers/demoify.md > Demoify is a pretend music service that keeps what a transfer writes. Use it to see the whole hosted flow before your own service is connected. Product: MusicAPI Managed Transfers. Section: Managed Transfers. Demoify is a pretend music service by MusicAPI. It needs no account and no sign-in, it keeps what a transfer writes, and it shows the result as a small streaming app: home, library, liked tracks, albums, artists and every playlist. Nothing is written to a real music service. ## Two ways to use it ### The public demo Anybody can try it, with no MusicAPI account: ```url https://app.musicapi.com/demo/transfer ``` Pick a source. The **Sample library** needs no sign-in. A real service works too: the user's own account is only read, never changed. At the end, the user opens the transferred library in Demoify. A public demo library and the source account behind it are deleted after **7 days**. Every Demoify screen shows the date. ### Your own flow, before your service is ready A `transferring` sign-up starts with Demoify as its **Transfer destination**. Your own transfer URL then works on the first day: ```url https://app.musicapi.com/{slug}/transfer ``` Use it to check your branding, your webhooks and the Match Results API with real libraries. When your service is ready, pick it as the destination on the **Transfer Branding** page. ## What Demoify shows - Every transferred playlist, liked track, album and artist. - The unmatched items, marked in each collection. - A bar that says it is a demo, and that no real service was written to. ## What is different from a real destination - Requests to Demoify and to the Sample library are never metered. In your own flow, the requests to a real source service count as usual. - Demoify and the Sample library never show in a public service list, and the catalog search does not cover them. ## In the API | Service | Integration type | | -------------- | ---------------- | | Demoify | `demo` | | Sample library | `sample` | The `demo` account uses these two services as transfer destinations. The docs sandbox (Try it, signed out) runs the library calls on the music account that you connect. # Set up and verify webhooks URL: https://musicapi.com/docs/webhooks/set-up-and-verify.md > Add a webhook endpoint, pick the events, and check the X-MusicAPI-Signature of every delivery. Verification code in Node, Python and Go. Product: MusicAPI Managed Transfers. Section: Webhooks and alerts. Webhooks tell your backend what happens in Managed Transfers: a transfer started, progressed, finished or failed, a synced transfer ran, an account connected or lost its sign-in, a match report is ready. ## Add an endpoint 1. **Open API & webhooks** In the dashboard, open **API & webhooks** and add an endpoint. 2. **Enter the URL** A public `https` URL of your backend. URLs that point at private or internal addresses are refused, at save time and again before every delivery. 3. **Pick the events** Tick the events you want. An endpoint gets only those. 4. **Copy the signing secret** The secret shows **once**. Store it in your backend secrets. Lost it? Delete the endpoint and add a new one. 5. **Send a test event** Press **Send test event** and check your logs. See [Retries and testing](https://musicapi.com/docs/webhooks/retries-and-testing.md). ## The events | Event | When it fires | | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`transfer.created`](https://musicapi.com/docs/reference/events/transfer.created.md) | The transfer started, and the total item count is known. The first event of a transfer. | | [`transfer.progress`](https://musicapi.com/docs/reference/events/transfer.progress.md) | Items moved. At most one every 15 seconds per transfer, plus each 10% step. | | [`transfer.completed`](https://musicapi.com/docs/reference/events/transfer.completed.md) | The transfer ended. Carries the full summary and `resultsUrl`. | | [`transfer.failed`](https://musicapi.com/docs/reference/events/transfer.failed.md) | The transfer stopped and cannot continue. | | [`transfer.cancelled`](https://musicapi.com/docs/reference/events/transfer.cancelled.md) | The transfer was cancelled, for example when you [ended its session](https://musicapi.com/docs/managed-transfers/preauthorized-sessions.md#end-a-session). | | [`transfer.item_failed`](https://musicapi.com/docs/reference/events/transfer.item_failed.md) | Items of one collection could not be written. Up to 50 items per delivery. | | [`transfer.synced`](https://musicapi.com/docs/reference/events/transfer.synced.md) | A sync run of a synced transfer ended with news: what it wrote, or what waits. | | [`user.authorized`](https://musicapi.com/docs/reference/events/user.authorized.md) | An account was connected to one side of a session. | | [`user.auth_failed`](https://musicapi.com/docs/reference/events/user.auth_failed.md) | A running transfer lost an account and waits for a new sign-in (status `waitingForReauth`). Not terminal. There is no separate `transfer.reauth_required` event. | | [`match.report_ready`](https://musicapi.com/docs/reference/events/match.report_ready.md) | The unmatched-items report of a finished transfer is ready. | Each event page in the reference has the full `data` shape. ## The delivery Every delivery is a `POST` with a JSON body and three headers: - `Content-Type: application/json` - `X-MusicAPI-Event`: the event name, so you can route before you parse - `X-MusicAPI-Signature`: `t=,v1=` ```json { "id": "evt_3d1f6c0e-8a52-4c1b-9a0e-2f4b7c9d1e33", "event": "transfer.completed", "createdAt": "2026-10-16T10:15:42.101Z", "data": { "transferUuid": "6a002d4b-f95b-471f-a27d-609a42e10cdb", "status": "completedWithErrors", "totalItems": 1204, "processedItems": 1204, "matchedItems": 1131, "unmatchedItems": 31, "skippedItems": 42, "failedItems": 0, "sourceIntegrationType": "spotify", "targetIntegrationType": "tidal", "resultsUrl": "https://api.musicapi.com/public/transfers/6a002d4b-f95b-471f-a27d-609a42e10cdb/unmatched", "endUserId": "your-user-8841", "batch": null } } ``` `id` is the id of the delivery, `evt_`. A retry sends the same `id`, so dedupe by it. Two endpoints get two different ids for the same event. Deliveries queued before 2026-10-16 have no `id`. Read `data.status`, not only the event name: a `transfer.completed` can be `completed` or `completedWithErrors`. ## Route an event to your user Every event that names a transfer (every event with a `transferUuid`) also carries these fields: - `endUserId`: your own id for the person. It is the `externalUserId` you sent with the session, else the `merchantUserId` of the source login. `null` when you sent neither. - `batch`: the batch of the transfer, or `null`. A batch is one create call that made several transfers. - `sourceIntegrationType` and `targetIntegrationType`: the services of the two ends, as `transfer.created` names them. ```json { "uuid": "2b7e1f0c-9a4d-4c3e-8f21-6d5a4b3c2e10", "size": 3, "endedTransfers": 3, "complete": true, "notifyWhenDone": true, "completedTransfers": 2, "failedTransfers": 1, "matchedItems": 410, "totalItems": 432, "failedItems": 2, "progress": { "processedItems": 432, "totalItems": 432, "pct": 100 } } ``` - `size`: how many transfers the batch holds. `endedTransfers`: how many of them ended (completed, completed with errors, failed or cancelled). - `complete`: `true` when every transfer of the batch ended. The event is then the last one of the batch: send your one notice for the batch here. Two transfers that end at the same moment can both say `true`, so dedupe your notice by `batch.uuid`. - `notifyWhenDone`: the `notifyWhenDone` option of the create call, or `null` when the call did not send it. - `completedTransfers` and `failedTransfers`: the transfers that ended well, and the ones that failed or were cancelled. `matchedItems`, `totalItems` and `failedItems` are the sums over the transfers that ended well. - `progress`: how far the whole batch got, over every transfer of it (running ones too). `pct` is 0 to 100, rounded down. Use it for one progress bar per batch. The fields are additive. No field that an event had before changed. ## Why a transfer failed `transfer.failed` comes in two shapes. When the transfer ran to the end and the destination refused its playlists or items, the data is the full summary with `status`. When a step failed before the end, the data has `stage`, `error`, `errorType`, `errorName` (the class name of the error, when it has one) and no `status`. Treat `transfer.failed` as terminal in both shapes. Both shapes carry the counts (`totalItems`, `processedItems`, `matchedItems`, `unmatchedItems`, `skippedItems`, `failedItems`) and two fields you may branch on: | `failureReason` | What happened | | ------------------ | --------------------------------------------------------------------------------------- | | `notAuthenticated` | An account was signed out and could not be recovered. The user has to connect it again. | | `rateLimited` | A music service asked us to slow down too often. The same transfer would work later. | | `targetRefused` | The transfer ran to the end, but the destination refused its playlists or items. | | `unknown` | Anything else. Show a general message; `errorType` helps in a bug report. | `failureSide` is `source`, `target` or `null` (we cannot tell). Show these to your users instead of `error`: `error` is our internal text, it can change, and it is not written for end users. Like an unstable `errorType`, `errorName` is for a bug report or a dashboard, not for a branch. `transfer.cancelled` carries the same counts. ## Verify the signature `v1` is the HMAC-SHA256 of the string `t + "." + raw body`, keyed with the endpoint's signing secret, as lowercase hex. It is the same construction as Stripe's signature, with another header name. 1. Read the **raw** request body. A body that you parsed and serialized again no longer matches. 2. Split the header into `t` and `v1`. 3. Refuse a `t` that is more than **5 minutes** away from your clock. This stops a replayed delivery. 4. Compute the HMAC and compare it with `v1` in **constant time**. 5. Answer `2xx` only after the check passed. ```js import crypto from "node:crypto"; import express from "express"; const TOLERANCE_SECONDS = 300; export function verifyMusicApiSignature(header, rawBody, secret) { if (typeof header !== "string") return false; const parts = Object.fromEntries( header.split(",").map((part) => { const index = part.indexOf("="); return [part.slice(0, index).trim(), part.slice(index + 1).trim()]; }) ); const timestamp = Number(parts.t); if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) { return false; } const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex"); if (typeof parts.v1 !== "string" || Buffer.byteLength(parts.v1) !== expected.length) { return false; } return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)); } const app = express(); // The raw body, not express.json(): the signature covers the exact bytes. app.post("/webhooks/musicapi", express.raw({ type: "application/json" }), (req, res) => { const rawBody = req.body.toString("utf8"); if (!verifyMusicApiSignature(req.get("X-MusicAPI-Signature"), rawBody, process.env.MUSICAPI_WEBHOOK_SECRET)) { return res.status(400).send("bad signature"); } const delivery = JSON.parse(rawBody); // Queue the work, answer fast. A retry sends the same body again. res.status(204).end(); handleDelivery(delivery).catch(console.error); }); ``` ```python import hashlib import hmac import os import time from flask import Flask, abort, request TOLERANCE_SECONDS = 300 app = Flask(__name__) def verify_musicapi_signature(header: str | None, raw_body: bytes, secret: str) -> bool: if not header: return False parts = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part) try: timestamp = int(parts["t"]) except (KeyError, ValueError): return False if abs(time.time() - timestamp) > TOLERANCE_SECONDS: return False signed = parts["t"].encode() + b"." + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get("v1", "")) @app.post("/webhooks/musicapi") def musicapi_webhook(): raw_body = request.get_data() # the raw bytes, before any JSON parsing if not verify_musicapi_signature( request.headers.get("X-MusicAPI-Signature"), raw_body, os.environ["MUSICAPI_WEBHOOK_SECRET"] ): abort(400) delivery = request.get_json() enqueue(delivery) # answer fast; a retry sends the same body again return "", 204 ``` ```go package webhooks import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "math" "net/http" "os" "strconv" "strings" "time" ) const toleranceSeconds = 300 func VerifyMusicAPISignature(header string, rawBody []byte, secret string) bool { parts := map[string]string{} for _, part := range strings.Split(header, ",") { if key, value, ok := strings.Cut(strings.TrimSpace(part), "="); ok { parts[key] = value } } timestamp, err := strconv.ParseInt(parts["t"], 10, 64) if err != nil || math.Abs(float64(time.Now().Unix()-timestamp)) > toleranceSeconds { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(parts["t"] + ".")) mac.Write(rawBody) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(parts["v1"])) } func Handler(w http.ResponseWriter, r *http.Request) { rawBody, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "read error", http.StatusBadRequest) return } if !VerifyMusicAPISignature(r.Header.Get("X-MusicAPI-Signature"), rawBody, os.Getenv("MUSICAPI_WEBHOOK_SECRET")) { http.Error(w, "bad signature", http.StatusBadRequest) return } enqueue(r.Header.Get("X-MusicAPI-Event"), rawBody) // answer fast; a retry sends the same body w.WriteHeader(http.StatusNoContent) } ``` ## Handle deliveries well - **Answer fast.** We wait 10 seconds. Queue the work and answer `2xx` at once. - **Make the handler idempotent.** A retry sends the identical body with the same `id`, and a slow answer can arrive after we already retried. Store the `id` of each delivery you handled, and skip an `id` you already saw. - **Do not trust order.** Deliveries can arrive out of order. Read the `status` in the data, and use `createdAt` to drop an older state. - **Do not answer with a redirect.** We never follow one, and a `3xx` counts as a failed delivery. Next: [Retries and testing](https://musicapi.com/docs/webhooks/retries-and-testing.md). # Retries and testing URL: https://musicapi.com/docs/webhooks/retries-and-testing.md > What counts as a delivered webhook, the retry schedule of 5 attempts, why a redirect fails at once, and how the test event works. Product: MusicAPI Managed Transfers. Section: Webhooks and alerts. ## What counts as delivered We wait **10 seconds** for your endpoint. | Your answer | Result | | ------------------------------------------------- | -------------------------------------------------- | | Any `2xx` | Delivered. No more attempts. | | A `3xx` redirect | Failed, with no retry. We never follow a redirect. | | Any other status, a timeout, a refused connection | A failed attempt. We retry. | Only the status counts. We do not read the body of your answer. ## The retry schedule A delivery gets at most **5 attempts** in total. After a failed attempt, the next one waits `2^attempt × 30 seconds`: | Attempt | Waits before it | | ------- | --------------- | | 1 | Sent at once | | 2 | 60 seconds | | 3 | 120 seconds | | 4 | 240 seconds | | 5 | 480 seconds | After the fifth failed attempt, the delivery is marked failed and left alone. So a delivery has about 15 minutes to get through. A delivery also fails at once, with no retry, when: - you turned the endpoint off after the event was queued; - the endpoint's host now resolves to a private or internal address. Make your handler idempotent: a retry sends the identical body, with the same `id`. ## The delivery log Under **API & webhooks**, each endpoint shows its delivery log: the event, the status, the attempts, the status code and the response time of your server. The [MCP server](https://musicapi.com/docs/ai/mcp-server.md) reads the same log with `list_webhook_deliveries`. ## Send a test event The **Send test event** button queues a real `transfer.completed` delivery through the real path: the same signature, the same log. ```json { "id": "evt_5c2e8f1a-7b3d-4e9a-a1c6-0d4f2b8e6a71", "event": "transfer.completed", "createdAt": "2026-10-16T10:15:42.101Z", "data": { "test": true, "transferUuid": "00000000-0000-0000-0000-000000000000", "status": "completed", "totalItems": 100, "processedItems": 100, "matchedItems": 97, "unmatchedItems": 3, "skippedItems": 0, "failedItems": 0, "resultsUrl": "https://api.musicapi.com/public/transfers/00000000-0000-0000-0000-000000000000/unmatched", "endUserId": null, "batch": null } } ``` The data is a fixed sample with `"test": true` and a zeroed transfer uuid. Ignore deliveries with `test: true` in production logic. Two rules differ from a real delivery: - A test delivery gets **one** attempt, not five. - One test per endpoint per minute. A second press within the minute answers `429`. ## Test on your laptop Our deliveries need a public `https` URL. Expose your local server with a tunnel (for example `ngrok http 3000` or `cloudflared tunnel`), add the tunnel URL as an endpoint, and press **Send test event**. # Alert rules URL: https://musicapi.com/docs/webhooks/alert-rules.md > Get an email or a PagerDuty page when the error rate, the sign-in failure rate, the transfer throughput or a service goes bad. Metrics, windows and channels. Product: MusicAPI Managed Transfers. Section: Webhooks and alerts. An alert rule watches one metric of your account over a time window. When the metric crosses its threshold, the rule fires and notifies you once. Set rules up under **Analytics → Reliability** in the dashboard. Alert rules belong to Managed Transfers. A rule of an account with Managed Transfers off is not checked. ## Metrics | Metric | What it measures | Unit | Fires when the value is | | ---------------------- | ----------------------------------------------------------------------- | ---------------- | ----------------------- | | `destinationErrorRate` | The share of your API calls that failed in the window | percent | above the threshold | | `authFailureRate` | The share of sign-in attempts that did not end in a connected account | percent | above the threshold | | `transferThroughput` | Items your transfers really added, per minute, averaged over the window | items per minute | **below** the threshold | | `integrationDown` | Services whose every call failed in the window | count | above the threshold | The two rate metrics need at least **10 requests** in the window before they can fire. Two failed calls out of two say nothing. `transferThroughput` is checked only while a transfer of your account runs, so a quiet night does not page you. `integrationDown` counts a service only when it had enough calls, all of them failed. A rule fires when the value is strictly above (or, for throughput, strictly below) the threshold: `integrationDown` with threshold `0` fires for the first service that is down. ## Windows and checks - The window is 1 to 1,440 minutes (one day). The default is 5 minutes. - MusicAPI checks every rule **every 5 minutes**. - A rule moves between `ok` and `firing`. Only the step into `firing` notifies you. The step back to `ok` is recorded, with no message. - Any edit to what a rule measures (metric, threshold, window) puts it back to `ok`. ## Channels | Channel | Config | Status | | ----------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | `email` | `email`: the address to notify. Required. | Sends | | `pagerduty` | `routingKey`: an Events API v2 routing key. Optional when your account has a PagerDuty routing key in its settings. | Sends. A **test page** button sends a harmless test incident to your account's key. | | `slack` | `webhookUrl`: a Slack incoming webhook URL | Stored only. Nothing is sent to Slack yet. | Secrets are write-only. The dashboard and the API show them masked. An empty field on edit keeps the stored value. A rule that could reach nobody is refused: an email rule without an address, or a PagerDuty rule with no key on the rule and none on the account. ## Examples | Rule | Metric | Threshold | Window | | ---------------------------- | ---------------------- | --------- | ------ | | "More than 5% of calls fail" | `destinationErrorRate` | 5 | 15 | | "A third of sign-ins fail" | `authFailureRate` | 33 | 30 | | "Transfers stall" | `transferThroughput` | 10 | 30 | | "A service is down" | `integrationDown` | 0 | 15 | ## Set them up with an AI agent The [MusicAPI MCP server](https://musicapi.com/docs/ai/mcp-server.md) has `list_alert_rules`, `create_alert_rule`, `update_alert_rule` and `delete_alert_rule`. # Billing and usage URL: https://musicapi.com/docs/billing/billing-and-usage.md > What one billed request is, how MusicAPI Core and MusicAPI Managed Transfers are metered, the price tiers per 1,000 requests, the €500 monthly minimum, and where to see your usage. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Billing and usage. MusicAPI bills by the request, in euros, once a month. There is no seat price and no per-user price. ## What one request is The meter counts one request for each of these: | What | Counts as | | ------------------------------------------------------------------------------- | ------------------------------------------------------------- | | A library call: `/api/{integrationUserUUID}/…` | One request, failed calls included | | A sign-in that connects an account (hosted sign-in or a pre-authorized session) | One request | | A call that a managed transfer makes to a music service | One request per call. Retries of one call do not count again. | Not billed: - the [Match Results API](https://musicapi.com/docs/managed-transfers/results-and-unmatched.md); - anything on Demoify and the Sample library; - the docs sandbox. Calls from a signed-in docs session count like any other call of your account. ## Core and Managed Transfers metering A managed transfer reads the source service and writes the destination service. Your plan decides which side is billed: | Plan | Transfer requests billed | | -------------------------- | ------------------------------------------------------- | | MusicAPI Managed Transfers | Both sides: the source reads and the destination writes | | MusicAPI Core | The source side only | The destination is your own service, so on the Core plan the writes into it are not billed. Your library calls and sign-ins are billed on both plans. As a guide, one transfer makes about 60 requests on the source side and about 120 on the destination side. The real number depends on the size of the library and on the services. ## Price per 1,000 requests The tiers are cumulative within one month: the first 5 million requests are billed at the first rate, the next 20 million at the second, everything above at the third. | Monthly requests | Price per 1,000 requests | | ---------------- | ------------------------ | | 0 – 5M | €0.60 | | 5M – 25M | €0.45 | | 25M + | €0.30 | ## The monthly minimum Each month bills the greater of the metered charge and the **€500** minimum. When the metered charge is lower, the invoice adds a "Minimum monthly spend top-up" line. | Requests in the month | Metered charge | Invoice | | --------------------- | -------------------------------------- | ------------------------- | | 400,000 | 400 × €0.60 = €240 | €500 (with a €260 top-up) | | 2,000,000 | 2,000 × €0.60 = €1,200 | €1,200 | | 8,000,000 | 5,000 × €0.60 + 3,000 × €0.45 = €4,350 | €4,350 | The [pricing page](https://musicapi.com/pricing) has a calculator. ## Where to see usage - **Analytics → API usage** in the dashboard: requests over time, by endpoint, error rates and response times. - The [MCP server](https://musicapi.com/docs/ai/mcp-server.md) tool `get_usage_summary`: the billed requests of this month and last month, from the same table as the invoices. The numbers lag a few hours. ## Invoices Invoices come from Stripe, by default once a month. A different cycle, a custom tier or a custom limit is part of your agreement with us. [Contact us](https://schedule.musicapi.com/). ## Keep the bill down - Store what you read. A playlist list does not change every second. - Use `limitParam` with the service's maximum, so a full read takes fewer pages. - Send at most 20 items per write, but do send 20, not one at a time. - Use [webhooks](https://musicapi.com/docs/webhooks/set-up-and-verify.md) instead of polling. # Use MusicAPI with an AI coding agent URL: https://musicapi.com/docs/ai/ai-coding-agents.md > Give Claude Code, Cursor, Codex or Copilot the MusicAPI docs as Markdown and a ready prompt with the rules agents get wrong. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Guides for AI. AI coding agents write good MusicAPI code when they read the real docs and know a few rules. This page gives them both. ## The files for agents | File | What it holds | | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | [/llms.txt](https://musicapi.com/llms.txt) | The index of the docs in the llmstxt.org format: every page with its Markdown URL and a one-line summary. | | [/llms-full.txt](https://musicapi.com/llms-full.txt) | Every page in one file. | | [/openapi.json](https://musicapi.com/openapi.json) | The OpenAPI 3.1 spec of the partner API. The source of the API reference. | | Any page + `.md` | The Markdown of one page, for example [/docs/connect-users/start-sign-in.md](https://musicapi.com/docs/connect-users/start-sign-in.md). | Every page also has **Copy page**, **View as Markdown**, **Open in Claude** and **Open in ChatGPT** at the top. ## The prompt Paste this into your agent's project instructions (`CLAUDE.md`, `AGENTS.md`, `.cursor/rules`) or at the start of a chat. Signed in, it carries your slug and your Client ID. It never carries your Client Secret. ```markdown You integrate the MusicAPI partner API (https://api.musicapi.com). Read first: - Docs index: https://musicapi.com/llms.txt (every page as Markdown: add .md to a docs URL) - API spec: https://musicapi.com/openapi.json. Use only endpoints and fields from this spec. Do not invent any. Account values: - Account slug: {slug} - Client ID: $MUSICAPI_CLIENT_ID (public) - Client Secret: read from the env var MUSICAPI_CLIENT_SECRET on the server only Rules: 1. Secrets. The Client Secret never goes into browser code, a mobile app, a URL, a log or a repository. Basic auth (base64 of clientId:clientSecret) runs on the backend only. 2. Auth per route. - /public/* (catalog search, match, link inspect): `Authorization: Token ` is fine, also in a browser. - /api/{integrationUserUUID}/* (a user's library): Basic from the backend, or a DevToken (ES256 JWT: kid = key id from the dashboard, iss = client id, sub = the integrationUserUUID, short exp). A Client ID token does NOT work on /api. - DELETE /public/integrations/user/{uuid}, /public/transfer-sessions/*, GET /public/transfers/*: Basic only (transfer sessions also take a DevToken without sub). 3. Sign-in. Send the user to https://app.musicapi.com/{slug}?returnUrl=. The return URL gets `data64` (base64 JSON). It is not signed: confirm it on the backend with GET /public/integrations/auth/{authModel.uuid} (Basic) before you store integrationUser.uuid. 4. 412 with internalCode 14 (IntegrationUserNotAuthenticated): the user must sign in again. Show a reconnect button that opens the hosted sign-in. Do not retry the call in a loop. 5. 429: wait the seconds in Retry-After, then retry. The limit is 300 requests per minute per account, for all keys together. 6. Pagination: list answers are { results, nextParam, totalItems }. Pass nextParam back as a query parameter until it is null. limitParam is capped per service. 7. Writes: at most 20 ids per request (itemIds, entryIds). Split longer lists into chunks of 20. Removes take entryIds (the id of an entry in a playlist or in the liked tracks), not track ids. 8. Not every service supports every write. A 418 with internalCode 15 means "not supported by this service". Check https://musicapi.com/docs/get-started/services-and-features.md before you build a feature. 9. Ids are per service. To move a track to another service, find it there with POST /public/search/match. 10. Webhooks: verify X-MusicAPI-Signature (t=,v1=). v1 = HMAC-SHA256(secret, t + "." + raw body), hex. Use the raw body, compare in constant time, reject a t older than 5 minutes. Answer 2xx within 10 seconds, never with a redirect. Make the handler idempotent: retries send the same body. 11. Do not poll a running transfer. Wait for the transfer.completed or match.report_ready webhook, then read the Match Results API. ``` ## Rules agents get wrong These come up again and again in agent-written code. The prompt above covers each one. | Mistake | Correct | | ---------------------------------------------------- | ----------------------------------------------------------- | | The Client Secret in a React or mobile bundle | Basic on the backend, a DevToken with `sub` in the frontend | | `Authorization: Token` on `/api/…` | `Token` works on `/public/*` only | | A retry loop on `412` | Send the user to sign in again | | Reading only the first page | Follow `nextParam` until `null` | | 100 track ids in one write | Chunks of at most 20 | | Removing by track id | Remove by `entryId` | | Trusting `data64` as is | Confirm the sign-in on the backend | | `JSON.parse` before the signature check | Verify the raw body first | | Polling `/public/transfers/{uuid}` every few seconds | Webhooks, then one read | ## Give the agent tools, too The [MusicAPI MCP server](https://musicapi.com/docs/ai/mcp-server.md) lets the agent set up your account itself: integrations, return URLs, API keys, webhooks and alert rules. It also searches these docs. ## Ask about one page Use **Open in Claude** or **Open in ChatGPT** at the top of any page. It opens a chat that reads the Markdown of that page. # Set up your account with the MusicAPI MCP server URL: https://musicapi.com/docs/ai/mcp-server.md > Connect Claude Code, Cursor, Claude Desktop or ChatGPT to the MusicAPI MCP server, and let your AI agent set up and run your partner account. Install steps and all 26 tools. Product: MusicAPI Core and MusicAPI Managed Transfers. Section: Guides for AI. The MusicAPI MCP server lets an AI agent work on your partner account: read its setup, add integrations, set return URLs, make API keys, set up webhooks and alert rules, check usage, and search these docs. It acts as your dashboard account, after you allow it on a consent page. ```url https://api.musicapi.com/mcp ``` - Transport: Streamable HTTP. - Sign-in: OAuth 2.1 with PKCE and dynamic client registration. Your agent registers itself and opens the consent page on `app.musicapi.com`. You sign in to the dashboard as usual and press **Allow**. - Scopes: `account:read` for the read tools, `account:write` for the write and delete tools. - An access token lives 1 hour. The agent renews it with a refresh token for up to 30 days. ## Install **Claude Code** ```bash claude mcp add --transport http musicapi https://api.musicapi.com/mcp ``` Then run `/mcp` in Claude Code, pick **musicapi** and sign in. **Cursor** Add the server to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project): ```json { "mcpServers": { "musicapi": { "url": "https://api.musicapi.com/mcp" } } } ``` Cursor shows a **Needs login** button for the server. Press it to open the consent page. **Claude Desktop** Open **Settings → Connectors → Add custom connector**, name it `MusicAPI` and paste the server URL. Or add it to `claude_desktop_config.json` through the `mcp-remote` bridge: ```json { "mcpServers": { "musicapi": { "command": "npx", "args": ["-y", "mcp-remote", "https://api.musicapi.com/mcp"] } } } ``` Restart Claude Desktop. The consent page opens in your browser. **ChatGPT** 1. In ChatGPT, open **Settings → Apps & Connectors**, and turn on **Developer mode** under **Advanced settings**. 2. Choose **Create** to add a connector. 3. Name it `MusicAPI`, paste the server URL, and pick **OAuth** as the authentication. 4. Save, then sign in on the consent page. 5. In a chat, turn the connector on from the tools menu. ## Try it Ask your agent things like: - "Which music services are set up on my MusicAPI account, and which are off?" - "Add to my return URLs." - "Create a webhook for transfer.completed and match.report\_ready to , then send a test event." - "Page me when more than 5% of calls fail over 15 minutes." - "How many requests did we make this month?" ## The tools ### Read | Tool | What it does | | ------------------------- | --------------------------------------------------------------------------------------------------- | | `get_account` | Your account: name, slug, return URLs, Managed Transfers settings, evaluation status. | | `list_integrations` | The music services set up on your account, whether each is on, and which content types it may move. | | `list_api_keys` | Your API keys: each Client ID in full, each Client Secret masked. | | `list_integration_users` | The users who connected a service, newest first, 20 per page. No service tokens. | | `list_transfers` | Your Managed Transfers runs, newest first, with status and counts. | | `get_transfer` | One run: status, progress, groups and the reason when it failed. | | `list_webhooks` | Your webhook endpoints, their events and their deliveries of the last 30 days. Secrets masked. | | `list_webhook_deliveries` | The delivery log of one endpoint: event, status, attempts, your server's answer. | | `list_alert_rules` | Your alert rules and whether each one fires now. Secrets masked. | | `get_usage_summary` | Billed requests this month and last month, from the same table as the invoices. | | `list_services` | The services a user can connect, what each supports, and which the catalog search covers. | | `search_docs` | Searches these docs and answers the best pages with an excerpt. | | `get_doc_page` | Reads one docs page as Markdown, by its path or URL. | ### Write | Tool | What it does | | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `create_integration` | Adds a music service with your own developer app credentials. | | `update_integration` | Turns an integration on or off, sets its OAuth client id and secret, or its content types. | | `set_return_urls` | Replaces the whole list of Accepted Return URLs. | | `create_api_key` | Makes a Client ID and a Client Secret. The secret shows once, in the answer. | | `create_webhook` | Adds an endpoint for the events you pick. The signing secret shows once. | | `update_webhook` | Changes the URL, the events or the on/off switch of one endpoint. | | `send_test_webhook` | Sends a test `transfer.completed` to one endpoint. One per endpoint per minute. | | `create_alert_rule` | Adds a rule that emails or pages you when a metric passes its threshold. | | `update_alert_rule` | Changes one rule. | | `update_managed_transfers_settings` | Changes the branding, the custom domain and the destination of your hosted flow, the transfer emails and the attach-only browser tokens. | ### Delete These tools carry the MCP `destructiveHint`. A good agent asks you before it runs one. | Tool | What it does | | ------------------- | ------------------------------------------------------------------------------------------------------- | | `delete_api_key` | Revokes one key, both halves. Calls with it fail at once. The account's original key cannot be deleted. | | `delete_webhook` | Removes one endpoint and its delivery log. | | `delete_alert_rule` | Removes one alert rule. | ## What the server never does - No tool deletes a connected end user, and no tool reads a user's music-service tokens. - No tool shows a Client Secret or a signing secret again after it was made. ## Revoke access Open **Settings → Connected apps** in the dashboard. **Apps and AI agents** lists every app that you allowed. **Revoke** stops its tokens at once. # Exchange a docs sign-in code URL: https://musicapi.com/docs/reference/exchange-docs-code.md `POST https://api.musicapi.com/public/docs-sessions/exchange` Section: Get started. Product: MusicAPI Core and MusicAPI Managed Transfers. Auth: none. The docs site calls this after the dashboard sent the browser back with `#code=…`. A code works once and for 2 minutes. The token lives 8 hours. 20 calls per minute per IP. ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `code` | string | yes | | ## Response 200 OK | Field | Type | Required | Description | | --- | --- | --- | --- | | `token` | string | yes | Send it as `Authorization: DocsSession `. | | `expiresAt` | date-time | yes | | | `account` | DocsSessionAccount | yes | | | `account.name` | string | yes | | | `account.slug` | string | yes | | | `account.clientId` | uuid \| null | yes | Your oldest live Client ID. Never a Client Secret. | | `account.returnUrls` | string[] | yes | | | `account.isEvaluationAccount` | boolean | yes | True while the account is capped at 5 connected users. | | `account.isTransferServiceEnabled` | boolean | yes | True when MusicAPI Managed Transfers is on for the account. | | `account.enabledIntegrations` | string[] | yes | | ```json { "token": "string", "expiresAt": "2026-10-16T09:41:07.000Z", "account": { "name": "Acme Music", "slug": "acme", "clientId": "5b0c6f0e-2c8a-4d0e-9a51-7e3f1c2d9b44", "returnUrls": [ "string" ], "isEvaluationAccount": true, "isTransferServiceEnabled": true, "enabledIntegrations": [ "spotify" ] } } ``` ## Errors - 400: `invalidCode`: the code is used, expired or unknown. - 429: Too many exchanges from this IP. ## Example ```bash curl -X POST "https://api.musicapi.com/public/docs-sessions/exchange" \ -H "Content-Type: application/json" \ -d '{"code":"string"}' ``` # Start a sandbox docs session URL: https://musicapi.com/docs/reference/create-docs-sandbox-session.md `POST https://api.musicapi.com/public/docs-sessions/sandbox` Section: Get started. Product: MusicAPI Core and MusicAPI Managed Transfers. Auth: none. For "Try it" without signing in. The token lives 1 hour and acts as the `demo` account, 30 calls per minute. 30 sessions per hour per IP. Without a body the token reaches the operations marked `x-docs.sandbox: "catalog"`: the catalog reads and its own session routes, with the demo and sample users only. With `authModelUUID` the token is bound to the account that the visitor connected through the hosted sign-in of the `demo` account (`authModel.uuid` of `data64`). It then also reaches every operation marked `x-docs.sandbox: "connectedUser"`, for that connected user only: another uuid answers 404. The sign-in must have succeeded, must be at most 15 minutes old, and opens one session only. Erasing the user, `refreshAuthToken`, transfers and transfer sessions stay refused. ## Request body `application/json`. | Field | Type | Required | Description | | --- | --- | --- | --- | | `authModelUUID` | uuid | no | The `authModel.uuid` of a hosted sign-in of the `demo` account, at most 15 minutes old. Used once. | ## Response 200 OK | Field | Type | Required | Description | | --- | --- | --- | --- | | `token` | string | yes | Send it as `Authorization: DocsSession `. | | `expiresAt` | date-time | yes | | | `account` | DocsSessionAccount | yes | | | `account.name` | string | yes | | | `account.slug` | string | yes | | | `account.clientId` | uuid \| null | yes | Your oldest live Client ID. Never a Client Secret. | | `account.returnUrls` | string[] | yes | | | `account.isEvaluationAccount` | boolean | yes | True while the account is capped at 5 connected users. | | `account.isTransferServiceEnabled` | boolean | yes | True when MusicAPI Managed Transfers is on for the account. | | `account.enabledIntegrations` | string[] | yes | | | `connectedUser` | DocsConnectedUser \| null | yes | The connected account of a bound session. `null` without `authModelUUID`. | | `connectedUser.uuid` | uuid | yes | The `integrationUserUUID` of the account. | | `connectedUser.integrationType` | string | yes | The service, for example `spotify`. | | `connectedUser.name` | string \| null | yes | | | `connectedUser.imageUrl` | string \| null | yes | | ```json { "token": "string", "expiresAt": "2026-10-16T09:41:07.000Z", "account": { "name": "Acme Music", "slug": "acme", "clientId": "5b0c6f0e-2c8a-4d0e-9a51-7e3f1c2d9b44", "returnUrls": [ "string" ], "isEvaluationAccount": true, "isTransferServiceEnabled": true, "enabledIntegrations": [ "spotify" ] }, "connectedUser": { "uuid": "5b0c6f0e-2c8a-4d0e-9a51-7e3f1c2d9b44", "integrationType": "string", "name": "string", "imageUrl": "string" } } ``` ## Errors - 400: `invalidAuthModel`: the sign-in is unknown, failed, of another account, older than 15 minutes, has no connected user, or already opened a session. Connect the account again. - 429: Too many sandbox sessions from this IP. ## Example ```bash curl -X POST "https://api.musicapi.com/public/docs-sessions/sandbox" \ -H "Content-Type: application/json" \ -d '{"authModelUUID":"5b0c6f0e-2c8a-4d0e-9a51-7e3f1c2d9b44"}' ``` # Get the current docs session URL: https://musicapi.com/docs/reference/get-docs-session.md `GET https://api.musicapi.com/public/docs-sessions/current` Section: Get started. Product: MusicAPI Core and MusicAPI Managed Transfers. Auth: DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. ## Response 200 OK | Field | Type | Required | Description | | --- | --- | --- | --- | | `expiresAt` | date-time | yes | | | `kind` | string | yes | One of: partner, sandbox. | | `account` | DocsSessionAccount | yes | | | `account.name` | string | yes | | | `account.slug` | string | yes | | | `account.clientId` | uuid \| null | yes | Your oldest live Client ID. Never a Client Secret. | | `account.returnUrls` | string[] | yes | | | `account.isEvaluationAccount` | boolean | yes | True while the account is capped at 5 connected users. | | `account.isTransferServiceEnabled` | boolean | yes | True when MusicAPI Managed Transfers is on for the account. | | `account.enabledIntegrations` | string[] | yes | | | `connectedUser` | DocsConnectedUser \| null | no | Sandbox sessions only: the connected account of a bound session, else `null`. | | `connectedUser.uuid` | uuid | yes | The `integrationUserUUID` of the account. | | `connectedUser.integrationType` | string | yes | The service, for example `spotify`. | | `connectedUser.name` | string \| null | yes | | | `connectedUser.imageUrl` | string \| null | yes | | ```json { "expiresAt": "2026-10-16T09:41:07.000Z", "kind": "partner", "account": { "name": "Acme Music", "slug": "acme", "clientId": "5b0c6f0e-2c8a-4d0e-9a51-7e3f1c2d9b44", "returnUrls": [ "string" ], "isEvaluationAccount": true, "isTransferServiceEnabled": true, "enabledIntegrations": [ "spotify" ] }, "connectedUser": { "uuid": "5b0c6f0e-2c8a-4d0e-9a51-7e3f1c2d9b44", "integrationType": "string", "name": "string", "imageUrl": "string" } } ``` ## Errors - 401: `docsSessionEnded`: the token expired or was revoked. Sign in again. ## Example ```bash curl https://api.musicapi.com/public/docs-sessions/current \ -H "Authorization: DocsSession $MUSICAPI_DOCS_SESSION" ``` # List your connected users (docs picker) URL: https://musicapi.com/docs/reference/list-docs-integration-users.md `GET https://api.musicapi.com/public/docs-sessions/current/integration-users` Section: Get started. Product: MusicAPI Core and MusicAPI Managed Transfers. Auth: DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. The connected users of your account, newest first, 20 per page, without credentials. A sandbox session sees only the demo and sample users. A sandbox session bound to a connected account sees only that account. ## Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationType` | string | no | | | `cursor` | string | no | | ## Response 200 OK | Field | Type | Required | Description | | --- | --- | --- | --- | | `items` | object[] | yes | | | `items[].uuid` | uuid | yes | | | `items[].integrationType` | string | yes | | | `items[].name` | string \| null | yes | | | `items[].imageUrl` | string \| null | yes | | | `items[].createdAt` | date-time | yes | | | `nextCursor` | string \| null | yes | Send it as `cursor` for the next page. Null on the last page. | ```json { "items": [ { "uuid": "5b0c6f0e-2c8a-4d0e-9a51-7e3f1c2d9b44", "integrationType": "spotify", "name": "Mica Nowak", "imageUrl": "string", "createdAt": "2026-10-16T09:41:07.000Z" } ], "nextCursor": "string" } ``` ## Errors - 401: `docsSessionEnded`: sign in again. ## Example ```bash curl https://api.musicapi.com/public/docs-sessions/current/integration-users \ -H "Authorization: DocsSession $MUSICAPI_DOCS_SESSION" ``` # Sign out of the docs URL: https://musicapi.com/docs/reference/delete-docs-session.md `DELETE https://api.musicapi.com/public/docs-sessions/current` Section: Get started. Product: MusicAPI Core and MusicAPI Managed Transfers. Auth: DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. ## Response 204 The session is revoked. ## Errors - 401: `docsSessionEnded`: the session already ended. ## Example ```bash curl -X DELETE "https://api.musicapi.com/public/docs-sessions/current" \ -H "Authorization: DocsSession $MUSICAPI_DOCS_SESSION" ``` # Start a sign-in (service picker) URL: https://musicapi.com/docs/reference/start-sign-in.md `GET https://app.musicapi.com/{slug}` Section: Connect your users. Product: MusicAPI Core and MusicAPI Managed Transfers. Auth: none. This is a page for the user’s browser, not a JSON call. A page, not a JSON call. Send the user's browser here. The user picks one of the services you turned on, signs in, and comes back to `returnUrl` with the result (see "Handle the callback"). ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | yes | Your account slug (Dashboard, Settings). | ## Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `returnUrl` | uri | yes | Where the user comes back to. It must be one of your Return URLs (Dashboard, Settings). | | `requestOneTimeToken` | boolean | no | Add a one-time token to the callback. With it, your backend or app reads the raw service tokens once. Default: `false`. | | `uniqueId` | uuid | no | A UUID v4 that you make. Poll `GET /app/integrations/byUniqueId/{uniqueId}` with it. | | `forwardFailures` | boolean | no | Send the user back to `returnUrl` at once when the sign-in fails. Default: `false`. | | `merchantUserId` | string | no | Your own id for this user. It is stored on the connected user and in the usage data. | ## Response 200 The service picker page (HTML). ## Example Send the user’s browser to: ```text https://app.musicapi.com/{slug}?returnUrl={returnUrl} ``` # Start a sign-in with one service URL: https://musicapi.com/docs/reference/start-service-sign-in.md `GET https://app.musicapi.com/{slug}/{integrationType}/auth` Section: Connect your users. Product: MusicAPI Core and MusicAPI Managed Transfers. Auth: none. This is a page for the user’s browser, not a JSON call. A page, not a JSON call. Skips the service picker and sends the user straight to the sign-in of one service. This works even for a service that is off in your dashboard. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | yes | Your account slug. | | `integrationType` | string | yes | The service id, for example `spotify`, `appleMusic`, `tidal`. | ## Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `returnUrl` | uri | yes | Where the user comes back to. It must be one of your Return URLs (Dashboard, Settings). | | `requestOneTimeToken` | boolean | no | Add a one-time token to the callback. With it, your backend or app reads the raw service tokens once. Default: `false`. | | `uniqueId` | uuid | no | A UUID v4 that you make. Poll `GET /app/integrations/byUniqueId/{uniqueId}` with it. | | `forwardFailures` | boolean | no | Send the user back to `returnUrl` at once when the sign-in fails. Default: `false`. | | `merchantUserId` | string | no | Your own id for this user. It is stored on the connected user and in the usage data. | ## Response 302 A redirect to the service's sign-in page. ## Example Send the user’s browser to: ```text https://app.musicapi.com/{slug}/{integrationType}/auth?returnUrl={returnUrl} ``` # Poll the sign-in status URL: https://musicapi.com/docs/reference/get-sign-in-status.md `GET https://api.musicapi.com/app/integrations/byUniqueId/{uniqueId}` Section: Connect your users. Product: MusicAPI Core and MusicAPI Managed Transfers. Auth: none. Poll this while a sign-in that you started with `uniqueId` runs. It needs no credentials: the `uniqueId` you chose is the secret. Send `tokenOnly=1` from a browser, so the answer never holds music-service credentials. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `uniqueId` | uuid | yes | The UUID v4 that you passed as `uniqueId` when you started the sign-in. | ## Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `tokenOnly` | boolean | no | Leave `authData` out of the answer. Default: `false`. | ## Response 200 OK | Field | Type | Required | Description | | --- | --- | --- | --- | | `authModel` | AuthModel | yes | | | `authModel.uuid` | uuid | yes | | | `authModel.status` | string | yes | One of: initialised, success, error. | | `authModel.error` | string \| null | yes | | | `authModel.errorType` | string \| null | yes | One of: accessDenied, notAllowedInYourCountry, missingScopes, deniedSubscriptionTier, null. | | `oneTimeToken` | string \| null | yes | A one-time token for this login, when the login asked for one (`requestOneTimeToken=true`). | | `integrationUser` | IntegrationUserWithAuthData \| null | yes | The connected user once the login succeeded. `authData` is present only when the login asked for a one-time token, the call has no `tokenOnly=1`, and the return URL is not an http(s) URL. | | `integrationUser.uuid` | uuid | yes | The integrationUserUUID. Use it in every `/api/{integrationUserUUID}/...` call. | | `integrationUser.integrationUserId` | string | yes | The user's id on the music service. | | `integrationUser.name` | string \| null | yes | | | `integrationUser.email` | string \| null | yes | | | `integrationUser.imageUrl` | string \| null | yes | | | `integrationUser.country` | string \| null | yes | ISO 3166-1 alpha-2 country of the account, when the service tells us. | | `integrationUser.authData` | object | no | The raw music-service credentials, without the refresh token. Only in answers to a Client Secret (Basic) or one-time-token call. | | `integrationUser.authDataExpiresAt` | number \| null | no | When `authData` expires, in milliseconds since the epoch. | ```json { "authModel": { "uuid": "5b0c6f0e-2c8a-4d0e-9a51-7e3f1c2d9b44", "status": "initialised", "error": "string", "errorType": "accessDenied" }, "oneTimeToken": "string", "integrationUser": { "uuid": "3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11", "integrationUserId": "mica.nowak", "name": "Mica Nowak", "email": "mica@example.com", "imageUrl": "https://images.example.com/mica.jpg", "country": "PL", "authData": {}, "authDataExpiresAt": 0 } } ``` ## Errors - 404: No sign-in with this uniqueId. ## Example ```bash curl https://api.musicapi.com/app/integrations/byUniqueId/{uniqueId} ``` # Get a hosted sign-in attempt URL: https://musicapi.com/docs/reference/get-hosted-sign-in.md `GET https://api.musicapi.com/app/integrations/{authModelUUID}` Section: Connect your users. Product: MusicAPI Core and MusicAPI Managed Transfers. Auth: none. The state of one sign-in attempt, as the hosted pages read it. The `authModelUUID` comes back in the callback. It needs no credentials and never returns `authData`. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `authModelUUID` | uuid | yes | | ## Response 200 OK | Field | Type | Required | Description | | --- | --- | --- | --- | | `forwardFailures` | boolean | no | | | `authUrl` | string | yes | The music service's own sign-in URL for this login. | | `returnUrl` | string | yes | Where the browser goes when the login ends. | | `oneTimeToken` | string \| null | no | | | `authModel` | AuthModel | yes | | | `authModel.uuid` | uuid | yes | | | `authModel.status` | string | yes | One of: initialised, success, error. | | `authModel.error` | string \| null | yes | | | `authModel.errorType` | string \| null | yes | One of: accessDenied, notAllowedInYourCountry, missingScopes, deniedSubscriptionTier, null. | | `integrationUser` | UserResponse \| null | no | | | `integrationUser.uuid` | uuid | yes | The integrationUserUUID. Use it in every `/api/{integrationUserUUID}/...` call. | | `integrationUser.integrationUserId` | string | yes | The user's id on the music service. | | `integrationUser.name` | string \| null | yes | | | `integrationUser.email` | string \| null | yes | | | `integrationUser.imageUrl` | string \| null | yes | | | `integrationUser.country` | string \| null | yes | ISO 3166-1 alpha-2 country of the account, when the service tells us. | | `user` | PublicPartner | yes | | | `user.id` | number | yes | | | `user.slug` | string | yes | | | `user.name` | string | yes | | | `user.logoFile` | object \| null | no | | | `user.base64Logo` | string \| null | no | | | `user.logoSquareFile` | object \| null | no | | | `integration` | PublicIntegration | yes | One music service that the partner turned on, with its public settings. | | `integration.type` | string | yes | | ```json { "forwardFailures": true, "authUrl": "string", "returnUrl": "string", "oneTimeToken": "string", "authModel": { "uuid": "5b0c6f0e-2c8a-4d0e-9a51-7e3f1c2d9b44", "status": "initialised", "error": "string", "errorType": "accessDenied" }, "integrationUser": { "uuid": "3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11", "integrationUserId": "mica.nowak", "name": "Mica Nowak", "email": "mica@example.com", "imageUrl": "https://images.example.com/mica.jpg", "country": "PL" }, "user": { "id": 0, "slug": "acme", "name": "Acme Music", "logoFile": {}, "base64Logo": "string", "logoSquareFile": {} }, "integration": { "type": "spotify" } } ``` ## Errors - 404: No sign-in attempt with this uuid. ## Example ```bash curl https://api.musicapi.com/app/integrations/{authModelUUID} ``` # Get the result of a sign-in URL: https://musicapi.com/docs/reference/get-sign-in-result.md `GET https://api.musicapi.com/public/integrations/auth/{authModelUUID}` Section: Connect your users. Product: MusicAPI Core and MusicAPI Managed Transfers. Auth: Basic, Token, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. Reads one sign-in attempt of your account and the user it connected. `authData` is in the answer only for a Client Secret call or with a valid `oneTimeToken` from the callback. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `authModelUUID` | string | yes | | ## Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `oneTimeToken` | string | no | The one-time token from the callback. It must belong to your account and to this sign-in. | ## Response 200 OK | Field | Type | Required | Description | | --- | --- | --- | --- | | `authModel` | AuthModel | yes | | | `authModel.uuid` | uuid | yes | | | `authModel.status` | string | yes | One of: initialised, success, error. | | `authModel.error` | string \| null | yes | | | `authModel.errorType` | string \| null | yes | One of: accessDenied, notAllowedInYourCountry, missingScopes, deniedSubscriptionTier, null. | | `integrationUser` | IntegrationUserWithAuthData \| null | yes | | | `integrationUser.uuid` | uuid | yes | The integrationUserUUID. Use it in every `/api/{integrationUserUUID}/...` call. | | `integrationUser.integrationUserId` | string | yes | The user's id on the music service. | | `integrationUser.name` | string \| null | yes | | | `integrationUser.email` | string \| null | yes | | | `integrationUser.imageUrl` | string \| null | yes | | | `integrationUser.country` | string \| null | yes | ISO 3166-1 alpha-2 country of the account, when the service tells us. | | `integrationUser.authData` | object | no | The raw music-service credentials, without the refresh token. Only in answers to a Client Secret (Basic) or one-time-token call. | | `integrationUser.authDataExpiresAt` | number \| null | no | When `authData` expires, in milliseconds since the epoch. | ```json { "authModel": { "uuid": "5b0c6f0e-2c8a-4d0e-9a51-7e3f1c2d9b44", "status": "initialised", "error": "string", "errorType": "accessDenied" }, "integrationUser": { "uuid": "3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11", "integrationUserId": "mica.nowak", "name": "Mica Nowak", "email": "mica@example.com", "imageUrl": "https://images.example.com/mica.jpg", "country": "PL", "authData": {}, "authDataExpiresAt": 0 } } ``` ## Errors - 403: The one-time token belongs to another account or another sign-in. - 404: Your account has no sign-in attempt with this uuid. ## Example ```bash curl https://api.musicapi.com/public/integrations/auth/{authModelUUID} \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Get a connected user URL: https://musicapi.com/docs/reference/get-integration-user.md `GET https://api.musicapi.com/public/integrations/user/{integrationUserUUID}` Section: Connect your users. Product: MusicAPI Core and MusicAPI Managed Transfers. Auth: Basic, Token, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. Reads one connected user of your account. With Client Secret (Basic) authentication the answer also holds the raw service tokens (`authData`), refreshed when they expire within a minute. Every other credential gets the profile only. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | ## Response 200 OK | Field | Type | Required | Description | | --- | --- | --- | --- | | `integrationUser` | IntegrationUserWithAuthData | yes | | | `integrationUser.uuid` | uuid | yes | The integrationUserUUID. Use it in every `/api/{integrationUserUUID}/...` call. | | `integrationUser.integrationUserId` | string | yes | The user's id on the music service. | | `integrationUser.name` | string \| null | yes | | | `integrationUser.email` | string \| null | yes | | | `integrationUser.imageUrl` | string \| null | yes | | | `integrationUser.country` | string \| null | yes | ISO 3166-1 alpha-2 country of the account, when the service tells us. | | `integrationUser.authData` | object | no | The raw music-service credentials, without the refresh token. Only in answers to a Client Secret (Basic) or one-time-token call. | | `integrationUser.authDataExpiresAt` | number \| null | no | When `authData` expires, in milliseconds since the epoch. | | `integration` | object | yes | | | `integration.type` | string | yes | The music service of this user. | ```json { "integrationUser": { "uuid": "3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11", "integrationUserId": "mica.nowak", "name": "Mica Nowak", "email": "mica@example.com", "imageUrl": "https://images.example.com/mica.jpg", "country": "PL", "authData": {}, "authDataExpiresAt": 0 }, "integration": { "type": "spotify" } } ``` ## Errors - 401: The tokens could not be refreshed. Send the user to sign in again. - 404: Your account has no connected user with this uuid. ## Example ```bash curl https://api.musicapi.com/public/integrations/user/{integrationUserUUID} \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Get the user's service profile ("Me") URL: https://musicapi.com/docs/reference/get-user-profile.md `GET https://api.musicapi.com/api/{integrationUserUUID}/users/profile` Section: Connect your users. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. [Check which services support returning user information](/docs/get-started/services-and-features) This endpoint returns information about user that was fetched from Music Service. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | ## Response 200 OK | Field | Type | Required | Description | | --- | --- | --- | --- | | `uuid` | uuid | yes | The integrationUserUUID. Use it in every `/api/{integrationUserUUID}/...` call. | | `integrationUserId` | string | yes | The user's id on the music service. | | `name` | string \| null | yes | | | `email` | string \| null | yes | | | `imageUrl` | string \| null | yes | | | `country` | string \| null | yes | ISO 3166-1 alpha-2 country of the account, when the service tells us. | ```json { "uuid": "3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11", "integrationUserId": "mica.nowak", "name": "Mica Nowak", "email": "mica@example.com", "imageUrl": "https://images.example.com/mica.jpg", "country": "PL" } ``` ## Errors - 404: Your account has no connected user with this uuid. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). ## Example ```bash curl https://api.musicapi.com/api/{integrationUserUUID}/users/profile \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Force a token refresh URL: https://musicapi.com/docs/reference/refresh-integration-user-token.md `POST https://api.musicapi.com/public/integrations/refreshAuthToken` Section: Connect your users. Product: MusicAPI Core and MusicAPI Managed Transfers. Auth: Basic, Token, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. Refreshes the music-service tokens of one connected user now. With Client Secret authentication the answer holds the new `authData`. ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | | ## Response 200 OK | Field | Type | Required | Description | | --- | --- | --- | --- | | `integrationUser` | IntegrationUserWithAuthData | yes | | | `integrationUser.uuid` | uuid | yes | The integrationUserUUID. Use it in every `/api/{integrationUserUUID}/...` call. | | `integrationUser.integrationUserId` | string | yes | The user's id on the music service. | | `integrationUser.name` | string \| null | yes | | | `integrationUser.email` | string \| null | yes | | | `integrationUser.imageUrl` | string \| null | yes | | | `integrationUser.country` | string \| null | yes | ISO 3166-1 alpha-2 country of the account, when the service tells us. | | `integrationUser.authData` | object | no | The raw music-service credentials, without the refresh token. Only in answers to a Client Secret (Basic) or one-time-token call. | | `integrationUser.authDataExpiresAt` | number \| null | no | When `authData` expires, in milliseconds since the epoch. | ```json { "integrationUser": { "uuid": "3f0b8c1e-6a4d-4c7a-9a55-2f1e7d9b0c11", "integrationUserId": "mica.nowak", "name": "Mica Nowak", "email": "mica@example.com", "imageUrl": "https://images.example.com/mica.jpg", "country": "PL", "authData": {}, "authDataExpiresAt": 0 } } ``` ## Errors - 401: The refresh failed. Send the user to sign in again. - 404: Your account has no connected user with this uuid. ## Example ```bash curl -X POST "https://api.musicapi.com/public/integrations/refreshAuthToken" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"integrationUserUUID":"5b0c6f0e-2c8a-4d0e-9a51-7e3f1c2d9b44"}' ``` # Delete integration user URL: https://musicapi.com/docs/reference/delete-integration-user.md `DELETE https://api.musicapi.com/public/integrations/user/{integrationUserUUID}` Section: Connect your users. Product: MusicAPI Core and MusicAPI Managed Transfers. Auth: Basic (any one of them). Use Basic with your Client ID and Client Secret from your server only. Deletes a connected user, for example when they delete their account in your app. Removes the music service tokens, the user's name, email and profile image, and the copy of their library. Idempotent: deleting a user you already deleted returns 204 again. Requires Client Secret authentication, so call it only from your backend. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | ## Response 204 The user is deleted. The response has no body. ## Errors - 400: userModelUUID is not a valid UUID. - 401: The request has no credentials. - 403: The request does not use Client Secret authentication. - 404: Your account has no integration user with this UUID. ## Example ```bash curl -X DELETE "https://api.musicapi.com/public/integrations/user/{integrationUserUUID}" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Get your public sign-in profile URL: https://musicapi.com/docs/reference/get-partner-profile.md `GET https://api.musicapi.com/app/users/{slug}` Section: Connect your users. Product: MusicAPI Core and MusicAPI Managed Transfers. Auth: none. What the hosted sign-in pages show for your account: name, logos and the services you turned on. With `returnUrl` it also checks that URL against your Return URLs (400 when it does not match). ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `slug` | string | yes | | ## Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `returnUrl` | string | no | | ## Response 200 OK | Field | Type | Required | Description | | --- | --- | --- | --- | | `user` | PublicPartner | yes | | | `user.id` | number | yes | | | `user.slug` | string | yes | | | `user.name` | string | yes | | | `user.logoFile` | object \| null | no | | | `user.base64Logo` | string \| null | no | | | `user.logoSquareFile` | object \| null | no | | | `integrations` | PublicIntegration[] | yes | | | `integrations[].type` | string | yes | | | `returnUrl` | string | no | The `returnUrl` you sent, after it passed the check against your Return URLs. | ```json { "user": { "id": 0, "slug": "acme", "name": "Acme Music", "logoFile": {}, "base64Logo": "string", "logoSquareFile": {} }, "integrations": [ { "type": "spotify" } ], "returnUrl": "string" } ``` ## Errors - 400: `returnUrl` is not one of your Return URLs. - 404: No account with this slug. ## Example ```bash curl https://api.musicapi.com/app/users/{slug} ``` # Get user's playlists URL: https://musicapi.com/docs/reference/list-playlists.md `GET https://api.musicapi.com/api/{integrationUserUUID}/playlists` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint fetches owned & followed playlists from Music Service. As Amazon & Boomplay return data in separate endpoints, we combine the data from 2 sources (owned & liked playlist). So don't expect proper sorting for those. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | ## Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `nextParam` | string | no | Parameter returned in the response responsible for pagination. | | `limitParam` | number | no | Specifies the maximum number of results per request, adjusted based on the limitations of the respective music service. Ignored if the service doesn't support such a limit. | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `results` | Playlist[] | yes | | | `results[].name` | string | yes | | | `results[].id` | string | yes | | | `results[].totalItems` | number \| null | yes | | | `results[].imageUrl` | string \| null | yes | | | `results[].dateAdded` | string \| null | no | | | `results[].isOwner` | boolean | yes | | | `nextParam` | string \| null | yes | | | `totalItems` | number \| null | yes | | ```json { "results": [ { "name": "Viva Playlist", "id": "1", "totalItems": 25, "imageUrl": "https://www.example-image.com/978263478234", "dateAdded": "2024-10-16T07:52:07.000Z", "isOwner": true } ], "nextParam": "eyJvZmZzZXQiOjEwfQ==", "totalItems": 25 } ``` ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl https://api.musicapi.com/api/{integrationUserUUID}/playlists \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Create playlist URL: https://musicapi.com/docs/reference/create-playlist.md `POST https://api.musicapi.com/api/{integrationUserUUID}/playlists` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint creates playlist on Music Service. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | | | `description` | string | no | | | `visibility` | string | no | One of: public, private. | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | | | `id` | string | yes | | | `totalItems` | number \| null | yes | | | `imageUrl` | string \| null | yes | | | `dateAdded` | string \| null | no | | | `isOwner` | boolean | yes | | ```json { "name": "Viva Playlist", "id": "1", "totalItems": 25, "imageUrl": "https://www.example-image.com/978263478234", "dateAdded": "2024-10-16T07:52:07.000Z", "isOwner": true } ``` ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl -X POST "https://api.musicapi.com/api/{integrationUserUUID}/playlists" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"name":"Viva Loca New","description":"Description of your playlist","visibility":"public"}' ``` # Get playlist URL: https://musicapi.com/docs/reference/get-playlist.md `GET https://api.musicapi.com/api/{integrationUserUUID}/playlists/{playlistId}` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint fetches specified playlist and returns information about it. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | | `playlistId` | string | yes | ID of the playlist | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | yes | | | `id` | string | yes | | | `totalItems` | number \| null | yes | | | `imageUrl` | string \| null | yes | | | `dateAdded` | string \| null | no | | | `isOwner` | boolean | yes | | ```json { "name": "Viva Playlist", "id": "1", "totalItems": 25, "imageUrl": "https://www.example-image.com/978263478234", "dateAdded": "2024-10-16T07:52:07.000Z", "isOwner": true } ``` ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl https://api.musicapi.com/api/{integrationUserUUID}/playlists/{playlistId} \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Update playlist URL: https://musicapi.com/docs/reference/update-playlist.md `PUT https://api.musicapi.com/api/{integrationUserUUID}/playlists/{playlistId}` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. Change a playlist's name, description and public/private state (if available). The user must own the playlist. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | | `playlistId` | string | yes | ID of the playlist | ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | no | | | `description` | string | no | | | `visibility` | string | no | One of: public, private. | ## Response 200 ok ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl -X PUT "https://api.musicapi.com/api/{integrationUserUUID}/playlists/{playlistId}" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"name":"Viva Loca New","description":"Description of your playlist","visibility":"public"}' ``` # Remove playlist URL: https://musicapi.com/docs/reference/delete-playlist.md `DELETE https://api.musicapi.com/api/{integrationUserUUID}/playlists/{playlistId}` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint removes a playlist. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | | `playlistId` | string | yes | ID of the playlist | ## Response 200 ok ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl -X DELETE "https://api.musicapi.com/api/{integrationUserUUID}/playlists/{playlistId}" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Get playlist items URL: https://musicapi.com/docs/reference/list-playlist-items.md `GET https://api.musicapi.com/api/{integrationUserUUID}/playlists/{playlistId}/items` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint fetches playlist items from Music Service. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | | `playlistId` | string | yes | ID of the playlist | ## Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `nextParam` | string | no | Parameter returned in the response responsible for pagination. | | `limitParam` | number | no | Specifies the maximum number of results per request, adjusted based on the limitations of the respective music service. Ignored if the service doesn't support such a limit. | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `results` | Track[] | yes | | | `results[].id` | string | yes | | | `results[].entryId` | string \| null | yes | Identifier associated with a tracks within a specific collection. It is used to identify a particular instance of a track within a playlist or a user's library. Track entry id can be obtained during fetching playlist items or user's library. | | `results[].name` | string | yes | | | `results[].isrc` | string \| null | yes | | | `results[].duration` | number \| null | yes | | | `results[].imageUrl` | string \| null | yes | | | `results[].previewUrl` | string \| null | yes | | | `results[].album` | object \| null | yes | | | `results[].album.name` | string | yes | | | `results[].album.id` | string \| null | yes | | | `results[].album.totalItems` | number \| null | yes | | | `results[].album.artists` | ArtistSimplified[] \| null | yes | | | `results[].album.artists.id` | string \| null | yes | | | `results[].album.artists.entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `results[].album.artists.name` | string | yes | | | `results[].album.artists.imageUrl` | string \| null | yes | | | `results[].album.artists.followers` | number \| null | yes | | | `results[].artists` | ArtistSimplified[] \| null | yes | | | `results[].artists.id` | string \| null | yes | | | `results[].artists.entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `results[].artists.name` | string | yes | | | `results[].artists.imageUrl` | string \| null | yes | | | `results[].artists.followers` | number \| null | yes | | | `results[].isAvailable` | boolean \| null | no | Indicates whether the track is currently available for playback on the music service. For Spotify: false for local files or greyed-out tracks (copyright lost), true for playable tracks, null when unknown. | | `results[].dateAdded` | string \| null | no | | | `nextParam` | string \| null | yes | | | `totalItems` | number \| null | yes | | ```json { "results": [ { "id": "2nLtzopw4rPReszdYBJU6h", "entryId": "2nLtzopw4rPReszdYBJU6h", "name": "Numb", "isrc": "USWB10300474", "duration": 185586, "imageUrl": "https://i.scdn.co/image/ab67616d0000b273b4ad7ebaf4575f120eb3f193", "previewUrl": "https://p.scdn.co/mp3-preview/15e1178c9eb7f626ac1112ad8f56eccbec2cd6e5?cid=d3a370919fbb4853a2c037d9d92da680", "album": null, "artists": [ null ], "isAvailable": true, "dateAdded": "2024-10-16T07:52:07.000Z" } ], "nextParam": "eyJvZmZzZXQiOjEwfQ==", "totalItems": 25 } ``` ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl https://api.musicapi.com/api/{integrationUserUUID}/playlists/{playlistId}/items \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Add items to playlist URL: https://musicapi.com/docs/reference/add-playlist-items.md `POST https://api.musicapi.com/api/{integrationUserUUID}/playlists/{playlistId}/items` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. Add items to a user's playlist. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | | `playlistId` | string | yes | ID of the playlist | ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `itemIds` | string[] | yes | Track IDs that are retrievable through track searches or when fetching playlist items. A maximum of 20 IDs can be sent in one request. | ## Response 200 ok ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl -X POST "https://api.musicapi.com/api/{integrationUserUUID}/playlists/{playlistId}/items" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"itemIds":["42YpD6LQura7bGOtntatQw"]}' ``` # Update playlist items URL: https://musicapi.com/docs/reference/move-playlist-items.md `PUT https://api.musicapi.com/api/{integrationUserUUID}/playlists/{playlistId}/items` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. [Check which services support updating playlist items](/docs/get-started/services-and-features) Modify the order of items within a user's playlist based on the parameters provided in the request body. The reordering process starts from the position specified by `rangeStart` and encompasses the next `rangeLength` items. The destination for these reordered items is determined by the `insertBefore` property (starts with 0). Examples: - To move the first two items to the end of a playlist containing 10 items, set `rangeStart` as 0, `rangeLength` to 2 and `insertBefore` as 10. - To move the last item to the start of the playlist containing 10 items, set `rangeStart` to 9, `rangeLength` to 1 and `insertBefore` to 0. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | | `playlistId` | string | yes | ID of the playlist | ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `rangeStart` | number | yes | The position of the first item to be reordered | | `rangeLength` | number | yes | The amount of items to be reordered | | `insertBefore` | number | yes | The position where the items should be inserted | ## Response 200 ok ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl -X PUT "https://api.musicapi.com/api/{integrationUserUUID}/playlists/{playlistId}/items" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"rangeStart":1,"rangeLength":3,"insertBefore":10}' ``` # Remove playlist items URL: https://musicapi.com/docs/reference/remove-playlist-items.md `DELETE https://api.musicapi.com/api/{integrationUserUUID}/playlists/{playlistId}/items` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. [Check which services support removing playlist items](/docs/get-started/services-and-features) Remove items from a user's playlist. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | | `playlistId` | string | yes | ID of the playlist | ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entryIds` | string[] | yes | Identifiers associated with a tracks within a specific collection. It is used to identify a particular instance of a track within a playlist or a user's library. Track entry id can be obtained during fetching playlist items or user's library. A maximum of 20 IDs can be sent in one request. | ## Response 200 ok ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl -X DELETE "https://api.musicapi.com/api/{integrationUserUUID}/playlists/{playlistId}/items" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"entryIds":["42YpD6LQura7bGOtntatQw"]}' ``` # Get liked tracks URL: https://musicapi.com/docs/reference/list-liked-tracks.md `GET https://api.musicapi.com/api/{integrationUserUUID}/liked/tracks` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. [Check which services support returning ISRC and preview url](/docs/get-started/services-and-features) This endpoint fetches tracks liked by the user. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | ## Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `nextParam` | string | no | Parameter returned in the response responsible for pagination. | | `limitParam` | number | no | Specifies the maximum number of results per request, adjusted based on the limitations of the respective music service. Ignored if the service doesn't support such a limit. | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `results` | Track[] | yes | | | `results[].id` | string | yes | | | `results[].entryId` | string \| null | yes | Identifier associated with a tracks within a specific collection. It is used to identify a particular instance of a track within a playlist or a user's library. Track entry id can be obtained during fetching playlist items or user's library. | | `results[].name` | string | yes | | | `results[].isrc` | string \| null | yes | | | `results[].duration` | number \| null | yes | | | `results[].imageUrl` | string \| null | yes | | | `results[].previewUrl` | string \| null | yes | | | `results[].album` | object \| null | yes | | | `results[].album.name` | string | yes | | | `results[].album.id` | string \| null | yes | | | `results[].album.totalItems` | number \| null | yes | | | `results[].album.artists` | ArtistSimplified[] \| null | yes | | | `results[].album.artists.id` | string \| null | yes | | | `results[].album.artists.entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `results[].album.artists.name` | string | yes | | | `results[].album.artists.imageUrl` | string \| null | yes | | | `results[].album.artists.followers` | number \| null | yes | | | `results[].artists` | ArtistSimplified[] \| null | yes | | | `results[].artists.id` | string \| null | yes | | | `results[].artists.entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `results[].artists.name` | string | yes | | | `results[].artists.imageUrl` | string \| null | yes | | | `results[].artists.followers` | number \| null | yes | | | `results[].isAvailable` | boolean \| null | no | Indicates whether the track is currently available for playback on the music service. For Spotify: false for local files or greyed-out tracks (copyright lost), true for playable tracks, null when unknown. | | `results[].dateAdded` | string \| null | no | | | `nextParam` | string \| null | yes | | | `totalItems` | number \| null | yes | | ```json { "results": [ { "id": "2nLtzopw4rPReszdYBJU6h", "entryId": "2nLtzopw4rPReszdYBJU6h", "name": "Numb", "isrc": "USWB10300474", "duration": 185586, "imageUrl": "https://i.scdn.co/image/ab67616d0000b273b4ad7ebaf4575f120eb3f193", "previewUrl": "https://p.scdn.co/mp3-preview/15e1178c9eb7f626ac1112ad8f56eccbec2cd6e5?cid=d3a370919fbb4853a2c037d9d92da680", "album": null, "artists": [ null ], "isAvailable": true, "dateAdded": "2024-10-16T07:52:07.000Z" } ], "nextParam": "eyJvZmZzZXQiOjEwfQ==", "totalItems": 25 } ``` ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl https://api.musicapi.com/api/{integrationUserUUID}/liked/tracks \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Add tracks to library URL: https://musicapi.com/docs/reference/add-liked-tracks.md `POST https://api.musicapi.com/api/{integrationUserUUID}/liked/tracks` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. [Check which services support adding tracks to user's library](/docs/get-started/services-and-features) This endpoint saves tracks to the current user's library on Music Service. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `itemIds` | string[] | yes | Track IDs that are retrievable through track searches or when fetching playlist items. A maximum of 20 IDs can be sent in one request. | ## Response 200 ok ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl -X POST "https://api.musicapi.com/api/{integrationUserUUID}/liked/tracks" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"itemIds":["42YpD6LQura7bGOtntatQw"]}' ``` # Remove tracks from library URL: https://musicapi.com/docs/reference/remove-liked-tracks.md `DELETE https://api.musicapi.com/api/{integrationUserUUID}/liked/tracks` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. [Check which services support removing tracks from library](/docs/get-started/services-and-features) Remove tracks from a user's library. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `entryIds` | string[] | yes | Identifiers associated with a tracks within a specific collection. It is used to identify a particular instance of a track within a playlist or a user's library. Track entry id can be obtained during fetching playlist items or user's library. A maximum of 20 IDs can be sent in one request. | ## Response 200 ok ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl -X DELETE "https://api.musicapi.com/api/{integrationUserUUID}/liked/tracks" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"entryIds":["42YpD6LQura7bGOtntatQw"]}' ``` # Get user's liked albums URL: https://musicapi.com/docs/reference/list-albums.md `GET https://api.musicapi.com/api/{integrationUserUUID}/albums` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint fetches liked albums from Music Service. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | ## Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `nextParam` | string | no | Parameter returned in the response responsible for pagination. | | `limitParam` | number | no | Specifies the maximum number of results per request, adjusted based on the limitations of the respective music service. Ignored if the service doesn't support such a limit. | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `results` | Album[] | yes | | | `results[].id` | string | yes | | | `results[].name` | string | yes | | | `results[].totalItems` | number \| null | yes | | | `results[].imageUrl` | string \| null | yes | | | `results[].artists` | ArtistSimplified[] \| null | yes | | | `results[].artists.id` | string \| null | yes | | | `results[].artists.entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `results[].artists.name` | string | yes | | | `results[].artists.imageUrl` | string \| null | yes | | | `results[].artists.followers` | number \| null | yes | | | `nextParam` | string \| null | yes | | | `totalItems` | number \| null | yes | | ```json { "results": [ { "id": "4Gfnly5CzMJQqkUFfoHaP3", "name": "Meteora", "totalItems": 13, "imageUrl": "https://www.example-image.com/978263478234", "artists": [ null ] } ], "nextParam": "eyJvZmZzZXQiOjEwfQ==", "totalItems": 25 } ``` ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl https://api.musicapi.com/api/{integrationUserUUID}/albums \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Add album to user's library URL: https://musicapi.com/docs/reference/save-album.md `POST https://api.musicapi.com/api/{integrationUserUUID}/albums` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint saves album to the current user's library on Music Service. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `albumId` | string | yes | | ## Response 200 ok ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl -X POST "https://api.musicapi.com/api/{integrationUserUUID}/albums" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"albumId":"57bgtoPSgt236d8kj"}' ``` # Get album URL: https://musicapi.com/docs/reference/get-album.md `GET https://api.musicapi.com/api/{integrationUserUUID}/albums/{albumId}` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint fetches specified album and returns information about it. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | | `albumId` | string | yes | ID of the album | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `name` | string | yes | | | `totalItems` | number \| null | yes | | | `imageUrl` | string \| null | yes | | | `artists` | ArtistSimplified[] \| null | yes | | | `artists.id` | string \| null | yes | | | `artists.entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `artists.name` | string | yes | | | `artists.imageUrl` | string \| null | yes | | | `artists.followers` | number \| null | yes | | ```json { "id": "4Gfnly5CzMJQqkUFfoHaP3", "name": "Meteora", "totalItems": 13, "imageUrl": "https://www.example-image.com/978263478234", "artists": [ { "id": "53XhwfbYqKCa1cC15pYq2q", "entryId": "53XhwfbYqKCa1cC15pYq2q", "name": "Imagine Dragons", "imageUrl": "https://i.scdn.co/image/ab6761610000e5ebab47d8dae2b24f5afe7f9d38", "followers": 53107521 } ] } ``` ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl https://api.musicapi.com/api/{integrationUserUUID}/albums/{albumId} \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Remove album from user's library URL: https://musicapi.com/docs/reference/remove-album.md `DELETE https://api.musicapi.com/api/{integrationUserUUID}/albums/{albumId}` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint removes an album from user's library on Music Service. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | | `albumId` | string | yes | ID of the album | ## Response 200 ok ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl -X DELETE "https://api.musicapi.com/api/{integrationUserUUID}/albums/{albumId}" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Get album tracks URL: https://musicapi.com/docs/reference/list-album-items.md `GET https://api.musicapi.com/api/{integrationUserUUID}/albums/{albumId}/items` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint fetches album's tracks from Music Service. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | | `albumId` | string | yes | ID of the album | ## Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `nextParam` | string | no | Parameter returned in the response responsible for pagination. | | `limitParam` | number | no | Specifies the maximum number of results per request, adjusted based on the limitations of the respective music service. Ignored if the service doesn't support such a limit. | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `results` | Track[] | yes | | | `results[].id` | string | yes | | | `results[].entryId` | string \| null | yes | Identifier associated with a tracks within a specific collection. It is used to identify a particular instance of a track within a playlist or a user's library. Track entry id can be obtained during fetching playlist items or user's library. | | `results[].name` | string | yes | | | `results[].isrc` | string \| null | yes | | | `results[].duration` | number \| null | yes | | | `results[].imageUrl` | string \| null | yes | | | `results[].previewUrl` | string \| null | yes | | | `results[].album` | object \| null | yes | | | `results[].album.name` | string | yes | | | `results[].album.id` | string \| null | yes | | | `results[].album.totalItems` | number \| null | yes | | | `results[].album.artists` | ArtistSimplified[] \| null | yes | | | `results[].album.artists.id` | string \| null | yes | | | `results[].album.artists.entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `results[].album.artists.name` | string | yes | | | `results[].album.artists.imageUrl` | string \| null | yes | | | `results[].album.artists.followers` | number \| null | yes | | | `results[].artists` | ArtistSimplified[] \| null | yes | | | `results[].artists.id` | string \| null | yes | | | `results[].artists.entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `results[].artists.name` | string | yes | | | `results[].artists.imageUrl` | string \| null | yes | | | `results[].artists.followers` | number \| null | yes | | | `results[].isAvailable` | boolean \| null | no | Indicates whether the track is currently available for playback on the music service. For Spotify: false for local files or greyed-out tracks (copyright lost), true for playable tracks, null when unknown. | | `results[].dateAdded` | string \| null | no | | | `nextParam` | string \| null | yes | | | `totalItems` | number \| null | yes | | ```json { "results": [ { "id": "2nLtzopw4rPReszdYBJU6h", "entryId": "2nLtzopw4rPReszdYBJU6h", "name": "Numb", "isrc": "USWB10300474", "duration": 185586, "imageUrl": "https://i.scdn.co/image/ab67616d0000b273b4ad7ebaf4575f120eb3f193", "previewUrl": "https://p.scdn.co/mp3-preview/15e1178c9eb7f626ac1112ad8f56eccbec2cd6e5?cid=d3a370919fbb4853a2c037d9d92da680", "album": null, "artists": [ null ], "isAvailable": true, "dateAdded": "2024-10-16T07:52:07.000Z" } ], "nextParam": "eyJvZmZzZXQiOjEwfQ==", "totalItems": 25 } ``` ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl https://api.musicapi.com/api/{integrationUserUUID}/albums/{albumId}/items \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Get user's liked artists URL: https://musicapi.com/docs/reference/list-artists.md `GET https://api.musicapi.com/api/{integrationUserUUID}/artists` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint fetches liked artists from Music Service. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | ## Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `nextParam` | string | no | Parameter returned in the response responsible for pagination. | | `limitParam` | number | no | Specifies the maximum number of results per request, adjusted based on the limitations of the respective music service. Ignored if the service doesn't support such a limit. | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `results` | Artist[] | yes | | | `results[].id` | string | yes | | | `results[].entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `results[].name` | string | yes | | | `results[].imageUrl` | string \| null | yes | | | `results[].followers` | number \| null | yes | | | `nextParam` | string \| null | yes | | | `totalItems` | number \| null | yes | | ```json { "results": [ { "id": "53XhwfbYqKCa1cC15pYq2q", "entryId": "53XhwfbYqKCa1cC15pYq2q", "name": "Imagine Dragons", "imageUrl": "https://i.scdn.co/image/ab6761610000e5ebab47d8dae2b24f5afe7f9d38", "followers": 53107521 } ], "nextParam": "eyJvZmZzZXQiOjEwfQ==", "totalItems": 25 } ``` ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl https://api.musicapi.com/api/{integrationUserUUID}/artists \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Add artist to user's library URL: https://musicapi.com/docs/reference/follow-artist.md `POST https://api.musicapi.com/api/{integrationUserUUID}/artists` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint saves artist to the current user's library on Music Service. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `artistId` | string | yes | | ## Response 200 ok ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl -X POST "https://api.musicapi.com/api/{integrationUserUUID}/artists" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"artistId":"53XhwfbYqKCa1cC15pYq2q"}' ``` # Get artist URL: https://musicapi.com/docs/reference/get-artist.md `GET https://api.musicapi.com/api/{integrationUserUUID}/artists/{artistId}` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint fetches specified artist and returns information about it. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | | `artistId` | string | yes | ID of the artist | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | yes | | | `entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `name` | string | yes | | | `imageUrl` | string \| null | yes | | | `followers` | number \| null | yes | | ```json { "id": "53XhwfbYqKCa1cC15pYq2q", "entryId": "53XhwfbYqKCa1cC15pYq2q", "name": "Imagine Dragons", "imageUrl": "https://i.scdn.co/image/ab6761610000e5ebab47d8dae2b24f5afe7f9d38", "followers": 53107521 } ``` ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl https://api.musicapi.com/api/{integrationUserUUID}/artists/{artistId} \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Remove artist from user's library URL: https://musicapi.com/docs/reference/unfollow-artist.md `DELETE https://api.musicapi.com/api/{integrationUserUUID}/artists/{artistId}` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint removes an artist from user's library on Music Service. ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | | `artistId` | string | yes | ID of the artist | ## Response 200 ok ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl -X DELETE "https://api.musicapi.com/api/{integrationUserUUID}/artists/{artistId}" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Search tracks, albums and artists URL: https://musicapi.com/docs/reference/search-user-service.md `POST https://api.musicapi.com/api/{integrationUserUUID}/search` Section: Read and write a user's library. Product: MusicAPI Core. Auth: Basic, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. [Check which services support searching by ISRC](/docs/get-started/services-and-features) Search Endpoint allows you to search for - tracks - albums - artists ## Path parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `integrationUserUUID` | uuid | yes | The connected user: the `integrationUserUUID` from the sign-in callback. | ## Query parameters | Name | Type | Required | Description | | --- | --- | --- | --- | | `nextParam` | string | no | Parameter returned in the response responsible for pagination. | | `limitParam` | number | no | Specifies the maximum number of results per request, adjusted based on the limitations of the respective music service. Ignored if the service doesn't support such a limit. | ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | string | yes | One of: track, album, artist. | | `track` | string | no | Track title | | `artist` | string | no | Artist name | | `album` | string | no | Album title | | `isrc` | string | no | 12-digit code assigned to a unique recording of a specific track | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `results` | SearchResult[] | yes | | | `results[].id` | string | yes | | | `results[].type` | string | yes | One of: track, album, artist. | | `results[].track` | object \| null | yes | | | `results[].track.id` | string | yes | | | `results[].track.entryId` | string \| null | yes | Identifier associated with a tracks within a specific collection. It is used to identify a particular instance of a track within a playlist or a user's library. Track entry id can be obtained during fetching playlist items or user's library. | | `results[].track.name` | string | yes | | | `results[].track.isrc` | string \| null | yes | | | `results[].track.duration` | number \| null | yes | | | `results[].track.imageUrl` | string \| null | yes | | | `results[].track.previewUrl` | string \| null | yes | | | `results[].track.album` | object \| null | yes | | | `results[].track.album.name` | string | yes | | | `results[].track.album.id` | string \| null | yes | | | `results[].track.album.totalItems` | number \| null | yes | | | `results[].track.album.artists` | ArtistSimplified[] \| null | yes | | | `results[].track.album.artists.id` | string \| null | yes | | | `results[].track.album.artists.entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `results[].track.album.artists.name` | string | yes | | | `results[].track.album.artists.imageUrl` | string \| null | yes | | | `results[].track.album.artists.followers` | number \| null | yes | | | `results[].track.artists` | ArtistSimplified[] \| null | yes | | | `results[].track.artists.id` | string \| null | yes | | | `results[].track.artists.entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `results[].track.artists.name` | string | yes | | | `results[].track.artists.imageUrl` | string \| null | yes | | | `results[].track.artists.followers` | number \| null | yes | | | `results[].track.isAvailable` | boolean \| null | no | Indicates whether the track is currently available for playback on the music service. For Spotify: false for local files or greyed-out tracks (copyright lost), true for playable tracks, null when unknown. | | `results[].track.dateAdded` | string \| null | no | | | `results[].album` | object \| null | yes | | | `results[].album.id` | string | yes | | | `results[].album.name` | string | yes | | | `results[].album.totalItems` | number \| null | yes | | | `results[].album.imageUrl` | string \| null | yes | | | `results[].album.artists` | ArtistSimplified[] \| null | yes | | | `results[].album.artists.id` | string \| null | yes | | | `results[].album.artists.entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `results[].album.artists.name` | string | yes | | | `results[].album.artists.imageUrl` | string \| null | yes | | | `results[].album.artists.followers` | number \| null | yes | | | `results[].artist` | object \| null | yes | | | `results[].artist.id` | string | yes | | | `results[].artist.entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `results[].artist.name` | string | yes | | | `results[].artist.imageUrl` | string \| null | yes | | | `results[].artist.followers` | number \| null | yes | | | `nextParam` | string \| null | yes | | | `totalItems` | number \| null | yes | | ```json { "results": [ { "id": "57bgtoPSgt236HzfBOd8kj", "type": "track", "track": null, "album": null, "artist": null } ], "nextParam": "eyJvZmZzZXQiOjEwfQ==", "totalItems": 0 } ``` ## Errors - 404: Your account has no connected user with this uuid, or the item does not exist. - 412: The user must sign in again (`IntegrationUserNotAuthenticated`). - 429: Too many requests. Wait for the seconds in `Retry-After`. ## Example ```bash curl -X POST "https://api.musicapi.com/api/{integrationUserUUID}/search" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"type":"track","track":"Numb","artist":"Linkin Park","album":"Meteora","isrc":"USWB10300474"}' ``` # Search URL: https://musicapi.com/docs/reference/search-catalog.md `POST https://api.musicapi.com/public/search` Section: Search and match the catalog. Product: MusicAPI Core. Auth: Basic, Token, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. Searches the catalogues of one or more services for a track, an album or an artist. Needs no connected user. [Check which services return the ISRC](/docs/get-started/services-and-features). ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | string | yes | One of: track, album, artist. | | `track` | string | no | Track title. | | `artist` | string | no | Artist name. | | `album` | string | no | Album title. | | `sources` | string[] | yes | At least one search source. `GET /public/search/introspection` lists the valid values. | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `tracks` | object[] | yes | | | `tracks[].source` | string | yes | | | `tracks[].status` | string | yes | | | `tracks[].data` | object | yes | | | `tracks[].data.externalId` | string | yes | | | `tracks[].data.name` | string | yes | | | `tracks[].data.artistNames` | string[] \| null | yes | | | `tracks[].data.albumName` | string \| null | yes | | | `tracks[].data.imageUrl` | string | yes | | | `tracks[].data.previewUrl` | string \| null | yes | | | `tracks[].data.isrc` | string | yes | | | `tracks[].data.duration` | number \| null | yes | | | `tracks[].data.url` | string | yes | | | `tracks[].type` | string | yes | | ```json { "tracks": [ { "source": "spotify", "status": "success", "data": { "externalId": "5aszL9hl6SBzFNsOvw8u8w", "name": "Bezos I", "artistNames": [ "Bo Burnham" ], "albumName": "INSIDE", "imageUrl": "https://i.scdn.co/image/ab67616d0000b27388fed14b936c38007a302413", "previewUrl": "string", "isrc": "", "duration": 58149, "url": "https://open.spotify.com/track/5aszL9hl6SBzFNsOvw8u8w" }, "type": "track" } ] } ``` ## Example ```bash curl -X POST "https://api.musicapi.com/public/search" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"type":"track","track":"Bezos I","artist":"Bo Burnham","album":"INSIDE","sources":["spotify"]}' ``` # Search Introspection URL: https://musicapi.com/docs/reference/get-search-introspection.md `GET https://api.musicapi.com/public/search/introspection` Section: Search and match the catalog. Product: MusicAPI Core. Auth: Basic, Token, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. Allows you to fetch all different enums needed for sending API requests. ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `sources` | string[] | yes | | | `authSources` | string[] | yes | | | `types` | string[] | yes | | ```json { "sources": [ "amazonMusic" ], "authSources": [ "spotify" ], "types": [ "track" ] } ``` ## Example ```bash curl https://api.musicapi.com/public/search/introspection \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" ``` # Match items into other services URL: https://musicapi.com/docs/reference/match-catalog-items.md `POST https://api.musicapi.com/public/search/match` Section: Search and match the catalog. Product: MusicAPI Core. Auth: Basic, Token, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. Finds each track, album or artist that you name in the destination services. Needs no connected user. ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `destinations` | string[] | yes | The services to find each object in. | | `objects` | CatalogObjectRef[] | yes | | | `objects[].id` | string | yes | The id of the track, album or artist on its service. | | `objects[].source` | string | yes | A search source. `GET /public/search/introspection` lists them. | ## Response 200 OK | Field | Type | Required | Description | | --- | --- | --- | --- | | `matches` | object[] | yes | | | `matches[].object` | object | yes | The object you sent, with its catalogue data. | | `matches[].matches` | object[] | yes | One entry per destination that has the object. | ```json { "matches": [ { "object": {}, "matches": [ {} ] } ] } ``` ## Errors - 400: A source or a destination is not a search source. ## Example ```bash curl -X POST "https://api.musicapi.com/public/search/match" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"destinations":["appleMusic"],"objects":[{"id":"5aszL9hl6SBzFNsOvw8u8w","source":"spotify"}]}' ``` # Inspect URL: https://musicapi.com/docs/reference/inspect-url.md `POST https://api.musicapi.com/public/inspect/url` Section: Search and match the catalog. Product: MusicAPI Core. Auth: Basic, Token, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. [Check which services support returning ISRC](/docs/get-started/services-and-features) Inspect endpoint allows you fetch the metadata of music service URL. You can pass any URL from music service (eg. `https://tidal.com/browse/track/131696293`) and the API will return you information about what it points at. Inspect API supports: - albums - tracks - playlists. ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `url` | string | yes | | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string | yes | | | `source` | string | yes | | | `type` | string | yes | | | `data` | object | yes | | | `data.externalId` | string | yes | | | `data.name` | string | yes | | | `data.artistNames` | string[] \| null | yes | | | `data.albumName` | string | yes | | | `data.imageUrl` | string | yes | | | `data.previewUrl` | string \| null | yes | | | `data.isrc` | string | yes | | | `data.duration` | number \| null | yes | | | `data.url` | string | yes | | ```json { "status": "success", "source": "spotify", "type": "track", "data": { "externalId": "5aszL9hl6SBzFNsOvw8u8w", "name": "Bezos I", "artistNames": [ "Bo Burnham" ], "albumName": "INSIDE", "imageUrl": "https://i.scdn.co/image/ab67616d00001e0288fed14b936c38007a302413", "previewUrl": "string", "isrc": "", "duration": 58149, "url": "https://open.spotify.com/track/5aszL9hl6SBzFNsOvw8u8w" } } ``` ## Example ```bash curl -X POST "https://api.musicapi.com/public/inspect/url" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"url":"https://open.spotify.com/track/5aszL9hl6SBzFNsOvw8u8w"}' ``` # Inspect Playlist Items URL: https://musicapi.com/docs/reference/get-public-playlist-items.md `POST https://api.musicapi.com/public/inspect/playlistItems` Section: Search and match the catalog. Product: MusicAPI Core. Auth: Basic, Token, DevToken, DocsSession (any one of them). Use Basic with your Client ID and Client Secret from your server only. This endpoint allows you to fetch the tracks of a playlist from a music service without user authentication. You need to provide the `playlistId` (which can be extracted from a playlist URL) and the `serviceId` of the music service. The response includes paginated track results (up to 50 tracks per page). To fetch subsequent pages, pass the `nextParam` value from the response back in the `nextParam` field of the request body. When the response `nextParam` is `null`, there are no more pages. Supported services: Spotify, Apple Music, Deezer, YouTube, YouTube Music, SoundCloud, Tidal, Amazon Music, Pandora, Qobuz. ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `playlistId` | string | yes | ID of the playlist on the music service. Can be extracted from the playlist URL. | | `serviceId` | string | yes | ID of the music service One of: spotify, youtube, appleMusic, amazonMusic, tidal, resso, boomplay, deezer, pandora, soundCloud, youtubeMusic, napster, qobuz, qqMusic, yandexMusic, vkMusic, zvuk, gaana, jiosaavn, audiomack, audius. | | `nextParam` | string | no | Pagination cursor. Omit to fetch the first page; to fetch a subsequent page pass the `nextParam` value returned in the previous response. | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string | yes | One of: success, not_found, error. | | `source` | string | no | The music service that provided the data | | `data` | object \| null | yes | | | `data.results` | Track[] | yes | | | `data.results[].id` | string | yes | | | `data.results[].entryId` | string \| null | yes | Identifier associated with a tracks within a specific collection. It is used to identify a particular instance of a track within a playlist or a user's library. Track entry id can be obtained during fetching playlist items or user's library. | | `data.results[].name` | string | yes | | | `data.results[].isrc` | string \| null | yes | | | `data.results[].duration` | number \| null | yes | | | `data.results[].imageUrl` | string \| null | yes | | | `data.results[].previewUrl` | string \| null | yes | | | `data.results[].album` | object \| null | yes | | | `data.results[].album.name` | string | yes | | | `data.results[].album.id` | string \| null | yes | | | `data.results[].album.totalItems` | number \| null | yes | | | `data.results[].album.artists` | ArtistSimplified[] \| null | yes | | | `data.results[].album.artists.id` | string \| null | yes | | | `data.results[].album.artists.entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `data.results[].album.artists.name` | string | yes | | | `data.results[].album.artists.imageUrl` | string \| null | yes | | | `data.results[].album.artists.followers` | number \| null | yes | | | `data.results[].artists` | ArtistSimplified[] \| null | yes | | | `data.results[].artists.id` | string \| null | yes | | | `data.results[].artists.entryId` | string \| null | yes | Identifier associated with an artist within a user's library. Artist entry id can be obtained during fetching artists from user's library. | | `data.results[].artists.name` | string | yes | | | `data.results[].artists.imageUrl` | string \| null | yes | | | `data.results[].artists.followers` | number \| null | yes | | | `data.results[].isAvailable` | boolean \| null | no | Indicates whether the track is currently available for playback on the music service. For Spotify: false for local files or greyed-out tracks (copyright lost), true for playable tracks, null when unknown. | | `data.results[].dateAdded` | string \| null | no | | | `data.nextParam` | string \| null | yes | | | `data.totalItems` | number \| null | yes | | ```json { "status": "success", "source": "spotify", "data": { "results": [ null ], "nextParam": "eyJvZmZzZXQiOjEwfQ==", "totalItems": 25 } } ``` ## Example ```bash curl -X POST "https://api.musicapi.com/public/inspect/playlistItems" \ -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -d '{"playlistId":"37i9dQZF1DXcBWIGoYBM5M","serviceId":"spotify","nextParam":"eyJvZmZzZXQiOjEwfQ=="}' ``` # Embed player URL: https://musicapi.com/docs/reference/create-embed-player.md `POST https://api.musicapi.com/public/embed/url` Section: Search and match the catalog. Product: MusicAPI Core. Auth: none. This endpoint allows you to send a request with a share URL to any track, artist, or album from music service, and it will generate an embed player code (an `iframe`) for that resource, enabling easy embedding of the music player on a website or application. This endpoint, in addition to the iframe will return available sizes of that music player, so if you would like to change the default size, you need to insert the selected height in two places: into the `height` property of a returned iframe as well as in the URL height param within that iframe `src` property. ## Request body `application/json`, required. | Field | Type | Required | Description | | --- | --- | --- | --- | | `url` | string | yes | URL to track, album or artist of music service | ## Response 200 ok | Field | Type | Required | Description | | --- | --- | --- | --- | | `embedCode` | string | yes | HTML iframe code used to embed music player | | `embedUrl` | string | yes | URL used as the source of the iframe | | `sizes` | EmbedPlayerSize[] | yes | Available sizes for iframe | | `sizes[].height` | number | yes | | | `sizes[].label` | string | yes | | | `defaultSize` | EmbedPlayerSize | yes | | | `defaultSize.height` | number | yes | | | `defaultSize.label` | string | yes | | ```json { "embedCode": "