Skip to the content
  1. Docs
  2. Managed Transfers

Pre-authorized sessions

Managed TransfersGuide · 5 min
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.

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. 01
    Your backend
    POST /public/transfer-sessions with the user's token
  2. 02
    MusicAPI
    Checks the token at the service and answers a launchUrl
  3. 03
    Your app
    Opens the launchUrl. The user connects the source.
  4. 04
    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

curl
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
stringrequired
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.
201 Created
{
  "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.

StatusWhy
400The body is not valid, no destination is set, or the destination does not support this flow.
401No credential.
403The credential is not secret, or Managed Transfers is off for your account.
422The music service refused the access token. We store nothing.
503We 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, 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

curl
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
    }
  }'
200 OK
{
  "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

curl
curl -X DELETE "https://api.musicapi.com/public/transfer-sessions/0f1e2d3c-4b5a-4968-8776-655443322110" \
  -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET"
200 OK
{
  "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.