Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Freqblog Com Spotify Compat API
version: 1.5.0
contact:
name: FreqBlog
url: https://freqblog.com/
email: hello@freqblog.com
license:
name: Commercial — see freqblog.com/terms.html
url: https://freqblog.com/terms.html
description: 'Operations tagged Spotify-compat across 2 of this provider''s published API definitions: freqblog-com-openapi.json, freqblog-com-openapi1.json. Each path carries the servers of the definition it was published in.'
servers:
- url: https://api.freqblog.com
description: Production
security:
- APIKeyHeader: []
tags:
- name: Spotify-compat
description: Spotify-shape-compatible routes for endpoints deprecated 2024-11-27. Field names and response structure match Spotify byte-for-byte; numeric values come from this pipeline's open-source analysis and open datasets. Values are FreqBlog's own, not Spotify's — re-derive any thresholds on our values. See /v1/audio-features description and the Field-by-Field Threshold Guide at /blog/spotify-audio-features-migration-thresholds/.
paths:
/v1/audio-features/{identifier}:
get:
tags:
- Spotify-compat
summary: Get audio features — Spotify-shape compatible
description: '**Spotify-shape compatible replacement for the deprecated `GET /v1/audio-features/{id}`.** Change the host (`api.spotify.com` → `api.freqblog.com`) and the path + response field names match byte-for-byte: `acousticness`, `danceability`, `energy`, `instrumentalness`, `key` (0–11, `-1` if undetected), `liveness`, `loudness` (dB), `mode` (0/1), `speechiness`, `tempo` (BPM), `time_signature`, `valence`, plus `id` / `uri` / `track_href` / `analysis_url` / `duration_ms` / `type`.
`{identifier}` is auto-detected: a 22-character base-62 string → Spotify track ID (also accepts `spotify:track:…` URIs and `open.spotify.com/track/…` URLs); a 12-character ISRC (e.g. `USUM71900001`, hyphens optional) → ISRC lookup (catalog first, then MusicBrainz resolves the recording on a miss). A Spotify track ID is resolved per request: we read the track''s **title** from Spotify''s public embed page and match it against our catalogue. Spotify IDs are never stored — a resolution is remembered in memory for at most 24 hours. A title that several catalog artists share is **ambiguous and returns 404 rather than a guess** — the wrong recording would mean the wrong bpm and key — so this is still not a universal Spotify-ID reverse lookup. For reliable coverage, identify tracks by **name or ISRC**: `GET /lookup?track=&artist=` searches the full catalog and queues an on-demand fetch + analysis on a miss; if your own Spotify integration already gives you the ISRC (`external_ids.isrc`), call this same route as `GET /v1/audio-features/` or `GET /lookup?isrc=`.
Notes — the *values* are FreqBlog''s own measurements, on our own scale, not Spotify''s numbers: re-derive any thresholds on our values rather than copying Spotify''s (see the Field-by-Field Guide).
`?calibrate=spotify` is deprecated and ignored (since 2026-09-26): values are always returned uncalibrated.
`analysis_url` is always `null` (no per-segment-analysis analog). For tracks resolved by ISRC, `uri` / `track_href` are `null` and `id` carries the ISRC, since we have no Spotify ID for the recording. Error bodies use FreqBlog''s `{"detail": …}` shape, not Spotify''s `{"error": {…}}`. Counts as one quota request. For batches, use the Spotify-shaped sibling: `GET /v1/audio-features?ids=…` (comma-separated, up to 100 IDs).'
operationId: spotify_audio_features_endpoint_v1_audio_features__identifier__get
parameters:
- name: identifier
in: path
required: true
schema:
type: string
minLength: 1
maxLength: 128
description: A Spotify track ID (22-char base-62, or a `spotify:track:…` URI / open.spotify.com URL) — or an ISRC (12-char, e.g. `USUM71900001`; hyphens optional).
title: Identifier
description: A Spotify track ID (22-char base-62, or a `spotify:track:…` URI / open.spotify.com URL) — or an ISRC (12-char, e.g. `USUM71900001`; hyphens optional).
- name: calibrate
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Deprecated and ignored since 2026-09-26 — accepted so existing clients keep working, but values are always returned uncalibrated, whatever the value. (It applied a loudness regression fitted to Spotify's own values, which Spotify's Developer Terms do not permit.)
title: Calibrate
description: Deprecated and ignored since 2026-09-26 — accepted so existing clients keep working, but values are always returned uncalibrated, whatever the value. (It applied a loudness regression fitted to Spotify's own values, which Spotify's Developer Terms do not permit.)
deprecated: true
- name: x-api-key
in: header
required: false
schema:
type: string
title: X-Api-Key
responses:
'200':
description: Audio features in Spotify's `AudioFeaturesObject` shape
content:
application/json:
schema: {}
example:
acousticness: 0.04
danceability: 0.76
duration_ms: 200040
energy: 0.91
id: 0VjIjW4GlUZAMYd2vXMi3b
key: 1
loudness: -7.1
mode: 0
tempo: 171.0
time_signature: 4
track_href: https://api.spotify.com/v1/tracks/0VjIjW4GlUZAMYd2vXMi3b
type: audio_features
uri: spotify:track:0VjIjW4GlUZAMYd2vXMi3b
valence: 0.51
'401':
description: 'Missing, unrecognised, deactivated or superseded API key, or a free key whose account can no longer hold one — `error_code` says which: `KEY_MISSING` / `KEY_DEACTIVATED` / `KEY_SUPERSEDED` / `KEY_FREE_UNAVAILABLE`'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
description: No track found for that Spotify track ID / ISRC. **For an ISRC we could identify but don't yet hold, this 404 also queues an on-demand fetch — retry the same call in ~15-30s and it usually resolves.** A Spotify ID is resolved per request from the track's title (read from Spotify's public embed page) — so it 404s when nothing in the catalog carries that title, or when several artists share it and the right recording is ambiguous (we return 404 rather than guess). For reliable coverage, identify the track by name (`GET /lookup?track=<title>&artist=<artist>`, which fetches on a miss) or by ISRC (`GET /lookup?isrc=<ISRC>`)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'422':
description: '`identifier` is neither a 22-char Spotify track ID nor a 12-char ISRC'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'429':
description: Monthly quota exceeded, or too many concurrent single-ID lookups for this key (per-key in-flight cap shared with /lookup — free 6, paid 10). Back off per Retry-After, or send a large migration through the batch form `GET /v1/audio-features?ids=…`.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'503':
description: The resolve exceeded its internal deadline (kept below nginx's 30s) under load — transient. Retry per Retry-After, or use the time-boxed batch form `GET /v1/audio-features?ids=…` / `POST /bulk`, which return partial results instead of timing out.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
servers:
- url: https://api.freqblog.com
description: Production
/v1/audio-features:
get:
tags:
- Spotify-compat
summary: Get audio features for several tracks — Spotify-shape compatible (batch)
description: '**Spotify-shape compatible replacement for the deprecated `GET /v1/audio-features?ids=…`.** Up to **100** comma-separated identifiers per request. Returns `{"audio_features": [...]}` — the array length and order match the input; entries are `null` for IDs that don''t resolve (matching Spotify''s behaviour).
Each `ids` entry is auto-detected exactly like the single-ID route: a 22-char base-62 string → Spotify track ID (also accepts `spotify:track:…` URIs and `open.spotify.com/track/…` URLs); a 12-char ISRC (e.g. `USUM71900001`, hyphens optional) → ISRC lookup. Identifiers that match neither shape fill their slot with `null` rather than failing the whole batch.
As with the single-ID route, a Spotify ID is resolved per request by reading the track''s **title** from Spotify''s public embed page (Spotify IDs are never stored). Slots come back `null` when nothing in the catalog carries that title, or when several artists share it and the right recording is ambiguous — we return `null` rather than guess, since the wrong recording would mean the wrong bpm and key.
**A `null` ISRC slot now queues an on-demand fetch.** When we can identify the recording behind an ISRC but don''t hold it yet, the slot comes back `null` (and uncharged) *and* an ingest is queued — so **re-sending the same call in ~15-30s typically returns the data**. Retrying a null-heavy batch once is worthwhile; it wasn''t before. Spotify-ID slots can''t do this: an unresolvable Spotify ID has no track name to fetch by. For reliable coverage, identify tracks by **name or ISRC**: `GET /lookup?track=&artist=` searches the full catalog and fetches on a miss, and ISRCs (e.g. from your own Spotify integration''s `external_ids.isrc`) resolve via the same batch route or `GET /lookup?isrc=`.
**Quota — one token per ID that returns data.** Each resolved ID counts as one quota request; IDs that come back `null` — an unresolvable Spotify ID, an ISRC with no match, or one not resolved before the time-box — are **not** charged, exactly like the single-ID route, which is free on a 404 miss. So a 100-ID batch of raw Spotify IDs where only 5 resolve bills 5, not 100. Spotify billed a 100-ID batch as 1 call; we bill per *resolved* ID, because each resolved lookup runs its own catalog query (and potentially a MusicBrainz roundtrip on an ISRC). See `/pricing` for higher tiers.
Other notes from the single-ID route apply: the *values* are FreqBlog''s own measurements, on our own scale (see the Field-by-Field Guide); `analysis_url` is always `null`; errors use FreqBlog''s `{"detail": …}` shape, not Spotify''s `{"error": {…}}`.
**Resolved in parallel server-side, bounded for stability.** A large batch of uncatalogued IDs can take a while (each ISRC miss is a MusicBrainz roundtrip); if it exceeds the server time-box you get **partial results** — IDs resolved in time carry features, the rest come back `null` (and aren''t charged) and resolve on a retry. A small number of `/bulk` + `/v1/audio-features` batch requests per key run at once — excess get `429` with `Retry-After`.'
operationId: spotify_audio_features_batch_endpoint_v1_audio_features_get
parameters:
- name: ids
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Comma-separated identifiers (max 100). Each is either a Spotify track ID (22-char base-62; also accepts `spotify:track:…` URIs and `open.spotify.com/track/…` URLs) or an ISRC (12-char, e.g. `USUM71900001`; hyphens optional). Only IDs that resolve to data are charged — unresolvable IDs return `null` for free. No raw Spotify IDs? Identify tracks by name via `GET /lookup?track=<title>&artist=<artist>`.
examples:
- USUM71900001,003vvx7Niy0yvhvHt4a68B,USUMV1902060
title: Ids
description: Comma-separated identifiers (max 100). Each is either a Spotify track ID (22-char base-62; also accepts `spotify:track:…` URIs and `open.spotify.com/track/…` URLs) or an ISRC (12-char, e.g. `USUM71900001`; hyphens optional). Only IDs that resolve to data are charged — unresolvable IDs return `null` for free. No raw Spotify IDs? Identify tracks by name via `GET /lookup?track=<title>&artist=<artist>`.
- name: calibrate
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Deprecated and ignored since 2026-09-26 — accepted so existing clients keep working, but values are always returned uncalibrated, whatever the value. (It applied a loudness regression fitted to Spotify's own values, which Spotify's Developer Terms do not permit.)
title: Calibrate
description: Deprecated and ignored since 2026-09-26 — accepted so existing clients keep working, but values are always returned uncalibrated, whatever the value. (It applied a loudness regression fitted to Spotify's own values, which Spotify's Developer Terms do not permit.)
deprecated: true
- name: x-api-key
in: header
required: false
schema:
type: string
title: X-Api-Key
responses:
'200':
description: 'Batch audio features in Spotify''s `{audio_features: [...]}` shape; entries are `null` for IDs that don''t resolve (Spotify''s behaviour)'
content:
application/json:
schema: {}
example:
audio_features:
- acousticness: 0.04
danceability: 0.76
energy: 0.91
id: 0VjIjW4GlUZAMYd2vXMi3b
key: 1
mode: 0
tempo: 171.0
type: audio_features
uri: spotify:track:0VjIjW4GlUZAMYd2vXMi3b
'...': (remaining Spotify fields)
- null
- id: USUM71900001
key: 5
tempo: 85.39
type: audio_features
track_href: https://api.freqblog.com/lookup?isrc=USUM71900001
'...': (remaining Spotify fields)
'400':
description: '`ids` missing, empty, or longer than 100. Supply `?ids=<comma-separated Spotify track IDs or 12-char ISRCs, up to 100>`. No raw Spotify IDs? Identify tracks by name via `GET /lookup?track=<title>&artist=<artist>`'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: 'Missing, unrecognised, deactivated or superseded API key, or a free key whose account can no longer hold one — `error_code` says which: `KEY_MISSING` / `KEY_DEACTIVATED` / `KEY_SUPERSEDED` / `KEY_FREE_UNAVAILABLE`'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'429':
description: Monthly quota insufficient, or too many concurrent batch requests for this key (shared with /bulk)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'503':
description: Reserved for genuine service unavailability. The server time-box no longer fails a slow batch — IDs unresolved in time come back `null` in the 200 response (and aren't charged); retry to collect them.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
servers:
- url: https://api.freqblog.com
description: Production
components:
schemas:
ValidationError:
properties:
loc:
items:
anyOf:
- type: string
- type: integer
type: array
title: Location
msg:
type: string
title: Message
type:
type: string
title: Error Type
type: object
required:
- loc
- msg
- type
title: ValidationError
ErrorResponse:
properties:
detail:
type: string
title: Detail
error_code:
anyOf:
- type: string
- type: 'null'
title: Error Code
type: object
required:
- detail
title: ErrorResponse
description: 'A JSON error. `detail` is for humans, `error_code` is for code.
⚠ Branch on `error_code`, never on `detail`. `detail` is prose we deliberately keep
improving — five of these messages were reworded on 2026-08-13 alone — so matching on
it is a breaking change waiting to happen. `error_code` is UPPER_SNAKE, always present
on an error, and its meaning is frozen once shipped. Full registry: app/errors.py.'
HTTPValidationError:
properties:
detail:
items:
$ref: '#/components/schemas/ValidationError'
type: array
title: Detail
type: object
title: HTTPValidationError
ErrorResponse_2:
properties:
detail:
type: string
title: Detail
error_code:
type:
- string
- 'null'
title: Error Code
type: object
required:
- detail
title: ErrorResponse
description: 'A JSON error. `detail` is for humans, `error_code` is for code.
⚠ Branch on `error_code`, never on `detail`. `detail` is prose we deliberately keep
improving — five of these messages were reworded on 2026-08-13 alone — so matching on
it is a breaking change waiting to happen. `error_code` is UPPER_SNAKE, always present
on an error, and its meaning is frozen once shipped. Full registry: app/errors.py.'
securitySchemes:
APIKeyHeader:
type: apiKey
in: header
name: X-Api-Key
x-refined-from:
- freqblog-com-openapi.json
- freqblog-com-openapi1.json