Authentication (OAuth2)

Client-credentials flow, token lifecycle and scopes.

The MusiMap Web API uses the standard OAuth2 client-credentials flow for server-to-server calls. You exchange a client_id and client_secret for a short-lived access token, then include it in the Authorization header of every subsequent request.

Letting users sign in with their MusiMap account?

That's a different flow. OpenID Connect Authorization Code + PKCE. It's documented under Sign in with MusiMap. This page is about server-to-server auth.

Overview

In one picture:

  1. Create an OAuth client from the developers dashboard. You get a client_id and a one-time client_secret.
  2. POST both to https://api.musimap.com/oauth/token with grant_type=client_credentials.
  3. You receive an access_token valid for one hour. Cache it. refresh only when it's about to expire.
  4. Send it as Authorization: Bearer <token> on every /v1/* Web API call.

OAuth protocol vs versioned Web API

MusiMap exposes two related but distinct surfaces on https://api.musimap.com:

  • /oauth/*. OAuth 2.0 and OpenID Connect protocol endpoints (token exchange, revocation, UserInfo). Responses follow RFC/OIDC shapes directly. They are not wrapped in the MusiMap {status, message, data} envelope.
  • /v1/*: the versioned Web API for catalogue, ingestion, tagging, and identity. Responses use the MusiMap envelope, including authentication errors (data.error_code such as invalid_token).

Use /oauth/token to obtain tokens; use /v1/* for business operations.

Prerequisites

You create OAuth clients from the developers dashboard. Each client belongs to an organisation. Which scopes you may delegate depends on the capabilities shown under Capabilities in the dashboard.

Store the client_secret somewhere safe. We show it exactly once, at creation; after that, you'll need to rotate it from the dashboard if you lose it.

Token endpoint

Exchange your credentials for an access token by POSTing to /oauth/token. The request is standard application/x-www-form-urlencoded: same as every other OAuth2 server you've seen.

curl -X POST https://api.musimap.com/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials' \
  -d 'client_id=YOUR_CLIENT_ID' \
  -d 'client_secret=YOUR_CLIENT_SECRET' \
  -d 'scope=catalog.read tagging.write'

You'll receive the token, its type, its lifetime in seconds, and the scopes that were actually granted (they may be a subset of what you asked for if your client isn't configured for all of them):

200 OK
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type":   "Bearer",
  "expires_in":   3600,
  "scope":        "catalog.read tagging.write"
}

Scopes

Access tokens are scoped. Request only what you need. A single OAuth client can request multiple scopes separated by spaces. The public API contract is expressed exclusively through OAuth scopes. Organisation product access appears in the dashboard as capabilities.

The table below shows common scopes. See References → Scopes for the full catalogue.

Scope Grants access to
catalog.read List catalogues and read Workspace inventory (/v1/workspace/*).
catalog.write Create and update Workspace catalogue entities.
atlas.read Read MusiMap Atlas inventory and include Atlas in search.
ingestion.read Read ingestion batches, items, entities, and stats.
ingestion.write Create ingestion batches.
storage.write Request presigned upload URLs and delete inbound objects.
tagging.write Run live MusiTag tagging on an authorised S3 object.
search.read Run MusiSearch track discovery.
credits.read Read organisation credit balances and history.

Using the token

Pass the token in the Authorization header, with the Bearer prefix, on every API call:

curl https://api.musimap.com/v1/me \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Check your token context

After you have an access token, call GET /v1/me to confirm which principal, organisation, and catalogue context the token operates under, and which OAuth scopes were granted. This is the recommended sanity check during integration. For the full list of catalogues available to your OAuth client, call GET /v1/catalogs (see Catalogues).

When catalog_id is null, the client is unbound (not restricted to a single catalogue). Use catalog_access_mode, default_catalog_id, and GET /v1/catalogs to understand accessible catalogues.

curl https://api.musimap.com/v1/me \
  -H "Authorization: Bearer $ACCESS_TOKEN"

The response uses the MusiMap envelope:

200 OK
{
  "status": 200,
  "message": "OK",
  "data": {
    "principal_type": "client",
    "subject_id": "019c41e3-6532-7020-8f81-b9c9405387dd",
    "client_id": "your-client-id",
    "organization_id": "019c41e3-6532-7020-8f81-b9c9405383ee",
    "catalog_id": null,
    "catalog_binding": null,
    "catalog_access_mode": "organization",
    "default_catalog_id": "019c41e3-6532-7020-8f81-b9c9405384aa",
    "scopes": ["catalog.read", "tagging.write"],
    "capabilities": ["customer_catalog", "tagging"]
  }
}

Use GET /v1/me/scopes to read the scopes attached to the current token. Use scopes and the capabilities list as the public authorization contract for the token. Neither GET /v1/me nor GET /v1/me/scopes returns commercial entitlement lists; product packaging is managed in the dashboard.

Token lifecycle

Access tokens live for one hour (expires_in: 3600). There is no refresh token for the client-credentials flow. to extend your session, you simply exchange your credentials again. The recommended pattern is to cache the token in memory and re-issue it a minute before expiry:

import os, time, requests

_token = {'value': None, 'expires_at': 0}

def get_access_token() -> str:
    # Refresh ~60s before the real expiry to stay on the safe side.
    if _token['value'] and _token['expires_at'] - 60 > time.time():
        return _token['value']

    r = requests.post(
        'https://api.musimap.com/oauth/token',
        data={
            'grant_type':    'client_credentials',
            'client_id':     os.environ['MUSIMAP_CLIENT_ID'],
            'client_secret': os.environ['MUSIMAP_CLIENT_SECRET'],
        },
        timeout=10,
    )
    r.raise_for_status()
    data = r.json()
    _token['value']      = data['access_token']
    _token['expires_at'] = time.time() + data['expires_in']
    return _token['value']
Don't request a new token on every call.

The token endpoint is rate-limited more aggressively than the rest of the API. If you issue a new token for every request, you'll hit 429 errors at moderate volume. Caching is not optional; the SDKs do it for you automatically.

Common errors

When something goes wrong, you get back a JSON body following the OAuth2 error format (error + error_description). Here are the ones you're most likely to see:

HTTP Error When it happens How to fix
401 invalid_client Wrong client_id / client_secret, or the client has been revoked. Double-check your credentials. Rotate the secret from the dashboard if it leaked.
400 invalid_grant Grant type other than client_credentials for a server-to-server client. Use grant_type=client_credentials. For user sign-in, see Sign in with MusiMap.
400 invalid_scope You requested a scope your client is not allowed to have, or your organisation cannot grant. Check the OAuth client's scopes and organisation capabilities in the dashboard.
401 invalid_token The Authorization header is missing, malformed, or the token has expired. Request a new token. SDKs do this automatically.
403 insufficient_scope Your token is valid but does not include the scope required by this endpoint. Request a token with the correct scope. See Scopes below and References → Scopes.
403 entitlement_required Your organisation does not hold the product access that unlocks this capability. Ask MusiMap to enable the product capability, then re-delegate the matching scopes.
403 entitlement_revoked The product access that backed this scope was removed after the token was issued. Request a new token. Re-delegate scopes only after product access is restored.
429 rate_limited Too many token exchanges in a short window, often from not caching the access token. Cache the token until a few minutes before expiry. SDKs handle this for you.

For the full MusiMap error envelope on /v1/* (including data.error_code and optional data.details), see Errors. OAuth protocol endpoints keep the RFC error shape shown above.

Where to go next