# 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](https://docs.songco.de/account/) under **api**, with **new key**. It starts with `sc_` and is shown once. Send it on every call:

```sh
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](https://docs.songco.de/developers/mcp/#sign-in-for-apps) 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`](https://docs.songco.de/developers/api/#account) 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](https://docs.songco.de/account/), 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:

```sh
-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}`](https://docs.songco.de/developers/api/#get-jobs-job).

```json
{
  "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`](https://docs.songco.de/developers/api/#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](https://docs.songco.de/developers/api/#what-to-pick). `"ultra": true` still means `ultra`. |
| `model`, `reasoning` | on your own key, as [above](https://docs.songco.de/developers/api/#what-to-pick). |
| `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}`](https://docs.songco.de/developers/api/#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`:

```json
{
  "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.

```json
{ "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.

```json
{ "code": "ode\ntempo 124\n…" }
```

```json
{
  "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](https://docs.songco.de/ode/language/) and [rows and settings](https://docs.songco.de/ode/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:

```json
{
  "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.

```html
<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](https://docs.songco.de/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:

- **`.ode`** is the code.
- **`.songcode`** is the project: code, settings and everything the app needs to open it again.
- **`.mid`** is 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](https://docs.songco.de/app/export/) 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](https://songco.de/privacy#connections).

---

This page on the web: https://docs.songco.de/developers/api/
Every page of the Songcode docs, for agents: https://docs.songco.de/llms.txt (index) and https://docs.songco.de/llms-full.txt (all of it in one file).
