Post a prompt or a brief, poll a song id.
One header, minted in the console.
Authorization: Bearer jb_live_... # or, for clients that cannot set Authorization x-api-key: jb_live_...
Two things worth knowing before you build
A key is a bearer credential for a whole workspace and every render it makes is billed to that workspace. It belongs on a server, never in a browser, a mobile app or a playout box a contractor can read.
Scopes are fixed when a key is issued and are never edited afterwards. Widening one means minting a new key, which is deliberate: the jingle endpoints carry a scope of their own because one call to them can spend twenty times what one compose does.
Every endpoint, generated from the contract.
| Method | Path | What it does |
|---|---|---|
| POST | /api/v1/songsmusic | Compose a song Text to music, with sung vocals or instrumental. |
| GET | /api/v1/songs | List songs The workspace's catalogue, newest first, cursor-paginated. |
| GET | /api/v1/songs/{song_id} | Get a song One song in full, with its takes and stems. |
| DELETE | /api/v1/songs/{song_id}library | Delete a song Forget a song, its takes, its stems and its audio. |
| GET | /api/v1/songs/{song_id}/audio | Download the audio The rendered master, as bytes. |
| GET | /api/v1/songs/{song_id}/stems | List stems What has been separated out of this track so far. |
| POST | /api/v1/songs/{song_id}/stemsstems | Export stems Separate a finished track into its parts. |
| POST | /api/v1/songs/{song_id}/extendmusic | Extend a song Continue a finished track past its ending. |
| POST | /api/v1/songs/{song_id}/covermusic | Cover a song Re-render an existing track in another style. |
| Method | Path | What it does |
|---|---|---|
| POST | /api/v1/lyricslyrics | Write lyrics Words to sing, written to a brief and a length. |
| Method | Path | What it does |
|---|---|---|
| POST | /api/v1/jingles/clientjingles | Client jingle An advertiser's jingle, from the brief, cut to broadcast lengths. |
| POST | /api/v1/jingles/radiojingles | Radio imaging Station IDs, sweepers and beds, sung to the station's own facts. |
| Method | Path | What it does |
|---|---|---|
| GET | /api/v1/personas | List personas Saved style identities. |
| POST | /api/v1/personaslibrary | Create a persona Save a style identity for reuse. |
| GET | /api/v1/playlists | List playlists Saved collections of songs. |
| POST | /api/v1/playlistslibrary | Create a playlist Collect songs under one name. |
| Method | Path | What it does |
|---|---|---|
| GET | /api/v1/usage | Usage and allowance Seconds spent, seconds left, and when the period rolls. |
3 calls, one for each kind of thing you can ask for.
/api/v1/songsCompose a songText to music, with sung vocals or instrumental.
curl -X POST 'https://jinglebuddy.radioworkflow.com/api/v1/songs' \
-H "Authorization: Bearer $JINGLEBUDDY_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 6f9619ff-8b86-d011-b42d-00c04fc964ff' \
-d '{
"prompt": "Warm family pizzeria jingle, singable hook, upbeat and friendly",
"kind": "song",
"style_tags": [
"uptempo pop",
"bright horns",
"female vocal"
],
"instrumental": false,
"duration_sec": 30,
"takes": 1,
"is_public": false
}' \
--max-time 900{
"song_id": "sg7k2m4p0001s7f3zq9r1d6t",
"status": "queued",
"job_id": "jb6c2n8t0001s7f3wd5y4q3l",
"poll_url": "https://jinglebuddy.example/api/v1/songs/sg7k2m4p0001s7f3zq9r1d6t",
"seconds_claimed": 30,
"estimated_wait_sec": 90
}/api/v1/jingles/clientClient jingleAn advertiser's jingle, from the brief, cut to broadcast lengths.
curl -X POST 'https://jinglebuddy.radioworkflow.com/api/v1/jingles/client' \
-H "Authorization: Bearer $JINGLEBUDDY_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 6f9619ff-8b86-d011-b42d-00c04fc964ff' \
-d '{
"business_name": "Marco'\''s Pizza",
"lengths": [
15,
30,
60
],
"website": "marcospizza.com",
"style_tags": [
"uptempo pop",
"bright horns",
"female vocal"
],
"takes": 1,
"save_brief": true
}' \
--max-time 900{
"kind": "client_jingle",
"client_brief_id": "cb8j2p6d0005s7f3rn3c7v4a",
"lyric_id": "ly9d4b6y0002s7f3ln5r7w1k",
"missing_phrases": [],
"songs": [
{ "song_id": "sg7k2m4p0001s7f3zq9r1d6t", "duration_sec": 30, "status": "queued" }
],
"seconds_claimed": 30,
"poll_url": "https://jinglebuddy.example/api/v1/songs?kind=client_jingle"
}/api/v1/jingles/radioRadio imagingStation IDs, sweepers and beds, sung to the station's own facts.
curl -X POST 'https://jinglebuddy.radioworkflow.com/api/v1/jingles/radio' \
-H "Authorization: Bearer $JINGLEBUDDY_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 6f9619ff-8b86-d011-b42d-00c04fc964ff' \
-d '{
"station_name": "K H I T",
"dial_position": "101.5",
"station_format": "country",
"package": [
"id",
"sweeper",
"promo_bed"
],
"lengths": [
10,
30
],
"style_tags": [
"uptempo pop",
"bright horns",
"female vocal"
],
"takes": 1,
"save_profile": true
}' \
--max-time 900{
"kind": "radio_jingle",
"station_profile_id": "st4v9m1q0007s7f3gt6c2y8x",
"songs": [
{ "song_id": "sg7k2m4p0001s7f3zq9r1d6t", "piece": "id", "duration_sec": 10, "status": "queued" }
],
"seconds_claimed": 40,
"poll_url": "https://jinglebuddy.example/api/v1/songs?kind=radio_jingle"
}A compose answers with an id, not with audio.
Queued can mean waking
The engine sleeps when nobody is using it. The first render after roughly fifteen idle minutes spends 30 to 90 seconds waking up before the render starts, and it sits in queued for all of it. That is why queued and rendering are separate states: collapsing them would make every cold start read as a hang.
Poll every five seconds
Read the song id back until its status leaves queued or rendering. A one second loop is a hundred pointless requests before the answer is even possible, and the rate limiter will start refusing them first. A compose is allowed to run for minutes on our side, so set a client timeout that can wait for it.
Or do not poll
Webhooks fire when a render lands and when a whole package does. For a twenty piece imaging package that is the difference between one subscription and four hundred polls. A compose endpoint cannot fire its own completion event, because the interesting moment happens after the response.
Everything that spends takes an idempotency key.
Idempotency-Key
For example: 6f9619ff-8b86-d011-b42d-00c04fc964ff
A value you choose that makes this request safe to retry. The first call with a given key does the work; a retry carrying the same key AND the same payload answers with that first result instead of composing and billing again, and carries idempotent-replay: true.
What the key is scoped to, exactly: the method, the path (so the same key against a different song or a different endpoint is a different request), the API key that sent it (so two services sharing a workspace cannot collide on each other's job ids), and a fingerprint of the payload. Changing the payload under the same key does not replay: it is a different request, so it renders and it bills. You are never handed the answer to a question you did not ask, but the key is not a way to avoid paying for a second, different one.
format is deliberately outside the fingerprint, so a retry that only changes the container replays the stored track rather than paying for a re-render.
This matters more here than it does for a speech API. A compose is seconds against a warm GPU and a minute and a half against a cold one, which is comfortably past the point where clients time out, proxies retry and people press the button again.
Retained for 24 hours; after that the key is forgotten and an identical request renders and bills as new. A key whose request FAILED is released at once, because a failed render costs nothing and has to stay retryable.
1 to 255 printable ASCII characters; a UUID per logical request is the usual choice. Omit the header entirely and nothing changes.
One envelope, and a code you can branch on.
| Status | Code | What happened | What to do |
|---|---|---|---|
| 401 | missing_api_keyAny endpoint | No bearer token and no | Send the key as |
| 401 | invalid_api_keyAny endpoint | The key is unknown, revoked or past its expiry. All three answer identically so the endpoint cannot be used to enumerate keys. | Check the secret was copied whole, then check the key is still live under API Keys. If it expired, issue a new one: |
| 403 | insufficient_scope | The key is valid but was not granted the scope this endpoint needs. | Scopes are fixed when a key is issued. Create a new key carrying the scope named in the message; there is nothing to change on the request. |
| 404 | not_found | No such song, lyric, persona or playlist, or one that exists but is not visible to this workspace. | Re-read the id from the endpoint that produced it. A record belonging to another workspace answers this rather than 403, so the two are indistinguishable from outside. |
| 402 | insufficient_credit | The workspace has spent its plan allowance for the period and has no account credit left to cover the rest. | Unlike |
| 409 | idempotency_conflict | An | If |
| 409 | resource_not_ready | The id is real and this workspace owns it, but the thing you asked for does not exist yet: audio for a song still rendering, stems for a song with no master, an extend or a cover of a track that has not landed. | Poll |
| 413 | payload_too_large | The request body is over this endpoint's ceiling. | Shorten the lyric or the prompt. The bounds are published on every field, so the same bytes will never be accepted. |
| 422 | invalid_request | The request parsed but a field is missing, out of range or the wrong type. | Read |
| 429 | quota_exceeded | The workspace has spent its allowance of rendered SECONDS for the billing period. Seconds, not songs: a :15 station ID and a three minute track cost the GPU wildly different amounts. | Terminal until the period rolls, and |
| 429 | rate_limitedAny endpoint | Too many requests from this key. | Sleep for |
| 503 | provider_unavailable | The endpoint is implemented but this deployment has not satisfied the gate it depends on. Two gates are worth knowing about. Composing needs | Nothing about the request can fix it, so retrying is pointless until an operator acts. Render |
| 502 | engine_unavailable | The music engine refused or could not be reached. | Retry once after a short pause. If it persists the engine itself is down and no change to the payload will help. |
| 504 | engine_timeout | The engine did not answer in time. Usually a compose that landed on a GPU instance still loading the model. | Retry once, with a client timeout of 900 seconds so a legitimate 30 to 90 second cold start has room to land. Never retry in a loop. The engine runs scale-to-zero by an explicit owner decision, so the FIRST render after fifteen idle minutes is the slow one and the retry usually lands on a warm instance. |
| 500 | internal_errorAny endpoint | Something broke on our side. The response never carries a stack trace. | Retry once. If it persists, quote the |
The numbers your client has to know about.
The engine's floor. A request under it is refused rather than silently clamped, because a caller who asked for five seconds and received ten has no field telling them why.
The engine's ceiling. Your plan's single track limit may be lower.
Of rendered audio, not one song. A jingle package at three lengths and four takes is priced on the response before you commit.
Concurrent renders, per plan
| Plan | At once | API |
|---|---|---|
| Free | 1 | Not on this plan |
| Creator | 2 | Not on this plan |
| Station | 4 | Included |
| Group | 8 | Included |
Concurrency is a card count
Generation holds a GPU for the length of the render, so the numbers beside each plan are a real resource rather than a throttle we invented. A request over the ceiling waits rather than failing, which is worth knowing before you fan out a package from a cron job.
What happens when the allowance runs out
Renders stop and the call answers with a code you can branch on, which is why the error table above is worth reading before you write the retry. The refusal happens before the engine is called, so nothing is stored and nothing is billed. The billing side of it, including the refund rule for a render that failed on our side, is on the pricing page.