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