Home Pricing Docs Compare SDK Blog Changelog Status Contact Get API Key →

Single-file SDKs

Drop in. Zero build.

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.

Python

~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 →

Node

~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 →

Webhook signature verification

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.

Knowing what to retry: ingest.failed

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

reasonterminalmeaning
not_on_streamingyesNo 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_audioyesMatched, but the release carries no 30-second preview clip — and features are computed from preview audio.
throttlednoA music source rate-limited us — nothing wrong with the track.
cdn_forbiddennoThe preview URL was refused by its CDN and no fallback source had it.
download_failednoThe preview fetch failed before analysis.
analysis_failednoAudio downloaded, but analysis didn't complete.
internal_errornoOur 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.

Delivery, retries and failing endpoints

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.

Python

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

Node (Express)

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();
    });

Want a different language? Auto-generate one.

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:

Go

openapi-generator-cli generate \
  -i https://api.freqblog.com/openapi.json \
  -g go -o ./sdk-go --package-name freqblog

Ruby

openapi-generator-cli generate \
  -i https://api.freqblog.com/openapi.json \
  -g ruby -o ./sdk-ruby --additional-properties=gemName=freqblog

PHP

openapi-generator-cli generate \
  -i https://api.freqblog.com/openapi.json \
  -g php -o ./sdk-php --additional-properties=invokerPackage=FreqBlog

Rust

openapi-generator-cli generate \
  -i https://api.freqblog.com/openapi.json \
  -g rust -o ./sdk-rust --additional-properties=packageName=freqblog

Swift / Kotlin

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.

MCP server

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.