Unofficial notes on Beatport's APIs. The source is Beatport's own OpenAPI spec (/v4/swagger-ui/json/), the developer docs bundle at https://api.beatport.com/v4/docs/, and live probes.
Last checked: 2026-10-28. The spec on that date: OpenAPI 3.0.3, 117 paths, 98 schemas.
| API | Base URL | Purpose | Authentication |
|---|---|---|---|
| Public API | https://api.beatport.com/v4 |
The API for apps. Use this one. | OAuth 2.0 bearer token |
| Account / identity host | https://account.beatport.com |
OAuth authorize and token pages, account settings | Session |
| Internal API | https://api-internal.beatportprod.com/v4 |
Server-side rendering of the Next.js store | Private network only |
Beatport has no self-serve developer program. You cannot register your own OAuth app, so you get no client secret. The only client ID that you can use is the one of Beatport's own Developer Docs app.
- Open
https://api.beatport.com/v4/docs/. - Look at the
<script src="/static/btprt/<hash>.js">tags. The client ID is in the first one, the small webpack bootstrap file (about 8 KB). - Search that file for
API_CLIENT_ID. The bundle merges it into aconfigobject and setswindow.Config = config:
var config = _objectSpread({}, common, {
API_CLIENT_ID: '<client id>'
});
if (typeof window !== 'undefined') {
window.Config = config;
}The file name is a content hash. It changes when Beatport deploys, so do not hardcode the file name or the ID. Scrape it at runtime:
curl -s https://api.beatport.com/v4/docs/ \
| grep -oE '/static/btprt/[^"]+\.js' \
| while read p; do curl -s "https://api.beatport.com$p" | grep -oE "API_CLIENT_ID:[[:space:]]*'[^']+'"; doneRegex for code: API_CLIENT_ID:\s*['"]([^'"]+)['"]. In PHP, kemo/beatport-api-client has AuthClient::scrapeClientId().
Facts about this client (from a token that the docs app issued):
application_id:1- Default scope:
app:docs user:dj expires_in:600(10 minutes). Use the refresh token.- It is a public client. It has no secret. The "User Password" and "Client Credentials" grants in Beatport's docs need a secret, so you cannot use them.
The docs client accepts only its registered redirect URIs. Any other value, for example your own https://example.com/callback, gives this page:
Error: invalid_request
Mismatching redirect URI.
Accepted values (tested 2026-10-28):
https://api.beatport.com/v4/auth/o/post-message/https://account.beatport.com/o/post-message/
You cannot send the code to your own callback. Read the code query parameter from the redirect instead:
- Server side: send the authorize request with the session cookies and do not follow redirects. The
Locationheader of the302has?code=.... - Browser: the
post-messagepage redirects away, so the address bar does not show the code. Open DevTools, select the Network tab, and set "Preserve log". Then load the authorize URL. Readcodefrom theLocationheader of the302, or from the URL of thepost-messagerequest.
1. Log in to get session cookies
POST https://api.beatport.com/v4/auth/login/
Content-Type: application/json
{"username": "<username>", "password": "<password>"}
A wrong login returns 403 with "Incorrect username or password.". A GET on this URL now redirects to https://account.beatport.com/, but the POST still works.
2. Authorize with the session cookies
GET https://api.beatport.com/v4/auth/o/authorize/
?client_id={client_id}
&response_type=code
&redirect_uri=https://api.beatport.com/v4/auth/o/post-message/
Cookie: {session cookies}
Do not follow the redirect. Take code from the Location header. Use the trailing slash: /authorize without it returns 301.
3. Exchange the code for tokens
POST https://api.beatport.com/v4/auth/o/token/
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code={code}
&client_id={client_id}
&redirect_uri=https://api.beatport.com/v4/auth/o/post-message/
The redirect_uri must be the same value as in step 2. No client_secret is sent.
4. Refresh
POST https://api.beatport.com/v4/auth/o/token/
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token={refresh_token}
&client_id={client_id}
Token response:
{
"access_token": "...",
"refresh_token": "...",
"expires_in": 600,
"scope": "app:docs user:dj",
"token_type": "Bearer"
}Check a token:
GET https://api.beatport.com/v4/auth/o/introspect/
Authorization: Bearer {access_token}
Both hosts respond. The spec lists the api.beatport.com paths. The current docs bundle uses the account.beatport.com paths.
| Purpose | Spec path | Docs bundle path |
|---|---|---|
| Authorize | https://api.beatport.com/v4/auth/o/authorize/ |
https://account.beatport.com/o/authorize/ |
| Token | https://api.beatport.com/v4/auth/o/token/ |
https://account.beatport.com/o/token/ |
| Redirect target | https://api.beatport.com/v4/auth/o/post-message/ |
https://account.beatport.com/o/post-message/ |
Beatport's docs app does not send code_challenge or code_verifier. PKCE is not necessary with this client.
Do not send scope=library. That scope is not in the spec. Leave scope out to get the default scope of the client.
The spec lists these scope groups:
app:*:app:docs,app:store,app:djapp,app:mobile,app:external,app:integrations, and moreuser:*:user:dj,user:artist,user:label,user:supplier,user:anon,user:bs_sunsetstaff:*: admin, content_ops, curation, marketing, label_mgmt, support, finance- Data scopes:
catalog:read,catalog:write,curation:read,curation:write,landing_pages:read,landing_pages:write,coupons:read,coupons:write
The spec also lists basicAuth and cookieAuth (cookie sessionid) as security schemes.
Authorization: Bearer {access_token}
Accept: application/json
All paths below are relative to https://api.beatport.com/v4. Every endpoint needs a bearer token. Without a token, the API returns 401. An unknown path returns 404.
| Method | Path | Notes |
|---|---|---|
| GET | /catalog/artists/ |
|
| GET | /catalog/artists/{id}/ |
|
| GET | /catalog/artists/{id}/images/ |
Return/create this artist's images. |
| GET | /catalog/artists/{id}/top/{num}/ |
Return top tracks for a artist by most popular rank, where N limited between 1 and 100. |
| GET | /catalog/artists/{id}/tracks/ |
Return this artist's tracks |
| GET | /catalog/artists/facets/ |
Filter facets (help text for each filter) |
| GET | /catalog/charts/ |
|
| GET | /catalog/charts/{id}/ |
|
| GET | /catalog/charts/{id}/images/ |
Return/create this chart's images. |
| GET | /catalog/charts/{id}/tracks/ |
Return this chart's tracks, updating if necessary. |
| GET | /catalog/charts/{id}/tracks/ids/ |
Return this chart's track IDs. |
| GET | /catalog/charts/facets/ |
Filter facets (help text for each filter) |
| GET | /catalog/charts/genre-overview/ |
|
| GET | /catalog/genres/ |
|
| GET | /catalog/genres/{id}/ |
|
| GET | /catalog/genres/{id}/sub-genres/ |
Return this genre's sub-genres |
| GET | /catalog/genres/{id}/top/{num}/ |
Return top tracks for a genre by most popular rank, where N limited between 1 and 100. |
| GET | /catalog/genres/{id}/tracks/ |
Return this genre's tracks |
| GET | /catalog/genres/facets/ |
Filter facets (help text for each filter) |
| GET | /catalog/labels/ |
|
| GET | /catalog/labels/{id}/ |
|
| GET | /catalog/labels/{id}/images/ |
Return/create this label's images. |
| GET | /catalog/labels/{id}/releases/ |
Return this label's releases |
| GET | /catalog/labels/{id}/top/{num}/ |
Return top tracks for a label by most popular rank, where N limited between 1 and 100. |
| GET | /catalog/labels/facets/ |
Filter facets (help text for each filter) |
| GET | /catalog/playlists/ |
Return list of Playlists. |
| GET | /catalog/playlists/{id}/ |
Return a Playlist. |
| GET | /catalog/playlists/{id}/chart-mapping/ |
Get the chart mapping value if any |
| GET | /catalog/playlists/{id}/partners/ |
Get the playlist Link! partners if any |
| GET | /catalog/playlists/{playlists_pk}/tracks/ |
|
| GET | /catalog/playlists/{playlists_pk}/tracks/facets/ |
Filter facets (help text for each filter) |
| GET | /catalog/playlists/{playlists_pk}/tracks/ids/ |
Return a list of PlaylistTrack IDs for the given playlist. |
| GET | /catalog/playlists/facets/ |
Filter facets (help text for each filter) |
| GET | /catalog/releases/ |
|
| GET | /catalog/releases/{id}/ |
|
| GET | /catalog/releases/{id}/beatbot/ |
|
| GET | /catalog/releases/{id}/dj-version-parent/ |
Read or update the parent track of a DJ Edit (DJ Version) release. |
| GET | /catalog/releases/{id}/images/ |
Return this release's images. |
| GET | /catalog/releases/{id}/uab/ |
|
| GET | /catalog/releases/{release_pk}/dj-edit-tracks/ |
|
| GET | /catalog/releases/{release_pk}/dj-edit-tracks/facets/ |
Filter facets (help text for each filter) |
| GET | /catalog/releases/{release_pk}/tracks/ |
|
| GET | /catalog/releases/{release_pk}/tracks/facets/ |
Filter facets (help text for each filter) |
| GET | /catalog/releases/{release_pk}/tracks/ids/ |
Return a list of Track IDs for the given release. |
| GET | /catalog/releases/facets/ |
Filter facets (help text for each filter) |
| GET | /catalog/releases/similar/ |
|
| GET | /catalog/releases/top/{num}/ |
Return top releases by global most popular rank, where N limited between 1 and 100. |
| GET | /catalog/search/ |
Search (see below) |
| GET | /catalog/search/facets/ |
Filter facets (help text for each filter) |
| GET | /catalog/sub-genres/ |
|
| GET | /catalog/sub-genres/{id}/ |
|
| GET | /catalog/sub-genres/{id}/tracks/ |
Return this sub-genre's tracks |
| GET | /catalog/sub-genres/facets/ |
Filter facets (help text for each filter) |
| GET | /catalog/tracks/ |
|
| GET | /catalog/tracks/{id}/ |
Return a territory restricted track. |
| GET | /catalog/tracks/{id}/audio-files/ |
Return track audio files. |
| GET | /catalog/tracks/{id}/beatbot/ |
|
| GET | /catalog/tracks/{id}/images/ |
Return this track's images. |
| GET | /catalog/tracks/{id}/publishers/ |
Return this Track's Publishers, updating if necessary |
| GET | /catalog/tracks/{id}/stream-sdk/ |
|
| GET | /catalog/tracks/{id}/uab/ |
|
| GET | /catalog/tracks/facets/ |
Filter facets (help text for each filter) |
| GET | /catalog/tracks/similar/ |
|
| GET | /catalog/tracks/store/{isrc}/ |
Return the store url for a track given the correct isrc. |
| GET | /catalog/tracks/top/{num}/ |
Return top tracks by global most popular rank, where N limited between 1 and 100. |
| GET | /catalog/tracks/top/openformat/{num}/ |
Return top Open Format tracks by global most popular rank, |
| Method | Path | Notes |
|---|---|---|
| GET | /my/account/ |
|
| PUT, PATCH | /my/account/{id}/ |
|
| GET | /my/account/facets/ |
Filter facets (help text for each filter) |
| PUT, PATCH | /my/account/myaccount/ |
|
| GET, POST | /my/beatport/ |
|
| GET | /my/beatport/artists/ |
|
| DELETE | /my/beatport/delete/ |
Delete subscription entries for the requesting user for the provided |
| GET | /my/beatport/facets/ |
Filter facets (help text for each filter) |
| GET | /my/beatport/labels/ |
|
| GET | /my/beatport/playlists/ |
|
| GET, POST | /my/beatport/tracks/ |
|
| GET, POST | /my/charts/ |
|
| GET, POST | /my/charts/{mycharts_pk}/tracks/ |
|
| PUT, PATCH, DELETE | /my/charts/{mycharts_pk}/tracks/{id}/ |
|
| GET | /my/charts/{mycharts_pk}/tracks/facets/ |
Filter facets (help text for each filter) |
| GET, PUT, PATCH, DELETE | /my/charts/{id}/ |
|
| GET, POST | /my/charts/{id}/images/ |
Return/create this chart's images. |
| GET | /my/charts/facets/ |
Filter facets (help text for each filter) |
| GET | /my/downloads/ |
|
| GET | /my/downloads/encode-status/ |
|
| GET | /my/downloads/encode-status/facets/ |
Filter facets (help text for each filter) |
| GET | /my/downloads/facets/ |
Filter facets (help text for each filter) |
| PATCH | /my/email-preferences/{id}/ |
|
| GET, POST | /my/genres/ |
|
| DELETE | /my/genres/{id}/ |
|
| GET | /my/genres/facets/ |
Filter facets (help text for each filter) |
| GET, POST | /my/playlists/ |
Return list of Playlists belonging to authenticated user. |
| GET, POST | /my/playlists/{myplaylists_pk}/tracks/ |
|
| PATCH, DELETE | /my/playlists/{myplaylists_pk}/tracks/{id}/ |
|
| POST, PATCH, DELETE | /my/playlists/{myplaylists_pk}/tracks/bulk/ |
|
| GET | /my/playlists/{myplaylists_pk}/tracks/facets/ |
Filter facets (help text for each filter) |
| GET | /my/playlists/{myplaylists_pk}/tracks/ids/ |
Return a list of PlaylistTrack IDs for the given playlist. |
| GET, PATCH, DELETE | /my/playlists/{id}/ |
Return a Playlist. |
| PATCH | /my/playlists/bulk/ |
Update playlist positions. |
| GET | /my/playlists/facets/ |
Filter facets (help text for each filter) |
| GET | /my/streaming-quality/ |
|
| PATCH | /my/streaming-quality/{id}/ |
|
| GET | /my/streaming-quality/facets/ |
Filter facets (help text for each filter) |
| Method | Path | Notes |
|---|---|---|
| GET | /curation/playlists/ |
|
| GET | /curation/playlists/{curatedplaylist_pk}/tracks/ |
|
| GET | /curation/playlists/{curatedplaylist_pk}/tracks/{id}/ |
|
| GET | /curation/playlists/{curatedplaylist_pk}/tracks/facets/ |
Filter facets (help text for each filter) |
| GET | /curation/playlists/{id}/ |
|
| GET | /curation/playlists/facets/ |
Filter facets (help text for each filter) |
| Method | Path | Notes |
|---|---|---|
| GET | /auxiliary/artist-types/ |
|
| GET | /auxiliary/artist-types/{id}/ |
|
| GET | /auxiliary/artist-types/facets/ |
Filter facets (help text for each filter) |
| GET | /auxiliary/commercial-model-types/ |
|
| GET | /auxiliary/commercial-model-types/facets/ |
Filter facets (help text for each filter) |
| Method | Path | Notes |
|---|---|---|
| GET | /db-health-check/ |
|
| GET | /health-check/ |
These paths return 401 (not 404) without a token, so they exist. The spec does not list them:
/catalog/keys/,/catalog/keys/{id}//catalog/chord-types/,/catalog/chord-types/{id}//catalog/sub-genres/{id}//auxiliary/current-status/,/auxiliary/sale-types/{id}/,/auxiliary/exclusive-period/{id}//auxiliary/item-types/,/auxiliary/page-module-types/,/auxiliary/page-types/,/auxiliary/source-types//curation/pages/,/curation/page-module-styles/
| Old path | Status | Use instead |
|---|---|---|
/catalog/artists/{id}/top-10-tracks/ |
404 |
/catalog/artists/{id}/top/{num}/ (num 1 to 100) |
/catalog/labels/{id}/top-10-tracks/ |
404 |
/catalog/labels/{id}/top/{num}/ |
/catalog/genres/{id}/top-10-tracks/ |
404 |
/catalog/genres/{id}/top/{num}/ |
/catalog/genres/{id}/top-10-releases/ |
404 |
/catalog/releases/top/{num}/ with filters, or /catalog/releases/?genre_id={id} |
/catalog/tracks/top/{genre_id}/ |
path exists, but {num} is a count |
/catalog/tracks/top/{num}/ is the global top N. For one genre, use /catalog/genres/{id}/top/{num}/ |
List endpoints take filters as query parameters. The /facets/ endpoint next to each list endpoint returns all filters with help text.
Filter syntax:
- OR lookup:
genre_id=5,6 - Dates:
publish_date=2026-01-01(exact),publish_date=:2026-01-01(on or before),publish_date=2026-01-01:(on or after),publish_date=2026-01-01:2026-02-01(range) - Numbers:
bpmtakes exact, less or greater than equal, and range, with the same:syntax - Keys:
key_name=A Minor,key_name=Ab Major,key_name=A# Minor(sharp#, flatb)
/catalog/tracks/ filters: artist_id, artist_name, available_worldwide, bpm, catalog_number, change_date, chord_type_id, current_status, dj_edits, dj_versions, enabled, encode_status, encoded_date, exclusive_date, exclusive_period, free_download_end_date, free_download_start_date, genre_enabled, genre_id, genre_name, guid, id, is_available_for_streaming, is_classic, is_explicit, is_hype, isrc, key_id, key_name, label_enabled, label_id, label_manager, label_name, label_name_exact, mix_name, name, new_release_date, pre_order_date, publish_date, publish_status, release_id, release_name, sale_type, sub_genre_id, supplier_id, supplier_name, track_number, type, type_id, ugc_remixes, was_ever_exclusive
/catalog/tracks/ order_by: publish_date, release_date, release_id, genre, label, name. Add - for descending, for example -publish_date.
/catalog/releases/ filters: artist_id, artist_name, catalog_number, created, current_status, enabled, encoded_date, exclusive, genre_id, genre_name, id, is_available_for_streaming, is_explicit, is_hype, label_enabled, label_id, label_name, label_name_exact, name, new_release_date, pre_order_date, publish_date, release_type_id, sub_genre_id, supplier_id, supplier_name, track_id, track_name, type, type_id, upc, updated
/catalog/releases/ order_by: id, label, name, publish_date, release_date, status (add - for descending)
/catalog/artists/ filters: created, enabled, id, name, name_exact, updated
/my/downloads/ takes the track filters plus purchase_date and received_status.
GET /catalog/search/?q={query}&type={type}&page=1&per_page=25
q: matches all fields. Matches onnamerank first.type:tracks,artists,labels,charts,releases,stems,stem-packs. Withouttype, the API searches all types.typeis mandatory when you add filters. Example:/catalog/search/?q=strobe&type=tracks&artist_name=deadmau5
Filters for each type:
| Type | Filters |
|---|---|
tracks |
artist_name, available_worldwide, bpm, change_date, current_status, genre_id, genre_name, guid, is_available_for_streaming, is_classic, isrc, key_name, label_name, mix_name, new_release_date, pre_order_date, publish_date, publish_status, release_name, was_ever_exclusive |
releases |
artist_name, created, label_name, pre_order_date, publish_date, release_type_id, upc |
artists |
created, enabled, updated |
labels |
enabled, is_available_for_streaming |
List endpoints return:
{
"count": 1000,
"next": "https://api.beatport.com/v4/...?page=2",
"previous": null,
"results": [...]
}Use page and per_page. The default per_page is 25.
The Track schema has these fields: id, name, mix_name, url, image, artists, remixers, release, label, genre, sub_genre, key, bpm, length_ms, isrc, catalog_number, price, publish_date, new_release_date, pre_order, pre_order_date, exclusive, exclusive_period, is_explicit, is_hype, is_classic, is_ugc_remix, is_dj_edit, is_dj_version, is_available_for_streaming, available_worldwide, current_status, sale_type, publishers, free_downloads, encode_status, encoded_date, streaming_date, audio_format, guid, number, desc, enabled, change_date, updated.
The track detail response also has sample_url, sample_start_ms, and sample_end_ms.
Example (shortened):
{
"id": 23011269,
"name": "Greece 2000",
"mix_name": "Max Styler Extended Rework",
"artists": [{"id": 8783, "name": "Three Drives", "slug": "three-drives"}],
"release": {"id": 5707323, "name": "Greece 2000 - Max Styler Rework", "label": {"id": 33099, "name": "Armada Music"}},
"genre": {"id": 7, "name": "Trance (Main Floor)"},
"bpm": 128,
"key": {"id": 16, "name": "A min", "camelot": "8A"},
"length_ms": 405000,
"sample_url": "https://geo-samples.beatport.com/track/{uuid}.LOFI.mp3",
"sample_start_ms": 130976,
"sample_end_ms": 250976
}| ID | Name |
|---|---|
| 1 | Drum & Bass |
| 3 | Electronica |
| 5 | House |
| 6 | Techno (Peak Time / Driving) |
| 7 | Trance (Main Floor) |
| 8 | Hard Dance / Hardcore / Neo Rave |
| 9 | Breaks / Breakbeat / UK Bass |
| 11 | Tech House |
| 12 | Deep House |
| 13 | Psy-Trance |
| 14 | Minimal / Deep Tech |
| 15 | Progressive House |
| 18 | Dubstep |
| 37 | Indie Dance |
| 38 | Trap / Future Bass |
| 39 | Dance / Pop |
| 50 | Nu Disco / Disco |
| 86 | UK Garage / Bassline |
| 89 | Afro House |
| 90 | Melodic House & Techno |
| 91 | Bass House |
| 92 | Techno (Raw / Deep / Hypnotic) |
| 96 | Mainstage |
Use GET /catalog/genres/ for the current list.
Do not use this API from outside Beatport. The host resolves to a private address (10.x.x.x), and connections from the public internet fail. Beatport's Next.js store calls it during server-side rendering. The paths are the same as the public API, so use https://api.beatport.com/v4 with a bearer token instead.
https://geo-samples.beatport.com/track/{uuid}.LOFI.mp3
The sample_start_ms and sample_end_ms fields set the preview window (usually 2 minutes).
https://geo-media.beatport.com/image_size/590x404/{uuid}.jpg
https://geo-media.beatport.com/image_size/{width}x{height}/{uuid}.jpg
Common sizes: 100x100, 250x250, 500x500, 590x404, 1400x1400.
The store uses Next.js with server-side rendering and React Query. Each page has the data in window.__NEXT_DATA__:
{
"page": "/track/[slug]/[id]",
"props": {
"pageProps": {
"dehydratedState": {
"queries": [
{"queryKey": ["track-23011269"], "state": {"data": {...}}}
]
}
}
}
}Query keys look like track-{id}, release-{id}, artist-{id}, label-{id}, genre-{id}, and search-all.
Beatport does not document rate limits. Recommended:
- Wait 500 ms between requests when you scrape.
- Cache responses.
- Expect
429when you send too many requests.
{"detail": "Authentication credentials were not provided."}301: a path without the trailing slash. Add/.302: a page that needs a login session.400: bad request.401: no token, or the token expired.403: no permission, or a wrong login on/auth/login/.404: the path does not exist.429: rate limited.
$response = $httpClient->request('GET', 'https://api.beatport.com/v4/catalog/tracks/23011269/', [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
'Accept' => 'application/json',
],
]);
$track = $response->toArray();$response = $httpClient->request('GET', 'https://api.beatport.com/v4/catalog/search/', [
'query' => ['q' => 'deadmau5', 'type' => 'tracks', 'page' => 1, 'per_page' => 25],
'headers' => ['Authorization' => 'Bearer ' . $accessToken],
]);$response = $httpClient->request('GET', 'https://api.beatport.com/v4/catalog/genres/5/top/100/', [
'headers' => ['Authorization' => 'Bearer ' . $accessToken],
]);$tracks = [];
$page = 1;
do {
$response = $httpClient->request('GET', 'https://api.beatport.com/v4/my/beatport/tracks/', [
'query' => ['page' => $page, 'per_page' => 100],
'headers' => ['Authorization' => 'Bearer ' . $accessToken],
]);
$data = $response->toArray();
$tracks = array_merge($tracks, $data['results']);
$hasMore = $data['next'] !== null;
$page++;
} while ($hasMore);- 2026-10-28: Endpoint tables regenerated from Beatport's OpenAPI spec (117 paths). Added where to get the client ID, the registered redirect URIs ("Mismatching redirect URI"), the flow without a client secret, the scope list, and the
account.beatport.comauth host. Marked the internal API as private. Listed removedtop-10-*endpoints. - 2026-02-05: Initial documentation created from frontend analysis
@kemo It worked, you are a LEGEND.
last question: since beatport doesn't have all this officially, do you think using this way has any merit to license problems ?