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.
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:
- Create an OAuth client from the developers dashboard. You get a
client_idand a one-timeclient_secret. - POST both to
https://api.musimap.com/oauth/tokenwithgrant_type=client_credentials. - You receive an
access_tokenvalid for one hour. Cache it. refresh only when it's about to expire. - 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_codesuch asinvalid_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'
import requests
response = requests.post(
'https://api.musimap.com/oauth/token',
data={
'grant_type': 'client_credentials',
'client_id': 'YOUR_CLIENT_ID',
'client_secret': 'YOUR_CLIENT_SECRET',
'scope': 'catalog.read tagging.write',
},
timeout=10,
)
response.raise_for_status()
token = response.json()
const body = new URLSearchParams({
grant_type: "client_credentials",
client_id: "YOUR_CLIENT_ID",
client_secret: "YOUR_CLIENT_SECRET",
scope: "catalog.read tagging.write",
});
const response = await fetch("https://api.musimap.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
});
if (!response.ok) throw new Error(await response.text());
const token = await response.json();
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):
{
"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"
requests.get(
'https://api.musimap.com/v1/me',
headers={'Authorization': f'Bearer {access_token}'},
)
await fetch("https://api.musimap.com/v1/me", {
headers: { Authorization: `Bearer ${accessToken}` },
});
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"
response = requests.get(
'https://api.musimap.com/v1/me',
headers={'Authorization': f'Bearer {access_token}'},
timeout=10,
)
response.raise_for_status()
context = response.json()['data']
const response = await fetch("https://api.musimap.com/v1/me", {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!response.ok) throw new Error(await response.text());
const { data: context } = await response.json();
The response uses the MusiMap envelope:
{
"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']
let cached: { value: string; expiresAt: number } | null = null;
export async function getAccessToken(): Promise<string> {
// Refresh ~60s before the real expiry.
if (cached && cached.expiresAt - 60_000 > Date.now()) return cached.value;
const body = new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.MUSIMAP_CLIENT_ID!,
client_secret: process.env.MUSIMAP_CLIENT_SECRET!,
});
const r = await fetch("https://api.musimap.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
});
if (!r.ok) throw new Error(await r.text());
const data = await r.json();
cached = {
value: data.access_token,
expiresAt: Date.now() + data.expires_in * 1000,
};
return cached.value;
}
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.