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. Token introspection (POST /oauth/introspect)
is reserved for internal and trusted partner integrations and is
not part of the public Web API documentation.
Prerequisites
You create OAuth clients from the developers dashboard (available at launch — for now, contact us). Each client belongs to an organization and is scoped to specific capabilities — see Organizations & OAuth clients for the full model.
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=audio.tagging audio.ingest'
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': 'audio.tagging audio.ingest',
},
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: "audio.tagging audio.ingest",
});
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": "audio.tagging audio.ingest"
}
Scopes
Access tokens are scoped. Request only what you need — if a token leaks, its blast radius is limited to the scopes it was issued with. A single OAuth client can request multiple scopes separated by spaces.
| Scope | Grants access to |
|---|---|
audio.ingest |
Upload audio files and sidecar metadata into your organization's catalogue. |
audio.tagging |
Run tagging analyses and read their results. |
audio.profiling |
Build and query listener profiles. |
audio.fingerprinting |
Identify tracks from short snippets against your catalogue. |
catalogue.read |
Search and read your catalogue of analysed tracks. |
catalogue.write |
Update track metadata and manage your catalogue. |
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 catalog 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).
The catalog_id on /v1/me is the default or current
catalogue context, not necessarily every catalogue you can access.
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": "019c41e3-6532-7020-8f81-b9c9405384aa",
"scopes": ["audio.tagging", "audio.ingest"],
"permissions": []
}
}
Use GET /v1/me/scopes to read the scopes attached to the current token.
The permissions field is reserved for fine-grained access control and
currently returns an empty list.
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 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 isn't allowed to have. | Check the OAuth client's scopes in the dashboard, or remove the scope from the request. |
| 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 doesn't include the scope required by this endpoint. | Request a token with the correct scope — see the Scopes section below. |
| 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.