← Stanzabar / API
Tokens

Drive Stanzabar from your own code

Everything the board asks the model is available over HTTP. A natural use is a songbook kept as data - a worship team's set, a teacher's class songs, a band's demos in ChordPro - where a script sends one song's arrangement, asks for lines that fit the metre, revises the lines a check flagged, or asks for a producer's critique.

One thing to be clear about first: the model is checked, not trusted. The caller sends the board's own syllable count and rhyme letter for every line; the reply carries the model's count and letter for every line it writes, and the board recounts both before anything is offered. Section ids, line numbers and chord positions must exist, and a chord substitution must be diatonic, borrowed or a secondary dominant in the song's key. The board's logic ships as plain scripts you can load in node — see the end of the examples.

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }

The token is minted for this app (the guest call takes {"slug":"stanzabar"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object: never wrap it as {"input": {…}} — that hides every field from the model, and /estimate warns unknown field 'input'.

Errors:

StatusCodeMeaning
400validation_errorThe body is not a JSON object.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the model lanes are metered and need a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

One system prompt serves three tasks. task picks the contract; a missing or unknown task is answered as critique, named in the reply's task.

taskWhenWhat targets holds
lyricsWrite lyric lines for chosen sections, in each section's metre and rhyme scheme, to the brief.[{"section":"S4","lines":4,"keep_existing":false}]
reviseRewrite only the marked lines; everything else is context.[{"section":"S2","n":[4]}]
critiqueCritique the arrangement: hook placement, section verdicts with lengths, moves, one chord substitution per section.[]

Every run body is a JSON object of string fields (the declared input schema is scalar, so object-valued fields are JSON-encoded strings; task, title, song, targets and facts are required, and /estimate warns when one is missing).

FieldWhat it holds
task"lyrics", "revise" or "critique".
titleThe song title.
briefJSON string: {"theme","mood","audience","pov"} - any may be empty.
songJSON string: {"key":"G","key_label":"G major","tempo":104,"meter":"4/4","sections":[{"id":"S2","type":"verse","label":"Verse 1","start_bar":5,"bars":8,"metre":"8","scheme":"ABAB","chords":[["1.1","G","I","diatonic"],["2.1","C","IV","diatonic"]],"lines":[{"n":1,"text":"...","syllables":8,"rhyme":"A","target":8}]}]}. chords entries are [bar.beat, symbol, roman, category]; metre is a number, a dotted hymn metre (8.6.8.6) or CM / LM / SM; scheme cycles over the lines, X = unrhymed. A line may carry range ("8-9") and marked. A long non-target section may arrive as its first and last line plus {"n":2,"omitted":k}.
targetsJSON string, per task (table above).
factsJSON string: the board's free checks - total_bars, duration, beats_per_bar, the key's diatonic and borrowed chords, repeated lines (hooks) and check findings (checks).
whyrevise only, optional: what the person wants changed.
clippedOptional: what was left out of the input.
retry_noteOptional: sent once when a previous reply was not valid JSON.

The reply text is one JSON object:

// task "lyrics" (and "revise", which returns only the marked line numbers)
{ "task": "lyrics",
  "sections": [{ "section": "S4", "lines": [{ "n": 1, "text": "...", "syllables": 8, "rhyme": "A", "note": "..." }] }],
  "hook": { "text": "...", "where": "S3.1" },
  "notes": ["..."] }

// task "critique"
{ "task": "critique", "summary": "...",
  "hook": { "section": "S4", "n": 1, "advice": "..." },
  "sections": [{ "section": "S1", "verdict": "keep|trim|extend|cut|move|rewrite", "bars": 4, "advice": "..." }],
  "moves": [{ "section": "S4", "to": 3, "reason": "..." }],
  "chords": [{ "section": "S2", "bar": 3, "beat": 1, "from": "Em", "to": "C", "reason": "..." }],
  "notes": ["..."] }

Check before you apply: recount every line (the board's estimator is Lyric.lineSyl), re-derive rhyme letters from the section as it would read after the change (Lyric.scheme), refuse lines for sections or line numbers that were not targets, refuse a from chord that is not on that bar, and refuse a to chord that is chromatic in the key (Theory.analyze(chord, key).category).

Worked examples

These are the bundled examples on the page: the exact run body the page builds and a reply produced against the shipped prompt (the page replays the same replies for free).

lyrics: a 90th-birthday singalong - write verse 2 and the bridge

Input (targets names the two empty sections):

{
 "task": "lyrics",
 "title": "Ninety Candles",
 "brief": "(a JSON string - decoded below)",
 "song": "(a JSON string - decoded below)",
 "targets": "(a JSON string - decoded below)",
 "facts": "(a JSON string - decoded below)"
}

brief, decoded:

{
 "theme": "Grandma Rose turns ninety: her rose garden by the gate, her terrible jokes, the kitchen radio she sings over",
 "mood": "warm, funny, singalong",
 "audience": "family party - three generations sing the chorus",
 "pov": "we"
}

song, decoded:

{
 "key": "G",
 "key_label": "G major",
 "tempo": 104,
 "meter": "4/4",
 "sections": [
  {
   "id": "S1",
   "type": "intro",
   "label": "Intro",
   "start_bar": 1,
   "bars": 4,
   "chords": [
    [
     "1.1",
     "G",
     "I",
     "diatonic"
    ],
    [
     "2.1",
     "C",
     "IV",
     "diatonic"
    ],
    [
     "3.1",
     "G",
     "I",
     "diatonic"
    ],
    [
     "4.1",
     "D",
     "V",
     "diatonic"
    ]
   ],
   "lines": []
  },
  {
   "id": "S2",
   "type": "verse",
   "label": "Verse 1",
   "start_bar": 5,
   "bars": 8,
   "metre": "8",
   "scheme": "ABAB",
   "chords": [
    [
     "1.1",
     "G",
     "I",
     "diatonic"
    ],
    [
     "2.1",
     "C",
     "IV",
     "diatonic"
    ],
    [
     "3.1",
     "Em",
     "vi",
     "diatonic"
    ],
    [
     "4.1",
     "D",
     "V",
     "diatonic"
    ],
    [
     "5.1",
     "G",
     "I",
     "diatonic"
    ],
    [
     "6.1",
     "C",
     "IV",
     "diatonic"
    ],
    [
     "7.1",
     "Em",
     "vi",
     "diatonic"
    ],
    [
     "8.1",
     "D",
     "V",
     "diatonic"
    ]
   ],
   "lines": [
    {
     "n": 1,
     "text": "She planted roses by the gate",
     "syllables": 8,
     "rhyme": "A",
     "target": 8
    },
    {
     "n": 2,
     "text": "Before the rest of us were born",
     "syllables": 8,
     "rhyme": "B",
     "target": 8
    },
    {
     "n": 3,
     "text": "She never let a supper wait",
     "syllables": 8,
     "rhyme": "A",
     "target": 8
    },
    {
     "n": 4,
     "text": "And sang along to every storm",
     "syllables": 9,
     "rhyme": "B",
     "range": "8-9",
     "target": 8
    }
   ]
  },
  {
   "id": "S3",
   "type": "chorus",
   "label": "Chorus",
   "start_bar": 13,
   "bars": 8,
   "metre": "7",
   "scheme": "AABB",
   "chords": [
    [
     "1.1",
     "G",
     "I",
     "diatonic"
    ],
    [
     "2.1",
     "D",
     "V",
     "diatonic"
    ],
    [
     "3.1",
     "Em",
     "vi",
     "diatonic"
    ],
    [
     "4.1",
     "C",
     "IV",
     "diatonic"
    ],
    [
     "5.1",
     "G",
     "I",
     "diatonic"
    ],
    [
     "6.1",
     "D",
     "V",
     "diatonic"
    ],
    [
     "7.1",
     "Em",
     "vi",
     "diatonic"
    ],
    [
     "8.1",
     "C",
     "IV",
     "diatonic"
    ]
   ],
   "lines": [
    {
     "n": 1,
     "text": "Ninety candles, Grandma Rose",
     "syllables": 7,
     "rhyme": "A",
     "target": 7
    },
    {
     "n": 2,
     "text": "Light them up from head to toes",
     "syllables": 7,
     "rhyme": "A",
     "target": 7
    },
    {
     "n": 3,
     "text": "Every year the garden grows",
     "syllables": 8,
     "rhyme": "A",
     "range": "7-8",
     "target": 7
    },
    {
     "n": 4,
     "text": "Happy birthday, Grandma Rose",
     "syllables": 7,
     "rhyme": "A",
     "target": 7
    }
   ]
  },
  {
   "id": "S4",
   "type": "verse",
   "label": "Verse 2",
   "start_bar": 21,
   "bars": 8,
   "metre": "8",
   "scheme": "ABAB",
   "chords": [
    [
     "1.1",
     "G",
     "I",
     "diatonic"
    ],
    [
     "2.1",
     "C",
     "IV",
     "diatonic"
    ],
    [
     "3.1",
     "Em",
     "vi",
     "diat
... (shortened here)

targets, decoded:

[
 {
  "section": "S4",
  "lines": 4,
  "keep_existing": false
 },
 {
  "section": "S6",
  "lines": 4,
  "keep_existing": false
 }
]

facts, decoded:

{
 "total_bars": 56,
 "duration": "2:09",
 "beats_per_bar": 4,
 "diatonic": [
  "G",
  "Am",
  "Bm",
  "C",
  "D",
  "Em",
  "F#dim"
 ],
 "borrowed": [
  "Cm",
  "Eb",
  "F",
  "Bb",
  "Dm",
  "Adim",
  "A7",
  "E7",
  "B7"
 ],
 "hooks": [
  {
   "text": "Ninety candles, Grandma Rose",
   "at": [
    "S3.1",
    "S5.1",
    "S7.1"
   ]
  },
  {
   "text": "Light them up from head to toes",
   "at": [
    "S3.2",
    "S5.2",
    "S7.2"
   ]
  },
  {
   "text": "Every year the garden grows",
   "at": [
    "S3.3",
    "S5.3",
    "S7.3"
   ]
  },
  {
   "text": "Happy birthday, Grandma Rose",
   "at": [
    "S3.4",
    "S5.4",
    "S7.4"
   ]
  }
 ],
 "checks": []
}

Reply:

{
 "task": "lyrics",
 "sections": [
  {
   "section": "S4",
   "lines": [
    {
     "n": 1,
     "text": "She tells the one about the crow",
     "syllables": 8,
     "rhyme": "A",
     "note": "brings in her terrible jokes"
    },
    {
     "n": 2,
     "text": "We groan the way we groaned before",
     "syllables": 8,
     "rhyme": "B",
     "note": "the family's running reaction, a little comedy"
    },
    {
     "n": 3,
     "text": "Then sings above the radio",
     "syllables": 8,
     "rhyme": "A",
     "note": "the kitchen radio she sings over"
    },
    {
     "n": 4,
     "text": "And spins us round the kitchen floor",
     "syllables": 8,
     "rhyme": "B",
     "note": "ends on action, keeps chorus words out"
    }
   ]
  },
  {
   "section": "S6",
   "lines": [
    {
     "n": 1,
     "text": "So pass the spade and pass the seeds",
     "syllables": 8,
     "rhyme": "A",
     "note": "turn: the family takes over the garden"
    },
    {
     "n": 2,
     "text": "We'll keep her roses free of weeds",
     "syllables": 8,
     "rhyme": "A",
     "note": "shifts to future tense, a promise"
    },
    {
     "n": 3,
     "text": "And when the grandkids roll their eyes",
     "syllables": 8,
     "rhyme": "B",
     "note": "the next generation enters"
    },
    {
     "n": 4,
     "text": "We'll tell her jokes and act surprised",
     "syllables": 8,
     "rhyme": "B",
     "note": "we inherit the bad jokes; near rhyme"
    }
   ]
  }
 ],
 "notes": [
  "The bridge turns from describing Grandma Rose to the family promising to carry on her garden and her jokes.",
  "S6 line 4 is a near rhyme (eyes/surprised) on purpose."
 ]
}

revise: a worship song - three lines marked that break common metre

Input (the marked lines carry "marked": true; targets lists them):

{
 "task": "revise",
 "title": "Morning Light",
 "brief": "(a JSON string - decoded below)",
 "song": "(a JSON string - decoded below)",
 "targets": "(a JSON string - decoded below)",
 "facts": "(a JSON string - decoded below)",
 "why": "Keep the plain worship words; make each marked line sing in its metre and keep the rhyme."
}

brief, decoded:

{
 "theme": "God's steady faithfulness from morning to evening; plain words a congregation can sing on first hearing",
 "mood": "calm, grateful, building in the bridge",
 "audience": "Sunday worship team and congregation",
 "pov": "I / you (to God)"
}

song, decoded:

{
 "key": "D",
 "key_label": "D major",
 "tempo": 76,
 "meter": "4/4",
 "sections": [
  {
   "id": "S1",
   "type": "intro",
   "label": "Intro",
   "start_bar": 1,
   "bars": 4,
   "chords": [
    [
     "1.1",
     "D",
     "I",
     "diatonic"
    ],
    [
     "2.1",
     "G",
     "IV",
     "diatonic"
    ],
    [
     "3.1",
     "D",
     "I",
     "diatonic"
    ],
    [
     "4.1",
     "A",
     "V",
     "diatonic"
    ]
   ],
   "lines": []
  },
  {
   "id": "S2",
   "type": "verse",
   "label": "Verse 1",
   "start_bar": 5,
   "bars": 16,
   "metre": "CM",
   "scheme": "ABAB",
   "chords": [
    [
     "1.1",
     "D",
     "I",
     "diatonic"
    ],
    [
     "3.1",
     "G",
     "IV",
     "diatonic"
    ],
    [
     "5.1",
     "D",
     "I",
     "diatonic"
    ],
    [
     "7.1",
     "A",
     "V",
     "diatonic"
    ],
    [
     "9.1",
     "Bm",
     "vi",
     "diatonic"
    ],
    [
     "11.1",
     "G",
     "IV",
     "diatonic"
    ],
    [
     "13.1",
     "A",
     "V",
     "diatonic"
    ],
    [
     "15.1",
     "D",
     "I",
     "diatonic"
    ]
   ],
   "lines": [
    {
     "n": 1,
     "text": "When morning breaks across the hills",
     "syllables": 8,
     "rhyme": "A",
     "target": 8
    },
    {
     "n": 2,
     "text": "And mist lifts off the lake",
     "syllables": 6,
     "rhyme": "B",
     "target": 6
    },
    {
     "n": 3,
     "text": "Your quiet mercy gently fills",
     "syllables": 8,
     "rhyme": "A",
     "target": 8
    },
    {
     "n": 4,
     "text": "Every single breath that I take",
     "syllables": 9,
     "rhyme": "B",
     "range": "8-9",
     "target": 6,
     "marked": true
    }
   ]
  },
  {
   "id": "S3",
   "type": "chorus",
   "label": "Chorus",
   "start_bar": 21,
   "bars": 8,
   "metre": "7",
   "scheme": "AABB",
   "chords": [
    [
     "1.1",
     "G",
     "IV",
     "diatonic"
    ],
    [
     "2.1",
     "D",
     "I",
     "diatonic"
    ],
    [
     "3.1",
     "A",
     "V",
     "diatonic"
    ],
    [
     "4.1",
     "Bm",
     "vi",
     "diatonic"
    ],
    [
     "5.1",
     "G",
     "IV",
     "diatonic"
    ],
    [
     "6.1",
     "D",
     "I",
     "diatonic"
    ],
    [
     "7.1",
     "A",
     "V",
     "diatonic"
    ],
    [
     "8.1",
     "Bm",
     "vi",
     "diatonic"
    ]
   ],
   "lines": [
    {
     "n": 1,
     "text": "Morning light, you call my name",
     "syllables": 7,
     "rhyme": "A",
     "target": 7
    },
    {
     "n": 2,
     "text": "Every day you stay the same",
     "syllables": 8,
     "rhyme": "A",
     "range": "7-8",
     "target": 7
    },
    {
     "n": 3,
     "text": "Hold me when the shadows fall",
     "syllables": 7,
     "rhyme": "B",
     "target": 7
    },
    {
     "n": 4,
     "text": "You are faithful through it all",
     "syllables": 7,
     "rhyme": "B",
     "target": 7
    }
   ]
  },
  {
   "id": "S4",
   "type": "verse",
   "label": "Verse 2",
   "start_bar": 29,
   "bars": 16,
   "metre": "CM",
   "scheme": "ABAB",
   "chords": [
    [
     "1.1",
     "D",
     "I",
     "diatonic"
    ],
    [
     "3.1",
     "G",
     "IV",
     "diatonic"
    ],
    [
     "5.
... (shortened here)

targets, decoded:

[
 {
  "section": "S2",
  "n": [
   4
  ]
 },
 {
  "section": "S4",
  "n": [
   4
  ]
 },
 {
  "section": "S6",
  "n": [
   3
  ]
 }
]

facts, decoded:

{
 "total_bars": 68,
 "duration": "3:35",
 "beats_per_bar": 4,
 "diatonic": [
  "D",
  "Em",
  "F#m",
  "G",
  "A",
  "Bm",
  "C#dim"
 ],
 "borrowed": [
  "Gm",
  "Bb",
  "C",
  "F",
  "Am",
  "Edim",
  "E7",
  "B7",
  "F#7"
 ],
 "hooks": [
  {
   "text": "Morning light, you call my name",
   "at": [
    "S3.1",
    "S5.1",
    "S7.1"
   ]
  },
  {
   "text": "Every day you stay the same",
   "at": [
    "S3.2",
    "S5.2",
    "S7.2"
   ]
  },
  {
   "text": "Hold me when the shadows fall",
   "at": [
    "S3.3",
    "S5.3",
    "S7.3"
   ]
  },
  {
   "text": "You are faithful through it all",
   "at": [
    "S3.4",
    "S5.4",
    "S7.4"
   ]
  }
 ],
 "checks": [
  "S2.4 metre: Verse 1 line 4: 9 (8-9) syllables against a target of 6.",
  "S4.4 metre: Verse 2 line 4: 9 syllables against a target of 6.",
  "S4.2 orphan: Verse 2 line 2 should rhyme by the ABAB scheme but has no partner (“low”).",
  "S4.4 orphan: Verse 2 line 4 should rhyme by the ABAB scheme but has no partner (“now”).",
  "S6.3 metre: Bridge line 3: 13 (12-13) syllables against a target of 8.",
  "S6 echo: Bridge gives away the chorus: “morning”, “light” also open the chorus."
 ]
}

Reply:

{
 "task": "revise",
 "sections": [
  {
   "section": "S2",
   "lines": [
    {
     "n": 4,
     "text": "In every breath I take",
     "syllables": 6,
     "rhyme": "B",
     "note": "was 9 syllables against 6; keeps the lake/take rhyme"
    }
   ]
  },
  {
   "section": "S4",
   "lines": [
    {
     "n": 4,
     "text": "And at your feet let go",
     "syllables": 6,
     "rhyme": "B",
     "note": "was 9 with filler now; go now rhymes with low"
    }
   ]
  },
  {
   "section": "S6",
   "lines": [
    {
     "n": 3,
     "text": "Nothing I do could earn this love",
     "syllables": 8,
     "rhyme": "B",
     "note": "was 13 syllables against 8; same thought, unrhymed X line"
    }
   ]
  }
 ],
 "notes": [
  "Bridge lines 1, 2 and 4 sit at 7 against 8; a held note covers them, so they were left alone.",
  "The bridge's last line still echoes the chorus with morning light; mark it if you want that changed."
 ]
}

critique: a band demo with a late chorus, a long verse and a chromatic chord

Input:

{
 "task": "critique",
 "title": "Night Bus",
 "brief": "(a JSON string - decoded below)",
 "song": "(a JSON string - decoded below)",
 "targets": "(a JSON string - decoded below)",
 "facts": "(a JSON string - decoded below)"
}

brief, decoded:

{
 "theme": "riding the last bus home after a breakup, watching other passengers, not ready to arrive",
 "mood": "moody indie, restrained then lifting",
 "audience": "a four-piece band's demo for a battle of the bands",
 "pov": "I"
}

song, decoded:

{
 "key": "Am",
 "key_label": "A minor",
 "tempo": 92,
 "meter": "4/4",
 "sections": [
  {
   "id": "S1",
   "type": "intro",
   "label": "Intro",
   "start_bar": 1,
   "bars": 8,
   "chords": [
    [
     "1.1",
     "Am",
     "i",
     "diatonic"
    ],
    [
     "2.1",
     "F",
     "VI",
     "diatonic"
    ],
    [
     "3.1",
     "C",
     "III",
     "diatonic"
    ],
    [
     "4.1",
     "G",
     "VII",
     "diatonic"
    ],
    [
     "5.1",
     "Am",
     "i",
     "diatonic"
    ],
    [
     "6.1",
     "F",
     "VI",
     "diatonic"
    ],
    [
     "7.1",
     "C",
     "III",
     "diatonic"
    ],
    [
     "8.1",
     "G",
     "VII",
     "diatonic"
    ]
   ],
   "lines": []
  },
  {
   "id": "S2",
   "type": "verse",
   "label": "Verse 1",
   "start_bar": 9,
   "bars": 8,
   "metre": "8",
   "scheme": "ABAB",
   "chords": [
    [
     "1.1",
     "Am",
     "i",
     "diatonic"
    ],
    [
     "2.1",
     "F",
     "VI",
     "diatonic"
    ],
    [
     "3.1",
     "C",
     "III",
     "diatonic"
    ],
    [
     "4.1",
     "G",
     "VII",
     "diatonic"
    ],
    [
     "5.1",
     "Am",
     "i",
     "diatonic"
    ],
    [
     "6.1",
     "F",
     "VI",
     "diatonic"
    ],
    [
     "7.1",
     "C",
     "III",
     "diatonic"
    ],
    [
     "8.1",
     "G",
     "VII",
     "diatonic"
    ]
   ],
   "lines": [
    {
     "n": 1,
     "text": "The ninety-four is running late",
     "syllables": 8,
     "rhyme": "A",
     "target": 8
    },
    {
     "n": 2,
     "text": "The windows sweat with city air",
     "syllables": 8,
     "rhyme": "B",
     "target": 8
    },
    {
     "n": 3,
     "text": "I trace your name and watch the gate",
     "syllables": 8,
     "rhyme": "A",
     "target": 8
    },
    {
     "n": 4,
     "text": "Of every stop that isn't there",
     "syllables": 9,
     "rhyme": "B",
     "range": "8-9",
     "target": 8
    }
   ]
  },
  {
   "id": "S3",
   "type": "verse",
   "label": "Verse 2",
   "start_bar": 17,
   "bars": 16,
   "metre": "8",
   "scheme": "ABAB",
   "chords": [
    [
     "1.1",
     "Am",
     "i",
     "diatonic"
    ],
    [
     "2.1",
     "F",
     "VI",
     "diatonic"
    ],
    [
     "3.1",
     "C",
     "III",
     "diatonic"
    ],
    [
     "4.1",
     "G",
     "VII",
     "diatonic"
    ],
    [
     "5.1",
     "Am",
     "i",
     "diatonic"
    ],
    [
     "6.1",
     "F",
     "VI",
     "diatonic"
    ],
    [
     "7.1",
     "C",
     "III",
     "diatonic"
    ],
    [
     "8.1",
     "G",
     "VII",
     "diatonic"
    ],
    [
     "9.1",
     "Am",
     "i",
     "diatonic"
    ],
    [
     "10.1",
     "F",
     "VI",
     "diatonic"
    ],
    [
     "11.1",
     "C",
     "III",
     "diatonic"
    ],
    [
     "12.1",
     "G",
     "VII",
     "diatonic"
    ],
    [
     "13.1",
     "Am",
     "i",
     "diatonic"
    ],
    [
     "14.1",
     "F",
     "VI",
     "diatonic"
    ],
    [
     "15.1",
     "C",
     "III",
     "diatonic"
    ],
    [
     "16.1",
     "G",
     "VII",
     "diatonic"
    ]
   ],
   "lines": [
    {
     "n": 1,
     "text": "A man in orange sleeps it off",
     "syllable
... (shortened here)

targets, decoded:

[]

facts, decoded:

{
 "total_bars": 72,
 "duration": "3:08",
 "beats_per_bar": 4,
 "diatonic": [
  "Am",
  "Bdim",
  "C",
  "Dm",
  "Em",
  "F",
  "G"
 ],
 "borrowed": [
  "D",
  "E",
  "G#dim",
  "A",
  "Bm",
  "B7",
  "C7"
 ],
 "hooks": [
  {
   "text": "Night bus, carry me home",
   "at": [
    "S4.1",
    "S7.1"
   ]
  },
  {
   "text": "Through the orange and the chrome",
   "at": [
    "S4.2",
    "S7.2"
   ]
  },
  {
   "text": "Every light a little flame",
   "at": [
    "S4.3",
    "S7.3"
   ]
  },
  {
   "text": "Every stop without your name",
   "at": [
    "S4.4",
    "S7.4"
   ]
  }
 ],
 "checks": [
  "S6 chromatic: Bridge bar 2: Eb is outside A minor (neither diatonic nor borrowed).",
  "S6 chromatic: Bridge bar 6: Eb is outside A minor (neither diatonic nor borrowed).",
  "S3 echo: Verse 2 gives away the chorus: “night”, “bus” also open the chorus."
 ]
}

Reply:

{
 "task": "critique",
 "summary": "The chorus is a real hook - \"Night bus, carry me home\" is the title, it opens the chorus and the Dm-Eb-E bridge gives the song a genuine harmonic turn. The biggest problem is the wait: an 8-bar intro, Verse 1 and a 16-bar Verse 2 back to back put the first chorus at bar 33, about 1:23 at 92 bpm, far too late for a battle-of-the-bands set.",
 "hook": {
  "section": "S4",
  "n": 1,
  "advice": "The hook sits in the right place (S4 line 1, repeated at S7.1), but it only lands twice and the first time is at bar 33, roughly 1:23 in. Move the first chorus straight after Verse 1 and trim the intro, and it arrives at bar 13 (about 0:31). At 6 syllables against a 7 target, let the band hold \"home\" for the extra beat rather than padding the line."
 },
 "sections": [
  {
   "section": "S1",
   "verdict": "trim",
   "bars": 4,
   "advice": "8 bars of Am-F-C-G before a word is sung is the same loop the verse uses, so nothing is lost by halving it. Four bars, one pass of the progression, then the vocal."
  },
  {
   "section": "S2",
   "verdict": "keep",
   "advice": "Strong, concrete opening - the late ninety-four and sweating windows set the scene. Line 4 \"Of every stop that isn't there\" can sing as 9; sing \"ev'ry\" to keep it on 8."
  },
  {
   "section": "S3",
   "verdict": "trim",
   "bars": 8,
   "advice": "16 bars for four 8-syllable lines means each line is stretched over four bars or the progression loops twice with half of it empty. Cut to 8 bars like Verse 1. Also rewrite line 4: \"And still the night bus rolls away\" gives away the chorus's opening words right before it - try an image of the bus itself (the doors folding shut, the route sign) without \"night\" or \"bus\"."
  },
  {
   "section": "S4",
   "verdict": "keep",
   "advice": "Good hook and a clean F-G-Am lift. Lines 3 and 4 run 7-8 against 7; sing \"ev'ry\" to keep both on 7 and the hook line punchier."
  },
  {
   "section": "S5",
   "verdict": "keep",
   "advice": "Keep the solo for the battle-of-the-bands showcase, but after the reorder it follows Verse 2, so build it: half-time drums for 4 bars, full band for the last 4, so it climbs into the bridge instead of idling on the verse loop."
  },
  {
   "section": "S6",
   "verdict": "keep",
   "advice": "This is the best section on the board: the lyric turns the question back on the singer and the Dm-Eb-Am-E move changes the harmonic colour completely. Drop the band to bass and voice for the first 4 bars so the final chorus has somewhere to rise from."
  },
  {
   "section": "S7",
   "verdict": "extend",
   "bars": 16,
   "advice": "The final chorus should be the peak, but it is identical to the first. Double it to 16 bars - the second pass with the full band, a higher harmony on \"carry me home\" and the last line repeated."
  },
  {
   "section": "S8",
   "verdict": "trim",
   "bars": 4,
   "advice": "Another 8 bars of the verse loop after the peak drains the energy. Four bars, ending on Am, or a single repeat of the hook line sung alone over the last chord - the bus pulling away."
  }
 ],
 "moves": [
  {
   "section": "S4",
   "to": 3,
   "reason": "Verse 1 and Verse 2 run back to back with no chorus between them, 24 bars of the same Am-F-C-G loop. Put the first chorus after Verse 1 so the hook arrives near 0:31 and Verse 2 has something to return from."
  }
 ],
 "chords": [
  {
   "section": "S2",
   "bar": 8,
   "beat": 1,
   "from": "G",
   "to": "E",
   "reason": "With the chorus now following Verse 1, an E (V) on the last bar pulls hard toward A minor and then side-steps to the chorus's F - a deceptive lift that announces the hook."
  },
  {
   "section": "S4",
   "bar": 8,
   "beat": 1,
   "from": "Am",
   "to": "E",
   "reason": "Two bars of Am close the chorus flat; an E on bar 8 creates tension that carries the band into Verse 2 instead of stopping."
  },
  {
   "section": "S6",
   "bar": 6,
   "beat": 1,
   "from": "Eb",
   "to": "E",
   "reason": "Keep the first Eb at bar 2 - it is the bridge's jolt, the moment the song looks out the window. On the repeat, E (V) makes a real cadence into Am on bar 7 and resolves the doubt."
  }
 ],
 "notes": [
  "The Eb in S
... (shortened here; the page shows it whole)

lyrics: a 12-bar blues school song - every AAB verse still to write

Input:

{
 "task": "lyrics",
 "title": "Sort It Out",
 "brief": "(a JSON string - decoded below)",
 "song": "(a JSON string - decoded below)",
 "targets": "(a JSON string - decoded below)",
 "facts": "(a JSON string - decoded below)"
}

brief, decoded:

{
 "theme": "recycling week at school: paper, cans, bottles and the compost bin - sorting rubbish is a superpower",
 "mood": "bouncy, call and response, a little silly",
 "audience": "children aged 7-8 singing at a school assembly",
 "pov": "we"
}

song, decoded:

{
 "key": "E",
 "key_label": "E major",
 "tempo": 116,
 "meter": "4/4",
 "sections": [
  {
   "id": "S1",
   "type": "intro",
   "label": "Intro",
   "start_bar": 1,
   "bars": 4,
   "chords": [
    [
     "1.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "2.1",
     "A7",
     "IV7",
     "borrowed"
    ],
    [
     "3.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "4.1",
     "B7",
     "V7",
     "diatonic"
    ]
   ],
   "lines": []
  },
  {
   "id": "S2",
   "type": "verse",
   "label": "Verse 1",
   "start_bar": 5,
   "bars": 12,
   "metre": "10",
   "scheme": "AAB",
   "chords": [
    [
     "1.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "2.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "3.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "4.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "5.1",
     "A7",
     "IV7",
     "borrowed"
    ],
    [
     "6.1",
     "A7",
     "IV7",
     "borrowed"
    ],
    [
     "7.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "8.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "9.1",
     "B7",
     "V7",
     "diatonic"
    ],
    [
     "10.1",
     "A7",
     "IV7",
     "borrowed"
    ],
    [
     "11.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "12.1",
     "B7",
     "V7",
     "diatonic"
    ]
   ],
   "lines": [
    {
     "n": 1,
     "text": "",
     "syllables": 0,
     "rhyme": "",
     "target": 10
    },
    {
     "n": 2,
     "text": "",
     "syllables": 0,
     "rhyme": "",
     "target": 10
    },
    {
     "n": 3,
     "text": "",
     "syllables": 0,
     "rhyme": "",
     "target": 10
    }
   ]
  },
  {
   "id": "S3",
   "type": "verse",
   "label": "Verse 2",
   "start_bar": 17,
   "bars": 12,
   "metre": "10",
   "scheme": "AAB",
   "chords": [
    [
     "1.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "2.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "3.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "4.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "5.1",
     "A7",
     "IV7",
     "borrowed"
    ],
    [
     "6.1",
     "A7",
     "IV7",
     "borrowed"
    ],
    [
     "7.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "8.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "9.1",
     "B7",
     "V7",
     "diatonic"
    ],
    [
     "10.1",
     "A7",
     "IV7",
     "borrowed"
    ],
    [
     "11.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "12.1",
     "B7",
     "V7",
     "diatonic"
    ]
   ],
   "lines": [
    {
     "n": 1,
     "text": "",
     "syllables": 0,
     "rhyme": "",
     "target": 10
    },
    {
     "n": 2,
     "text": "",
     "syllables": 0,
     "rhyme": "",
     "target": 10
    },
    {
     "n": 3,
     "text": "",
     "syllables": 0,
     "rhyme": "",
     "target": 10
    }
   ]
  },
  {
   "id": "S4",
   "type": "solo",
   "label": "Kazoo solo",
   "start_bar": 29,
   "bars": 12,
   "chords": [
    [
     "1.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
     "2.1",
     "E7",
     "I7",
     "borrowed"
    ],
    [
  
... (shortened here)

targets, decoded:

[
 {
  "section": "S2",
  "lines": 3,
  "keep_existing": false
 },
 {
  "section": "S3",
  "lines": 3,
  "keep_existing": false
 },
 {
  "section": "S5",
  "lines": 3,
  "keep_existing": false
 }
]

facts, decoded:

{
 "total_bars": 56,
 "duration": "1:56",
 "beats_per_bar": 4,
 "diatonic": [
  "E",
  "F#m",
  "G#m",
  "A",
  "B",
  "C#m",
  "D#dim"
 ],
 "borrowed": [
  "Am",
  "C",
  "D",
  "G",
  "Bm",
  "F#dim",
  "F#7",
  "C#7",
  "G#7"
 ],
 "hooks": [],
 "checks": []
}

Reply:

{
 "task": "lyrics",
 "sections": [
  {
   "section": "S2",
   "lines": [
    {
     "n": 1,
     "text": "Paper in the paper bin, nice and flat",
     "syllables": 10,
     "rhyme": "A",
     "note": "leader's call: the first job, paper"
    },
    {
     "n": 2,
     "text": "We said paper, paper bin, nice and flat",
     "syllables": 10,
     "rhyme": "A",
     "note": "everyone answers, blues repeat of line 1"
    },
    {
     "n": 3,
     "text": "Sorting is our superpower, hooray!",
     "syllables": 10,
     "rhyme": "B",
     "note": "answer line states the big idea"
    }
   ]
  },
  {
   "section": "S3",
   "lines": [
    {
     "n": 1,
     "text": "Stamp on the empty cans and hear them crunch",
     "syllables": 10,
     "rhyme": "A",
     "note": "call: a stomping action for the cans"
    },
    {
     "n": 2,
     "text": "Squish the fizzy cans from yesterday's lunch",
     "syllables": 10,
     "rhyme": "A",
     "note": "varied repeat with a silly lunch detail"
    },
    {
     "n": 3,
     "text": "Bottles go clink-clink-clink, then in they go",
     "syllables": 10,
     "rhyme": "B",
     "note": "answer moves on to bottles, sound effect"
    }
   ]
  },
  {
   "section": "S5",
   "lines": [
    {
     "n": 1,
     "text": "Apple cores and peels in the compost bin",
     "syllables": 10,
     "rhyme": "A",
     "note": "call: last stop, the compost bin"
    },
    {
     "n": 2,
     "text": "Banana skins, the wriggly worms dig in",
     "syllables": 10,
     "rhyme": "A",
     "note": "varied repeat, worms make it silly"
    },
    {
     "n": 3,
     "text": "Sort it out, and the whole school takes a bow",
     "syllables": 10,
     "rhyme": "B",
     "note": "title lands in the final answer line"
    }
   ]
  }
 ],
 "hook": {
  "text": "Sort it out, and the whole school takes a bow",
  "where": "S5.3"
 },
 "notes": [
  "No chorus on the board, so each verse ends on its own answer line; the title closes Verse 3.",
  "Line 1 of each verse suits a leader or one class, line 2 the whole hall."
 ]
}

The board counts and checks; the API does not

Over the API you get the model's lines and advice as JSON. The counting, the rhyme letters and the key checks are yours to run, and the board's own code is plain scripts you can load in node after global.window = {}, in this order: theory.js (Theory.analyze(chord, key), Theory.transpose(chord, from, to), Theory.diatonic(key)), lyric.js (Lyric.lineSyl(text), Lyric.scheme(lines), Lyric.rhymes(a, b)), song.js (Song.buildInput(song, {task, targets, why}) for this exact run body, Song.toChordPro(song), Song.parseChordPro(text)) and reconcile.js (Recon.normalize(Recon.parseResult(text), task) then Recon.reconcile(result, song, {task, targets}) for the proposals and their flags).

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://stanzabar.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"stanzabar"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.

# input.json: the run input object - {"task": "lyrics", "title": "...", "brief": "{...}", "song": "{...}", "targets": "[...]", "facts": "{...}"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses stanzabar:<task>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="stanzabar:lyrics:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}' 

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: stanzabar:lyrics:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json