Single-file SDKs
Hand-written Python and Node SDKs in a single file each. Clone, copy-paste, or curl them into your project. No npm install dance, no pip extras matrix to debug, no SDK version pinning to maintain. The SDK exists to save you typing — everything still works as a plain HTTP call.
~200 lines. Requires httpx only. Includes a one-liner quick_lookup() helper and a verify_webhook() method.
pip install httpx
curl -O https://freqblog.com/sdk/freqblog.py
Usage:
from freqblog import FreqBlog
with FreqBlog(api_key="sk_live_...") as fb:
track = fb.lookup("Blinding Lights", "The Weeknd")
print(track["bpm"], track["camelot"])
# Build a 20-track harmonic playlist
radio = fb.radio(track["itunes_track_id"], n=20)
track_ids = [t["itunes_track_id"] for t in radio["tracks"]]
# Export it — rekordbox, traktor, m3u/m3u8, csv (all fields) or cuesheet
# (serato/engine are accepted as aliases for rekordbox)
xml = fb.export("rekordbox", track_ids)
open("playlist.xml", "w").write(xml)
Download freqblog.py →
~150 lines. Zero dependencies — uses the native fetch built into Node 18+. CommonJS (require) — from an ESM project, save it as freqblog.cjs and import { FreqBlog } from './freqblog.cjs'. Includes signed-webhook verification.
curl -O https://freqblog.com/sdk/freqblog.js
Usage:
const { FreqBlog } = require('./freqblog');
(async () => {
const fb = new FreqBlog({ apiKey: 'sk_live_...' });
const track = await fb.lookup('Blinding Lights', 'The Weeknd');
console.log(track.bpm, track.camelot);
// Build a 20-track harmonic playlist
const radio = await fb.radio(track.itunes_track_id, { n: 20 });
const ids = radio.tracks.map(t => t.itunes_track_id);
// Export it — rekordbox, traktor, m3u/m3u8, csv (all fields) or cuesheet
// (serato/engine are accepted as aliases for rekordbox)
const xml = await fb.export('rekordbox', ids);
require('fs').writeFileSync('playlist.xml', xml);
})();
Download freqblog.js →
Completion webhooks (ingest.completed, ingest.failed, backfill.completed) are signed with X-FreqBlog-Signature: sha256=<hex> over <X-FreqBlog-Timestamp>.<rawBody>. The HMAC secret is sha256(sha256(your_api_key_plaintext)). Pass the timestamp header too — the verifier binds it and rejects deliveries older than ~5 minutes (replay guard). Both SDKs include a one-line verifier.
ingest.failedWhen an on-demand ingest can't complete we POST ingest.failed. It carries a machine-readable verdict so you can retry what's worth retrying and stop re-queueing what isn't — rather than applying one flat backoff to everything:
{
"type": "ingest.failed",
"status": "failed",
"track_name": "...",
"artist_name": "...",
"job_id": 123,
"timestamp": "2026-08-02T07:00:00Z",
"terminal": true,
"reason": "not_on_streaming",
"retry_after_days": 7,
"message": "This track isn't available on any streaming source we can analyse ..."
}
terminal: true means the verdict won't change until retry_after_days has elapsed — stop re-polling that track until then. terminal: false means the failure was transient and the track is worth another attempt (we retry it ourselves too). message is the human sentence, for your logs and support tickets — branch on reason, never on message. retry_after_days appears only when terminal is true.
reason | terminal | meaning |
|---|---|---|
not_on_streaming | yes | No acceptable match on Apple Music / Deezer — CD-only, unreleased, off-streaming or very regional. If you hold the audio yourself, upload it to POST /analyze. |
no_preview_audio | yes | Matched, but the release carries no 30-second preview clip — and features are computed from preview audio. |
throttled | no | A music source rate-limited us — nothing wrong with the track. |
cdn_forbidden | no | The preview URL was refused by its CDN and no fallback source had it. |
download_failed | no | The preview fetch failed before analysis. |
analysis_failed | no | Audio downloaded, but analysis didn't complete. |
internal_error | no | Our fault. Retry. |
not_on_streaming and no_preview_audio are the same strings a terminal /lookup 404 returns in its own reason field, with the same terminal and retry_after_days meaning — so one branch in your client handles both. Treat the list as open: a future release may add a reason, so default anything you don't recognise to “retry later” rather than rejecting the delivery.
Each event is attempted up to three times (immediately, then after 2s and 6s) with an 8-second timeout. Any 2xx counts as delivered. A 4xx other than 429 is treated as permanent and is not retried — so don't return 400/404 for a payload you'd like redelivered; return 5xx or 429.
If an endpoint fails 15 deliveries in a row we pause delivery to it rather than keep hammering a receiver that is clearly down. That pause is temporary: while it's in effect we send one event per hour as a probe, and the first probe you answer with a 2xx resumes normal delivery at once. You don't need to re-register your URL, and we monitor for endpoints stuck in this state — if yours is one, expect to hear from us.
Never treat a webhook as your only copy. Delivery is best-effort by design; the analysis is not. Every ingest lands in the catalogue regardless, so a /lookup for the same track returns the full record whenever you ask — the reliable pattern is to use the webhook as a signal to fetch (that's what result_url is for) and to reconcile anything you never heard about with a periodic sweep.
from flask import request
@app.post("/freqblog-webhook")
def webhook():
body = request.get_data()
sig = request.headers.get("X-FreqBlog-Signature", "")
ts = request.headers.get("X-FreqBlog-Timestamp", "")
if not fb.verify_webhook(sig, body, ts):
return "", 401
payload = request.get_json()
print("ingest done:", payload["track_name"], payload.get("result_url"))
return "", 200
app.post('/freqblog-webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
if (!fb.verifyWebhook(req.headers['x-freqblog-signature'], req.body,
req.headers['x-freqblog-timestamp'])) {
return res.status(401).end();
}
const payload = JSON.parse(req.body.toString('utf-8'));
console.log('ingest done:', payload.track_name, payload.result_url);
res.status(200).end();
});
The full OpenAPI spec is at api.freqblog.com/openapi.json. Pipe it into openapi-generator and pick from 50+ language targets. One-line commands for the most common ones:
openapi-generator-cli generate \
-i https://api.freqblog.com/openapi.json \
-g go -o ./sdk-go --package-name freqblog
openapi-generator-cli generate \
-i https://api.freqblog.com/openapi.json \
-g ruby -o ./sdk-ruby --additional-properties=gemName=freqblog
openapi-generator-cli generate \
-i https://api.freqblog.com/openapi.json \
-g php -o ./sdk-php --additional-properties=invokerPackage=FreqBlog
openapi-generator-cli generate \
-i https://api.freqblog.com/openapi.json \
-g rust -o ./sdk-rust --additional-properties=packageName=freqblog
openapi-generator-cli generate \
-i https://api.freqblog.com/openapi.json \
-g swift5 -o ./sdk-swift --additional-properties=projectName=FreqBlog
openapi-generator-cli generate \
-i https://api.freqblog.com/openapi.json \
-g kotlin -o ./sdk-kotlin --additional-properties=packageName=com.freqblog
All of the above pull straight from our live OpenAPI spec, which is auto-generated from the Pydantic models — so the client always tracks the API exactly. Pass your X-Api-Key via the standard header configuration on whichever client class the generator produces.
The fastest way to give an AI assistant access to the API is our hosted MCP connector — zero install, no API key. In Claude: Settings → Connectors → Add custom connector and paste:
https://mcp.freqblog.com/mcp
# Cursor (~/.cursor/mcp.json)
{"mcpServers":{"freqblog-music":{"url":"https://mcp.freqblog.com/mcp"}}}
# Windsurf (~/.codeium/windsurf/mcp_config.json) — note serverUrl, not url
{"mcpServers":{"freqblog-music":{"serverUrl":"https://mcp.freqblog.com/mcp"}}}
The hosted connector exposes 12 tools (audio features, batch, search, BPM & key discovery, harmonic keys, plus transition scoring, next-track, setlist, recommendations, related artists & track tagging). For a local server with your own key and the full 23-tool set — harmonic radio, DJ-format export, country charts, lyrics, waveforms, cover art and more — use the music-metadata-mcp npm package (v2.8.0):
# Claude Desktop — macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
# Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"music-metadata": {
"command": "npx",
"args": ["music-metadata-mcp", "--api-key=sk_live_YOUR_KEY"]
}
}
}
# Cursor (~/.cursor/mcp.json)
{"mcpServers":{"music-metadata":{"type":"stdio","command":"npx","args":["music-metadata-mcp","--api-key=sk_live_YOUR_KEY"]}}}
# Windsurf (~/.codeium/windsurf/mcp_config.json) — command/args, not serverUrl
{"mcpServers":{"music-metadata":{"command":"npx","args":["music-metadata-mcp","--api-key=sk_live_YOUR_KEY"]}}}
Once installed, your AI can build harmonic playlists, identify recordings, export Rekordbox XML and more — without writing any HTTP code. Source: npmjs.com/package/music-metadata-mcp.