REST API
Make, check, edit, publish and export Songcode songs from your own code. Every route, with the request and what comes back.
The API lives at https://studio.songco.de/api/v1. It speaks JSON, takes a bearer token, and does what the app does: songs Songcode writes for you, code you check and edit, drafts, published tracks and files.
#Authentication
Make a key on the settings page under api, with new key. It starts with sc_ and is shown once. Send it on every call:
curl https://studio.songco.de/api/v1/me \
-H "Authorization: Bearer sc_your_key"
A key acts as your account, with every permission. Revoke it on the same page and it stops working at once. An app that signs people in to their own accounts uses OAuth instead and gets an access token that works here too.
A call without a working token gets 401 and a WWW-Authenticate header that points at the sign-in metadata.
#Who writes the songs
Songcode does. Songs and stems (POST /songs, POST /drafts/{id}/stems) are written on Songcode’s servers, checked and fixed there, and saved as drafts. What changes is whose account pays.
On a plan, send nothing more. Songcode writes on models tuned for ode and takes the plan’s weekly usage, by how much it writes: a loop takes little, a full song more, ultra compose several times that, a stem very little, fast far less than extra. When the week is used up and the account holds no extra usage, the call answers 402 (no_usage_left) until the week resets or extra usage is bought on the settings page. GET /me shows how much of the week is used and when it resets.
Without a plan, Songcode writes with your own Anthropic or OpenAI key, and your provider bills you for it. Songcode sets no limit. Either save the key once on the settings page, under model key for the API and MCP (stored encrypted, never shown again), or send it with each call, where it is used for that call and never stored:
-H "x-provider: anthropic" -H "x-provider-key: sk-ant-..."
x-provider is anthropic or openai, and the key is one of theirs. OpenRouter keys are refused. A plan comes first: an account with one writes on the plan even when a key is sent.
With neither, writing answers 402 with "code": "needs_key_or_plan". Every other route works for any account.
#What to pick
The same choices as the app, which depend on who pays.
On a plan, quality picks how a song is composed:
quality | |
|---|---|
fast | the quickest song, and the least of the week |
balanced | between the two |
extra | the strongest ordinary song. The default. |
ultra | ultra compose: a longer, more composed full song. It takes several minutes and several times the usage, and always writes a full song. |
On your own key, pick the model and how hard it thinks, as the app’s key sheet does:
| field | |
|---|---|
model | haiku, sonnet or opus on an Anthropic key; luna, terra or sol on an OpenAI key. The full model id works too. Default: the largest. |
reasoning | low, medium, high, xhigh or max on every model, and none (no reasoning at all) on luna and terra. Default: the model’s usual level. GET /v1/account lists each model’s levels. |
Sending one writer’s choice to the other is refused with 400 (use_quality or use_model) and says what to send instead. Stems take the same fields, except ultra.
#Limits
120 calls a minute for each key or connected app, 429 past that. Three songs or stems can be in progress at once for an account. A song’s code can be 100 KB.
#Errors
Every error is JSON with a status: { "error": "what went wrong", "code": "optional_machine_code" }. The message is written to be shown to a person or handed back to a model.
| status | when |
|---|---|
| 400 | the request is malformed: a missing prompt, an unknown genre, a bad edit |
| 401 | no token, or one that was revoked or expired |
| 402 | Songcode was asked to write for an account with no plan and no model key (needs_key_or_plan), or with the week’s usage used up (no_usage_left) |
| 403 | the token lacks the permission (insufficient_scope) |
| 404 | no such song, or not yours |
| 409 | a stem with that name exists already |
| 422 | the code does not check cleanly and the call needs it to (publishing) |
| 429 | too many calls, or too many songs in progress |
#Writing songs
#POST /songs
Songcode writes a song, checks it, repairs it once if the checker finds a problem, and saves it as a draft. With a key in the headers the call runs until the song is done, since that key is never stored to finish it later. On a plan or a saved key, the call waits as long as wait asks (in seconds, up to 120), then answers 202 with the job to follow with GET /jobs/{job}.
{
"prompt": "late night house, warm organ stabs, a long filtered break",
"genre": "house",
"mode": "full",
"quality": "extra",
"bpm": 122,
"lyrics": "optional: lines to sing, or a description of them"
}
| field | |
|---|---|
prompt | required. Up to 2,000 characters. |
genre | one of the names from GET /genres. Default house. |
mode | loop (a short loop, the default) or full (a whole song with sections). |
quality | on a plan: fast, balanced, extra (the default) or ultra, as above. "ultra": true still means ultra. |
model, reasoning | on your own key, as above. |
bpm, lyrics, vocals, singer | the same as the app’s settings. Lyrics turn vocals on. They can be the lines to sing, one per sung line, or a description of the words wanted; Songcode tells which. To decide it yourself, add "settings": { "lyricsAs": "lines" } or "brief". |
settings | any other setting the app takes, by name. |
The answer is 201 with the finished job once the song is written, in the shape GET /jobs/{job} shows. A loop takes about half a minute and a full song one to three minutes, so give the request a long timeout. status ends as done or failed.
#GET /jobs/{job}
Reads a job. Add ?wait=60 to wait for it. When it is done:
{
"job": "job_6f1c…",
"status": "done",
"done": true,
"draftId": "va2t9697tjw5",
"url": "https://studio.songco.de/d/va2t9697tjw5",
"code": "ode\ntitle \"…\"\n…",
"check": { "ok": true, "bpm": 122, "bars": 96, "seconds": 189, "stems": [ … ] }
}
A failed job has error and errorStatus.
#POST /drafts/{id}/stems
Songcode writes one stem into a draft, fitted to its tempo, key, chords and other stems, paid for the same way as a song.
{ "op": "add", "stem": "strings", "role": "pad", "instruction": "slow swells under the chorus" }
op is add for a new stem or rewrite to replace one. role is one of kick, snare, clap, hats, perc, bass, sub, chords, stab, lead, arp, pad, vocal, texture, fx; it can be left out when the stem’s name says it (bass, hats, piano, strings). The answer is the finished job, like POST /songs.
#Code
#POST /check
Reads code the way the app’s checker does and says what it finds. Nothing is saved.
{ "code": "ode\ntempo 124\n…" }
{
"ok": true,
"errors": [],
"warnings": [],
"title": "late night warm",
"bpm": 124,
"key": "a minor",
"meter": "4/4",
"bars": 8,
"seconds": 15.5,
"loops": true,
"sections": [],
"stems": [{ "id": "kick", "sound": "warm", "muted": false, "ok": true }]
}
Each error says where it is and what is wrong, so a model can fix it and check again.
#GET /guide/{section}
The ode language, as Markdown: format (the song format, with two worked examples), rows (every row and setting), sounds (every sound by name) and genres. These are the same pages as the song format and rows and settings.
#GET /genres
The genres a song can be asked for, each with its name, its family and its tempo range.
#Drafts
A draft is a private song: only its account can open it. Every song written through the API is saved as one.
| route | |
|---|---|
GET /drafts | the account’s drafts, newest first. ?limit= up to 100. |
POST /drafts | saves code as a new draft: { "code": "…", "title": "optional" }. Answers 201 with the draft. |
GET /drafts/{id} | one draft: its code, its check, its link. |
PATCH /drafts/{id} | replaces its code or title: { "code": "…" }. |
DELETE /drafts/{id} | deletes it. A track published from it stays up. |
POST /drafts/{id}/edits | changes it by rule. See below. |
POST /drafts/{id}/publish | publishes it. See below. |
GET /drafts/{id}/export | the file. See below. |
#POST /drafts/{id}/edits
Exact changes, made by the same code that writes the app’s own edits, so the rest of the song is left as it was:
{
"edits": [
{ "op": "tempo", "bpm": 118 },
{ "op": "key", "tonic": "f#", "scale": "minor" },
{ "op": "mix", "stem": "bass", "db": -3, "pan": -0.2 },
{ "op": "mix", "stem": "hats", "mute": true },
{ "op": "rename_stem", "stem": "lead", "to": "hook" },
{ "op": "remove_stem", "stem": "clap" },
{ "op": "replace_stem", "stem": "pad", "block": "stem pad: warmpad {\n …\n}" },
{ "op": "add_stem", "stem": "rim", "role": "perc", "block": "stem rim: analog {\n …\n}" },
{ "op": "title", "title": "later night" }
]
}
Up to 40 edits, applied in order. If one cannot be made, none are, and the answer says which.
#POST /drafts/{id}/publish
Makes the draft a public track that anyone with its link can play: https://studio.songco.de/t/{trackId}. Publishing again replaces the track at the same link. A draft that does not check cleanly is refused with 422.
The answer carries embedUrl and embedHtml: a small player for the track that any site can frame.
<iframe src="https://studio.songco.de/embed/t/{trackId}" title="songcode player" width="100%" height="132" style="border:0;max-width:560px" allow="autoplay" loading="lazy"></iframe>
It plays the track with the Songcode Audio Engine in the visitor’s browser and links back to it. Drafts have no embed, because they are private.
#Published tracks
| route | |
|---|---|
GET /tracks | the account’s published tracks. |
GET /tracks/{id} | any published track, by its link’s id, with its embedUrl. |
GET /tracks/{id}/export | the file. |
GET /songs | drafts and published tracks together. |
#Files
GET /drafts/{id}/export.ode, .songcode or .mid (or ?format=) answers the file itself:
.odeis the code..songcodeis the project: code, settings and everything the app needs to open it again..midis a Standard MIDI File with a track for each pitched stem and the drums on channel 10.
Audio (WAV, MP3 and stems) is rendered by the Songcode Audio Engine in the browser, so it is not a file the API can return. Open the song’s link and download it there.
#Account
GET /me says who the token belongs to, which plan the account is on and how much of its week is used, the saved model key (its provider and last four characters), and whether Songcode can write for it now (compose).
#Your data
Calls to the API are logged with the route, the status, how long they took and which key or app made them, never with the key itself. Prompts and code are kept the same way as in the app. See the privacy policy.