← Lyrics Generator / API
Get a token

Drive Lyrics Generator from your own code

Everything the web app does goes through one public surface. Base URL: https://api.skillsafe.ai/v1/app-api. Every request carries Authorization: Bearer <token>.

One thing to know before you start: the client-side checking that makes this app what it is - the syllable counting, the rhyme-scheme derivation, the originality check - happens in the browser, not on the server. Over the API you get the model's reply and its own claims about its work. If you want the reply checked, run the same modules yourself: syllable.js, prosody.js, known.js, origin.js and songscan.js are all served from this origin and none of them touch the network.

The envelope

Every response is one of these two shapes.

{"ok": true,  "data": { ... }, "meta": { "request_id": "req_...", "timestamp": "..." }}
{"ok": false, "error": { "code": "...", "message": "...", "status": 400, "details": {} }}

Error codes

unauthorized (401)No token, or an expired one. On a cold start this is the correct answer, not a fault.
insufficient_credits (402)Balance below min_credits. Estimate first and you will never see it.
forbidden (403)The token belongs to another app.
not_found (404)Unknown job id or collection.
validation_error (400)The body was not shaped as the app expects. See error.details.
rate_limited (429)Back off and retry. Never tight-loop.

The input

task comes first and decides everything else. It is "write" or "revise".

taskRequired. "write" or "revise".
subjectRequired. What the song is about, in your words.
moodRequired object: id, label, overrides, overrides_rule, wants, brief. overrides names the one house craft rule this mood may break.
genreRequired object: id, label, overrides, overrides_rule, conventions.
planRequired array of {kind, label}, in order. kind is one of intro, verse, prechorus, chorus, bridge, refrain, outro.
devicesThe device glossary, as "id: description" strings. A vocabulary for describing finished work, not a checklist.
crowding{count, note} - how many known titles touch this subject. A number, deliberately never a list.
shapeOptional. {syllables_per_line: [min, max], even: bool}.
perspectiveOptional. Who is speaking, and to whom.
avoidOptional. Words, images or moves to stay away from.
priorrevise only. {title, sections: [{label, kind, lines: []}]}.
directiverevise only. What you want changed.

The output

Plain text in blocks, not JSON. A block starts with a marker alone on a line; inside it every line is either key: value or a lyric line prefixed with a pipe and a space. The format was chosen because a stream gets cut, and a truncated block list is a shorter block list while a truncated JSON object is nothing at all.

== SONG ==
title: <the title>
premise: <one sentence on what the song is actually about>

== SECTION ==
kind: verse
label: Verse 1
function: <what this section does that no other section does>
rhyme: <one letter per line; x means rhymes with nothing>
syllables: <one number per line, space separated>
| <one line of the lyric>
| <one line of the lyric>

== HOOK ==
line: <the hook, character for character as it appears in its section>
lands: <which section and line, and why there>
why: <what makes it a hook>

== BRIDGE ==
does: <what the bridge does that the verses do not>
against: <the verse move it works against>

== CRAFT ==
devices: <glossary ids, comma separated>
voice: <who speaks; sentence length; whether it evaluates; line endings>
note: <two to five sentences of craft talk>

== ORIGINALITY ==
statement: <that these words are newly written>
risk: <where the pull toward something existing was strongest>

One == SECTION == block per plan entry, same order, same labels. rhyme carries one letter per lyric line and syllables one number per lyric line - both are the model's claims, and the web app recomputes both and shows you where they disagree.

1. A tiny client helper

Every call below goes through one function. The envelope is always {"ok":true,"data":{...}} or {"ok":false,"error":{"code","message","status"}}, so unwrapping it once here means never unwrapping it again.

2. Get a token

Easiest route: open the token page, sign in, and copy the token. For a script, mint a guest token instead - a guest can browse and estimate, and needs credits to run.

3. Check who you are and what you have

GET /me returns exactly three fields: subject_type (user or guest), subject_id, and credits. A cold-start 401 here is correct rather than a fault - it means no token has been presented yet.

4. Price the run before you make it

POST /estimate costs nothing and starts no job. It returns hold_credits (what will be reserved), min_credits (the floor below which the run is refused), and the model binding. It does not validate your input. A bare string, a null and an empty array all come back ok:true with a correct model binding, so check the shape of your own body before you send it - this app's client does, in mustBeObject().

5. Run it, and poll

POST /run starts a job and returns immediately with a job_id. Poll GET /jobs/{job_id} until status is done or failed. The reply text is at output.output. Always send an Idempotency-Key header derived from the input: a retry on the same key returns the same job instead of billing a second one.

6. Or stream it

POST /run-stream returns text/event-stream. The wire format is an event: line, one or more data: lines, then a blank line terminating the frame. Event names are job, delta, done, pending and error. A delta payload carries text; done and pending carry the finished job; error carries code, message and job_id. This is taken from the sdk.js in this very bundle and asserted against it by the build, not copied from another app.

7. Revise a song you already have

Same endpoint, task: "revise", plus two fields: prior (the song, section by section) and directive (what to change). Everything else is identical. The revision changes what you asked about and holds the rest.

What the app does that the API does not

The reply is the model's. Everything this app is actually for happens afterwards, in the browser, and is yours to run or skip: