Skip to content

Instantly share code, notes, and snippets.

@kemo
Last active October 31, 2026 02:24
Show Gist options
  • Select an option

  • Save kemo/506ca56e35b9506ee5233bc4d773c1c8 to your computer and use it in GitHub Desktop.

Select an option

Save kemo/506ca56e35b9506ee5233bc4d773c1c8 to your computer and use it in GitHub Desktop.
# Beatport (Internal + External) API Documentation

Beatport API Documentation

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.

Overview

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

Authentication

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.

Where to get the client ID

  1. Open https://api.beatport.com/v4/docs/.
  2. 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).
  3. Search that file for API_CLIENT_ID. The bundle merges it into a config object and sets window.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:]]*'[^']+'"; done

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

"Mismatching redirect URI"

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 Location header of the 302 has ?code=....
  • Browser: the post-message page 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. Read code from the Location header of the 302, or from the URL of the post-message request.

Flow without a browser (what kemo/beatport-api-client LoginClient does)

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}

Auth hosts

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/

PKCE

Beatport's docs app does not send code_challenge or code_verifier. PKCE is not necessary with this client.

Scopes

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 more
  • user:*: user:dj, user:artist, user:label, user:supplier, user:anon, user:bs_sunset
  • staff:*: 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.

Request headers

Authorization: Bearer {access_token}
Accept: application/json

Public API endpoints (https://api.beatport.com/v4)

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.

Catalog

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,

My (user data, needs a user token)

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)

Curation

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)

Auxiliary

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)

Health

Method Path Notes
GET /db-health-check/
GET /health-check/

Endpoints that are not in the spec

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/

Removed or changed endpoints

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}/

Filters

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: bpm takes 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 #, flat b)

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

Search

GET /catalog/search/?q={query}&type={type}&page=1&per_page=25
  • q: matches all fields. Matches on name rank first.
  • type: tracks, artists, labels, charts, releases, stems, stem-packs. Without type, the API searches all types.
  • type is 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

Pagination

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.

Track fields

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
}

Known genre IDs

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.


Internal API (api-internal.beatportprod.com)

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.


Media URLs

Audio samples

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

Images

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.


Store frontend (www.beatport.com)

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.


Rate limiting

Beatport does not document rate limits. Recommended:

  • Wait 500 ms between requests when you scrape.
  • Cache responses.
  • Expect 429 when you send too many requests.

Errors

{"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.

Examples (PHP, Symfony HttpClient)

Get track details

$response = $httpClient->request('GET', 'https://api.beatport.com/v4/catalog/tracks/23011269/', [
    'headers' => [
        'Authorization' => 'Bearer ' . $accessToken,
        'Accept' => 'application/json',
    ],
]);

$track = $response->toArray();

Search tracks

$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],
]);

Top 100 tracks in one genre

$response = $httpClient->request('GET', 'https://api.beatport.com/v4/catalog/genres/5/top/100/', [
    'headers' => ['Authorization' => 'Bearer ' . $accessToken],
]);

Get the user library

$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);

Changelog

  • 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.com auth host. Marked the internal API as private. Listed removed top-10-* endpoints.
  • 2026-02-05: Initial documentation created from frontend analysis
@kemo

kemo commented Oct 28, 2026

Copy link
Copy Markdown
Author

@giorgilagidze this has been working as-is for years. It's the "unofficial official" way of doing it, just ensure your error handling is based around the API spec posted here.

GLHF

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment