Part of Jam Jam, a free chord, key and tempo detector: overview · web tool · Android · iOS beta · models and sounds
Quick start · How a job works · Mastering · Keys and scopes · Endpoints · AI assistants
Runnable examples for the Jam Jam REST API, in curl, JavaScript (Node 20+, no dependency) and Python (3.10+, requests). The API turns music files, links and text into a songbook (chords, lyrics, structure) or into portable exports, with the same conversion engine as the Jam Jam app.
What comes out: Jam Jam's songbook format, PDF, plain text, ChordPro, PNG, HTML, synced lyrics (.lrc, .srt), and, from a source that carries a melody, MIDI and MusicXML. The API can also read and write your private notebook (songs and playlists), read what other accounts have published, and master an audio file on the server.
- Base URL:
https://jam-jam.org/api/v1 - Contract (OpenAPI 3): https://jam-jam.org/api/v1/openapi.json (source of truth, read it for every field)
- Documentation: jam-jam.org/developers
The examples were written against contract version 0.6.0. If this README and the contract ever disagree, the contract wins.
1. Get an API key. Create a free account at jam-jam.org, then open Settings, API keys in the app (on the web: https://jam-jam.org/app/prefs/api-keys) and create a key. Tick the scopes you need: convert:read and convert:write are enough for the examples below (details in API keys and scopes).
2. Load it. The key starts with jj_live_ and is shown once. Copy it into .env:
cp .env.example .env # then edit .env
set -a; . ./.env; set +a # load it into your shell
3. Convert a song. From the repository root, with the sample samples/amazing-grace.cho, a public domain song:
curl/03-convert-text.sh txt # ChordPro text -> plain text, prints the job (note its "id")
curl/04-poll-job.sh <job_id> # poll until done
curl/06-download-result.sh <job_id> # saves the file in ./out
That is the whole loop. The same thing exists in JavaScript and Python below.
All examples read JAM_JAM_API_KEY from the environment and run from anywhere in the repository. The default source is samples/amazing-grace.cho.
curl/01-list-formats.sh # no key needed
curl/03-convert-text.sh txt # ChordPro text -> plain text, prints the job (note its "id")
curl/04-poll-job.sh <job_id> # poll until done
curl/06-download-result.sh <job_id> # saves the file in ./out
curl/07-download-songbook.sh <job_id> # the songbook JSON
Other scripts: 02-convert-file.sh (upload), 05-follow-job-sse.sh (Server-Sent Events), 08-usage.sh.
The same thing by hand:
curl -X POST https://jam-jam.org/api/v1/jobs \
-H "Authorization: Bearer $JAM_JAM_API_KEY" \
-H "Idempotency-Key: my-first-job-$(date +%s)" \
-F file=@samples/amazing-grace.cho \
-F inputFormat=chordpro \
-F output=pdf-chords
cd javascript
node list-formats.mjs [inputFormat]
node convert.mjs text # the sample song -> pdf-chords
node convert.mjs text ../samples/amazing-grace.cho --output txt
node convert.mjs file ../samples/amazing-grace.cho --output chordpro --input chordpro
node convert.mjs url https://example.com/a-page-you-may-convert --output songbook
node follow-events.mjs <job_id>
node usage.mjs
node notebook.mjs list # needs carnet:read
node notebook.mjs add # needs carnet:write, adds a public domain song
The core of it (lib.mjs)
const API = 'https://jam-jam.org/api/v1';
const headers = process.env.JAM_JAM_API_KEY
? { Authorization: `Bearer ${process.env.JAM_JAM_API_KEY}` }
: {};
let res = await fetch(`${API}/jobs`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
body: JSON.stringify({ text: '{title: Demo}\n[C]Hello [G]world', inputFormat: 'chordpro', output: 'txt' }),
});
let job = await res.json();
while (!['done', 'failed', 'cancelled'].includes(job.phase)) {
await new Promise((r) => setTimeout(r, (Number(res.headers.get('retry-after')) || 2) * 1000));
res = await fetch(`${API}/jobs/${job.id}`, { headers });
job = await res.json();
}
cd python
pip install -r requirements.txt
python list_formats.py [input_format]
python convert.py text # the sample song -> pdf-chords
python convert.py file ../samples/amazing-grace.cho --output txt --input chordpro
python convert.py url https://example.com/a-page-you-may-convert --output songbook
python follow_events.py <job_id>
python usage.py
python notebook.py list # needs carnet:read
python notebook.py add # needs carnet:write
The core of it (jamjam.py)
import os, time, uuid, requests
API = "https://jam-jam.org/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['JAM_JAM_API_KEY']}"} if os.environ.get("JAM_JAM_API_KEY") else {}
res = requests.post(f"{API}/jobs", headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
json={"text": "{title: Demo}\n[C]Hello [G]world", "inputFormat": "chordpro", "output": "txt"})
res.raise_for_status()
job = res.json()
while job["phase"] not in ("done", "failed", "cancelled"):
time.sleep(int(res.headers.get("Retry-After") or 2))
res = requests.get(f"{API}/jobs/{job['id']}", headers=HEADERS)
job = res.json()
- Start.
POST /jobstakes a file (multipart/form-data), a URL or text (application/json). It answers202with a job; a replayedIdempotency-Keyanswers200with the same job. - Follow. Poll
GET /jobs/{id}and honourRetry-After, or openGET /jobs/{id}/eventsfor the same progress as Server-Sent Events (state,done,error,ping). - Download.
GET /jobs/{id}/resultgives the main result file (?artifact=<id>for another one), andGET /jobs/{id}/result/songbookthe songbook JSON, available after any successful job.
The job fields used here: id, phase (queued, running, done, failed, cancelled), percent, inputFormat, outputFormat, expiresAt, steps, artifacts (id, kind, format, mediaType, bytes, sha256, filename), problem (set when phase is failed) and links.
Jam Jam masters your track on the server: it analyses the file, balances the tone, tames sibilance, adjusts the stereo width and brings the level to a loudness target (streaming, podcast, club...), with the same settings as the free web tool at https://jam-jam.org/app/mastering (the server render is reproducible, but not bit for bit the browser export). Upload an audio file, pick a preset, download a 48 kHz WAV. On the server, the same file, settings and engineVersion give the same output.
- Input: WAV, MP3, FLAC, Ogg, AAC, M4A and similar, 200 MB and 10 minutes at most. Mono or stereo (beyond two channels, the first two are used).
- Output: a WAV at 48 kHz, 16-bit (
format=wav-pcm16, the default), 24-bit (wav-pcm24) or 32-bit float (wav-f32), named<name>-master.wav. Kept for 24 hours; downloading again is free. - Presets: five are built in and read-only, used as
preset=builtin:<id>:builtin:balanced,builtin:warm,builtin:bright,builtin:punchy,builtin:crushed. You can save up to 50 of your own (name of 60 characters at most) and use their uuid. Or send the settings inline assettings. A render takes exactly one ofpresetandsettings. - Settings (
MasteringSettingsV1):autoEq,lows,mids,highs,deEss,width,loudness,destruct(each avalueandenabled), atarget(streaming,apple,deezer,podcast,club) andnormalize. Ranges and steps are inGET /mastering/capabilities; a value outside its range or off its step answers422 invalid-settingswith one entry per problem. Example:samples/mastering-settings.json. - Access and quota: rendering is open to every account, free or Premium. A free account gets 3 renders per ISO week (resets Monday 00:00 UTC), on the same counter as the web tool; Premium has no cap. Beyond, a render answers
429 quota-exhausted, checked before the upload. A render is counted when submitted and refunded if it fails or is cancelled.GET /mastering/capabilitieswith your key tells you (renderAllowed,quota), and a render answersX-Mastering-Quota-*headers. - Idempotency:
Idempotency-Keyis required on every Mastering write: 16 to 80 characters amongA-Za-z0-9_-(the examples use a fresh UUID). Replaying it with the same body returns the same result without a second charge.
The flow is the one of a conversion: POST /mastering/jobs (202), poll GET /mastering/jobs/{id} honouring Retry-After until status is done (it goes through queued, decoding, analyzing, rendering; the other final states are failed, cancelled and expired), then GET /mastering/jobs/{id}/result. The finished job carries a report with the loudness before and after. An account has one active render and three waiting at most.
Create a key with mastering:read and mastering:write (see API keys and scopes). samples/make-test-tone.py writes a 5-second synthetic WAV, so you can try without your own track.
python3 samples/make-test-tone.py # writes samples/test-tone.wav
curl/09-mastering-capabilities.sh # limits, ranges, built-in presets
curl/10-mastering-create-preset.sh "My warm preset" # saves samples/mastering-settings.json, prints the preset id
curl/11-mastering-render.sh samples/test-tone.wav # built-in preset builtin:balanced, prints the job
curl/11-mastering-render.sh samples/test-tone.wav builtin:warm wav-f32
curl/11-mastering-render.sh samples/test-tone.wav samples/mastering-settings.json # custom settings, sent inline
curl/12-mastering-poll.sh <job_id> # until done, prints the report
curl/13-mastering-download.sh <job_id> # saves ./out/<name>-master.wav
curl/14-mastering-delete.sh job <job_id> # or: preset <preset_id>
# Node 20+
cd javascript
node mastering.mjs capabilities
node mastering.mjs master ../samples/test-tone.wav --preset builtin:warm
node mastering.mjs master song.mp3 --settings ../samples/mastering-settings.json --format wav-f32
node mastering-batch.mjs ~/Music/mixes --preset builtin:balanced # a whole folder, one file after the other
# Python 3.10+
cd python
python mastering.py capabilities
python mastering.py create-preset "My warm preset"
python mastering.py master ../samples/test-tone.wav --preset builtin:warm
python mastering_batch.py ~/Music/mixes --preset builtin:balanced
The batch scripts skip a file whose <name>-master.wav already exists (so you can rerun them after an interruption) and stop at the first 403, or at a 429 that asks for a long wait.
Errors. As everywhere in the API, an error is application/problem+json with a stable code. The examples print a plain message for:
| Status | code |
Meaning |
|---|---|---|
| 401 | missing or invalid key | |
| 403 | insufficient-scope |
the key lacks mastering:read or mastering:write |
| 403 | premium-required |
rendering is not open to your tier; depending on your tier, see https://jam-jam.org/app/developers |
| 409 | not-ready |
the result is requested before the render is finished |
| 410 | expired |
the 24 hours are over |
| 413, 415, 422 | file too large, unsupported file, audio over 10 minutes, invalid settings (read errors[]), idempotency-conflict |
|
| 429 | quota-exhausted or a rate limit |
wait Retry-After seconds, do not retry in a loop |
Only master audio you hold the rights over. Documentation: jam-jam.org/developers. The free tool, in the browser: https://jam-jam.org/app/mastering.
- Create keys in the app (web or mobile): Settings, API keys (on the web: https://jam-jam.org/app/prefs/api-keys).
- Tick the scopes you need. A key cannot be widened afterwards: create another one and revoke the old one.
- The key starts with
jj_live_and is shown once. You can have up to 5 keys per account, each revocable at any time. - Never commit a real key (
.envis in.gitignore).
| Scope | What it allows |
|---|---|
convert:read |
GET /jobs/{id}, /jobs/{id}/events, /jobs/{id}/result*, /me/usage |
convert:write |
POST /jobs, DELETE /jobs/{id} |
carnet:read |
GET /me/songs*, GET /me/playlists*, /me/usage |
carnet:write |
POST/PUT/PATCH/DELETE on /me/songs* and /me/playlists*, /me/usage |
social:read |
GET /users/{handle}*, /me/followers, /me/following, /me/usage |
mastering:read |
GET /mastering/presets, GET /mastering/jobs/{id}, GET /mastering/jobs/{id}/result |
mastering:write |
POST /mastering/presets, DELETE /mastering/presets/{id}, POST /mastering/jobs, DELETE /mastering/jobs/{id} |
stems:write, audio:write, queue:read |
stem separation and the audio processing queue, see the contract |
A missing scope answers 403 with code: insufficient-scope. Give an integration only what it needs: converting needs convert:write and convert:read; importing songs into your notebook adds carnet:write.
GET /formats, GET /health and GET /openapi.json need no key. The contract also describes anonymous conversions (no Authorization header, a very small quota).
Note
In practice the anti-bot protection in front of the API can answer 403 (an HTML challenge, not an API error) to scripts that send no key, so plan on using a key. The examples only send the Authorization header when JAM_JAM_API_KEY is set.
Three independent counters, taken from the contract. The exact balance, window and reset time of your identity is always in GET /me/usage and in the response headers (X-Quota-* for conversions, X-Read-Quota-*, X-Write-Quota-*).
| Counter | Free account | Premium | Anonymous |
|---|---|---|---|
Conversions (POST /jobs, same counter as exporting from the notebook in the app) |
10 per week | 500 per month | 3 per 24 hours |
Reads (/me/songs*, /me/playlists*, /users/*, followers) |
2000 per day | 20000 per day | none |
| Notebook writes | 200 per day | 5000 per day | none |
Stem separation has its own counters, and API keys need a Premium account for it: 200 per UTC day with POST /jobs and output=stems; on /stem-tool/*, 10 per day and 100 per month with the fast model, 2 per day and 10 per month with quality. Mastering renders have their own counter too (see Mastering). Going over a limit answers 429 with a Retry-After header: wait that long instead of retrying in a loop.
| Method | Path | Scope | What it does |
|---|---|---|---|
| GET | /formats |
none | Input and output formats this server serves, with limits and tiers |
| POST | /jobs |
convert:write |
Start a conversion: a file (multipart/form-data), a URL or text (application/json) |
| GET | /jobs/{id} |
convert:read |
Job state (poll, honour Retry-After) |
| GET | /jobs/{id}/events |
convert:read |
The same progress as Server-Sent Events (state, done, error, ping) |
| GET | /jobs/{id}/result |
convert:read |
Download the main result file (?artifact=<id> for another one) |
| GET | /jobs/{id}/result/songbook |
convert:read |
The songbook JSON, available after any successful job |
| DELETE | /jobs/{id} |
convert:write |
Cancel a job and erase its files |
| GET | /me/usage |
any of convert:read, carnet:read, carnet:write, social:read |
Quota balances and the scopes of your key |
| GET, POST | /me/songs |
carnet:read, carnet:write |
List (cursor pagination) or add a song to your private notebook |
| GET, PUT, PATCH, DELETE | /me/songs/{songId} |
carnet:read, carnet:write |
One song |
| GET, POST | /me/playlists |
carnet:read, carnet:write |
List or create playlists |
| GET, PUT, PATCH, DELETE | /me/playlists/{playlistId} |
carnet:read, carnet:write |
One playlist |
| GET | /mastering/capabilities |
none | Limits, setting ranges, loudness targets, built-in presets, and (with a key) whether you may render and your balance |
| GET, POST | /mastering/presets |
mastering:read, mastering:write |
List or save your Mastering presets |
| DELETE | /mastering/presets/{id} |
mastering:write |
Delete a preset |
| POST | /mastering/jobs |
mastering:write |
Upload an audio file and start its mastering |
| GET, DELETE | /mastering/jobs/{id} |
mastering:read, mastering:write |
Job state with the loudness before and after, or cancel and erase |
| GET | /mastering/jobs/{id}/result |
mastering:read |
Download the mastered WAV |
| GET | /users/{handle}, /users/{handle}/songs, /users/{handle}/playlists, /me/followers, /me/following |
social:read |
Read-only view of published profiles |
The contract also covers the audio processing queue (/audio, /queue) and stem separation (/stem-tool/*). There is no social write: following, publishing and liking stay gestures made in the app (the only exception is the publish field on a song you own, with an explicit rights declaration).
Ask the server, do not guess: GET /formats lists every format with served: true|false. Pass the id in inputFormat and output.
curl https://jam-jam.org/api/v1/formats # everything
curl "https://jam-jam.org/api/v1/formats?input=chordpro" # what can be produced from ChordPro, and why not
A few things worth knowing, all visible in that response:
outputids are things likesongbook,pdf-chords,pdf-lyrics,txt,chordpro,html,png,lrc,srt,midi,musicxml. Note thatpdf-songbookis an input id (a PDF made by Jam Jam), not an output.requiresAccount: trueon an output (for examplesongbook) means a key is needed. The songbook JSON is nevertheless available fromGET /jobs/{id}/result/songbookafter any successful job.tier: premium(todaymidiandmusicxml) needs a Premium account.- An output only makes sense when the source carries what it needs:
midiandmusicxmlneed a melody,lrcandsrtneed timing.?input=tells you withallowedandreason. - Each input has its own limits (
maxBytes,maxPages,maxDurationS).
The contract is written to be read by an agent. Give your assistant two things, or use the official MCP server below.
- The address of the contract:
https://jam-jam.org/api/v1/openapi.json - An API key limited to the scopes the task needs (for example
convert:readandconvert:writeto convert, pluscarnet:writeto file the result in your notebook). Pass it asAuthorization: Bearer <key>; do not paste it in a shared conversation you do not control.
A prompt that works well:
Read https://jam-jam.org/api/v1/openapi.json. Call GET /formats first and only use ids with served=true. Send an Idempotency-Key on every write. On a 429, wait for Retry-After. Use GET /me/usage to check my quota and scopes before starting.
Revoke the key in the app when the task is done.
For assistants that speak the Model Context Protocol (Claude Desktop, Claude Code, Cursor and other MCP clients), the official server @jam-jam/convert-mcp exposes the same API as typed tools: conversion, your notebook (read and write), public profiles and Mastering. It only offers the tools that the scopes of your key allow.
{
"mcpServers": {
"jam-jam": {
"command": "npx",
"args": ["-y", "@jam-jam/convert-mcp"],
"env": { "JAM_JAM_API_KEY": "jj_live_..." }
}
}
}
The same server masters audio: jj_mastering_capabilities, jj_list_mastering_presets, jj_create_mastering_preset, jj_master_audio (a file, with a preset or inline settings) and jj_mastering_get_job, with the mastering:read and mastering:write scopes. You can ask an assistant: "Master ~/Music/mix.wav with the warm preset and tell me the loudness before and after."
The key goes in the environment, never on a command line. Full list of tools and options: the package page and jam-jam.org/developers.
- Every error is an
application/problem+jsonobject (RFC 9457) withtype,title,statusand a stablecodeto test (insufficient-scope,quota-exhausted,unsupported-pair,invalid-token, and so on, listed in the contract). Quota errors also carryretryAfterand thequotastate. - Put an
Idempotency-Keyheader (up to 200 characters) on every write. Replaying the same key and body returns the same result without a second charge (for notebook writes, the same key with a different body is422 idempotency-conflict). - Uploaded files and generated results are deleted 24 hours after the job was created (
expiresAt). After that,GET /jobs/{id}/result*answers410. There is no longer retention, including after a failure. - Your notebook is private: an id that does not exist and an id that belongs to someone else give the same
404.
Only convert content you have the right to use, and only submit audio you hold the rights over. The sample in samples/ is "Amazing Grace": the words are by John Newton (1779) and the melody is traditional, both in the public domain; the chord marks are a simple harmonization written for this repository. Please keep your own demos to public domain or your own material too.
See SECURITY.md.
The examples in this repository are released under the MIT License. The Jam Jam API and service are governed by the Jam Jam terms and privacy policy; the license of this repository does not cover them.