← Mic Rotation
Mic Rotation API
Tokens

Drive Mic Rotation from your own code

The one metered step - the running-order check of one karaoke night's sign-up sheet - is available over HTTP. A natural use is a sign-up form or a venue's queue screen: your code sends the sheet with the page's reading, and gets back every slot in a new order with a role (opener, build, peak, breather, sing-along, closer), the slots to cut when the night cannot fit the booking, a few host cues and a keep / reshuffle / rebuild verdict.

The division of labour first: the page reads and flags; the model orders; the page re-checks the model's order with its own rules. Reading the sheet, the estimates for missing lengths and energies, the booking clock and every pacing flag (back to back, second turn, slump, burnout, opener, closer, tough follow, repeat, overrun) come from setkit.js, which runs in node as well as in the browser. Its check function then holds the reply to the sheet: every slot id must be real and used once, slots the reply forgets keep their sign-up place at the end, every quote must appear in the slot's line as written, and all the flags and the clock are re-run on the new order.

Get a token on the token page (step 1 below also shows how to mint a guest token in code).

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":"mic-rotation"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or task is missing.
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 check is metered and needs 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

Every run sends task. There is one task, order. It returns title, verdict, summary; order [{slot, role, says, why}] (every kept slot, in the new order); cuts [{slot, says, why}] (empty unless the night cannot fit the booking); cues [{before, line}] (2-4 host lines); host_notes (0-3).

VerdictMeaning
keepThe order as signed up already builds and fits: no fix or worth-a-look flag. The reply keeps sign-up order and may still add roles and cues.
reshuffleThe night fits the booking but the order breaks a rule (a singer twice in a row, a second turn before a first, a slump, a burnout run, a slow opener, a flat closer, a first-timer after a showstopper): slots move, nothing is cut.
rebuildThe night cannot fit the booking or carries a repeated song: slots move and some are cut - a repeat first, then second turns, then the longest slow songs.
FieldValuesWhat the page does with it
order[].slot, cuts[].slotS1 ... SnSlot ids from slot_list. An unknown or repeated id is not counted; a slot in neither list keeps its sign-up place at the end.
order[].roleopener, build, peak, breather, singalong, closerMarked above each column of the curve and on the setlist. A first slot that is not the opener, or a last slot that is not the closer, gets a note.
cues[].beforea slot id in orderShown as a host line above that slot; a cue before a cut or unknown slot is dropped.

The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (SetKit.toWire does this). Only task is required by the schema.

Input fields

FieldTypeNotes
taskstringAlways order.
todaystringYYYY-MM-DD.
signupsstringThe sheet as pasted (singer | song | minutes | energy | type). Past 6,000 characters the middle is cut on whole lines around a marker.
factsstring (JSON){night, setting, slots, singers, start, booking_min, changeover_sec, runtime_min, overrun_min, first_timers, slow_slots, big_slots}. setting is room, bar or party; booking_min 0 means no booking was given.
slot_liststring (JSON)[{id, singer, song, minutes, len_est, energy, energy_est, type, first_timer}]; len_est / energy_est mark the page's estimates (3.5 min, energy 3).
page_readsstring (JSON)[{level, code, at, text}]; level is fix, warn or info; at lists the slot ids.
page_verdictstringThe page's own reading: keep, reshuffle or rebuild.
notesstringOptional, the host's own comments; past 1,200 characters the middle is cut.
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object. Parse it with SetKit.parseReply, then normalize, then check it with check(reply, state), which returns {rows, skipped, cuts, order, an, moved, cues, notes}: the placed slots with their roles, the entries not counted, the applied cuts, the new order, the page's analysis of it (clock, runtime, overrun, flags), how many slots moved, the cues kept, and the notes (ok, info, warn, fix) the page shows under "Checked by the page".

Worked examples

Replies produced against the app's prompt for the sample nights (made-up singers; song lengths and energies are rough estimates; lists shortened; the full replies ship in example-result.js). The page replays each of them free from its sample buttons. Inputs are shown with the long fields folded.

task: order - Team night, already paced - keep

Input:

{
 "task": "order",
 "today": "2026-10-20",
 "signups": "Dana | Don't Stop Believin' - Journey | 4:10 | 4 | solo\nMarcus | Valerie - Amy Winehouse | 3:40 | 4 | solo\nKeiko & Ravi | Don't Go Breaking My Heart - Elton Joh ... (679 characters)",
 "facts": "{\"night\":\"Team end-of-year night\",\"setting\":\"room\",\"slots\":12,\"singers\":10,\"start\":\"19:30\",\"booking_min\":60,\"changeover_sec\":30,\"runtime_min\":56.7,\"overrun_min\":-3.3,\"first_timers\":[\"Tom\"],\"slow_slots\":2,\"big_slots\":7}",
 "slot_list": "[{\"id\":\"S1\",\"singer\":\"Dana\",\"song\":\"Don't Stop Believin' - Journey\",\"minutes\":4.2,\"len_est\":false,\"energy\":4,\"energy_est\":false,\"type\":\"solo\",\"first_timer\":false},{\"id\":\"S2\",\"singer\":\"Marcus\",\"song\":\"Valerie - Amy Winehouse\",\"minutes\":3.7,\"len_est\":false,\"energy\":4,\"energy_est\":false,\"type\":\"solo\",\"first_timer\":false},\"... 10 more\"]",
 "page_reads": "[]",
 "page_verdict": "keep",
 "notes": "Private room for an hour. Tom has never done karaoke before."
}

Reply:

{
 "task": "order",
 "title": "Team End-of-Year Karaoke Night",
 "verdict": "keep",
 "summary": "Keep the order - the sign-up sheet already builds without tripping any of the host's rules, and it finishes about 3 minutes under the hour-long booking. The early peaks from Dana and Marcus settle into Keiko & Ravi's duet before Tom's first-timer solo, then big numbers from Bea, Jen and Priya alternate with breathers from Ola and Sam so the room never sags or burns out. Marcus and Dana's duet gives everyone a second-turn sing-along before Bea's second turn, and the whole group closes on Bohemian Rhapsody, the biggest number of the night.",
 "order": [
  {
   "slot": "S1",
   "role": "opener",
   "says": "Dana | Don't Stop Believin' - Journey",
   "why": "Confident energy-4 solo, not a first-timer - warms the room right away."
  },
  {
   "slot": "S2",
   "role": "peak",
   "says": "Marcus | Valerie - Amy Winehouse",
   "why": "A second big number keeps the momentum climbing after the opener."
  },
  {
   "slot": "S3",
   "role": "singalong",
   "says": "Keiko & Ravi | Don't Go Breaking My Heart - Elton John & Kiki Dee",
   "why": "A duet the room can clap along to - a breather after two big solos."
  },
  {
   "slot": "S4",
   "role": "build",
   "says": "Tom | Wonderwall - Oasis",
   "why": "Tom's first-timer solo lands early, straight after the duet rather than after a showstopper."
  },
  "..."
 ],
 "cuts": [],
 "cues": [
  {
   "before": "S1",
   "line": "Let's get this party started - Dana's opening us up with Don't Stop Believin'!"
  },
  {
   "before": "S4",
   "line": "This next one's a first for someone - Tom's never done karaoke before, so let's hear it for him on Wonderwall."
  },
  "..."
 ],
 "host_notes": [
  "The sheet runs about 3 minutes under the hour as signed up, so there's a little slack if changeovers run long.",
  "No one needs to leave early per the notes, so the order can run straight through without rearranging for a departure."
 ]
}

task: order - Birthday room, lumpy order - reshuffle

Input:

{
 "task": "order",
 "today": "2026-10-20",
 "signups": "Nia | Someone Like You - Adele | 4:45 | 1 | solo\nEveryone | Don't Stop Me Now - Queen | 3:35 | 5 | group\nTheo | Livin' on a Prayer - Bon Jovi | 4:10 | 5 | solo\n ... (790 characters)",
 "facts": "{\"night\":\"Nia's birthday room\",\"setting\":\"room\",\"slots\":14,\"singers\":11,\"start\":\"20:00\",\"booking_min\":90,\"changeover_sec\":45,\"runtime_min\":71.3,\"overrun_min\":-18.7,\"first_timers\":[\"Ben\"],\"slow_slots\":5,\"big_slots\":7}",
 "slot_list": "[{\"id\":\"S1\",\"singer\":\"Nia\",\"song\":\"Someone Like You - Adele\",\"minutes\":4.8,\"len_est\":false,\"energy\":1,\"energy_est\":false,\"type\":\"solo\",\"first_timer\":false},{\"id\":\"S2\",\"singer\":\"Everyone\",\"song\":\"Don't Stop Me Now - Queen\",\"minutes\":3.6,\"len_est\":false,\"energy\":5,\"energy_est\":false,\"type\":\"group\",\"first_timer\":false},\"... 12 more\"]",
 "page_reads": "[{\"level\":\"fix\",\"code\":\"back-to-back\",\"at\":[\"S3\",\"S4\"],\"text\":\"Theo sings S3 and S4 back to back - no breath, and the room watches one person twice.\"},{\"level\":\"warn\",\"code\":\"second-turn\",\"at\":[\"S4\"],\"text\":\"Theo gets a second turn at S4 before everyone has had a first.\"},\"... 6 more\"]",
 "page_verdict": "reshuffle",
 "notes": "It's Nia's 30th. Ben has never sung karaoke before and is nervous. Kat has to leave at 21:00 sharp."
}

Reply:

{
 "task": "order",
 "title": "Nia's 30th Birthday Karaoke Rotation",
 "verdict": "reshuffle",
 "summary": "This is a reshuffle, not a keep or a rebuild: the set already fits the 90-minute booking with about 19 minutes to spare, so nothing needs to be cut, but the page's fix-level flags - Theo singing back to back, three slow songs in a row, a slow opener and a slow closer - mean the sign-up order can't stand as is. The new order opens on Theo's energy-4 'Sweet Caroline' and then alternates big numbers (Grace, the Lou & Kat duet, Zoe, Jay) with slow breathers so no three slow songs and no four big songs ever stack, tucking Ben's first-timer turn in early right after a mid-energy song instead of after a showstopper. It climbs back up through Nia's and Theo's second turns into the group sing-along 'Don't Stop Me Now' as the closer, which is no one's turn and brings the whole room in. Kat's two turns - the duet and The Scientist - both land by about 20:56, well ahead of her 21:00 cutoff.",
 "order": [
  {
   "slot": "S4",
   "role": "opener",
   "says": "Theo | Sweet Caroline",
   "why": "an energy-4 song opens the room warm without going to the biggest peak yet"
  },
  {
   "slot": "S13",
   "role": "build",
   "says": "Rae | Torn",
   "why": "a mid-energy song settles the room after the opener and sets up Ben's first turn"
  },
  {
   "slot": "S6",
   "role": "build",
   "says": "Ben | Creep",
   "why": "Ben's first time up, called right after a mid-energy song, never after a showstopper"
  },
  {
   "slot": "S5",
   "role": "peak",
   "says": "Grace | I Will Always Love You",
   "why": "a big number rises after the quieter opening stretch"
  },
  "..."
 ],
 "cuts": [],
 "cues": [
  {
   "before": "S6",
   "line": "First time at the mic tonight - let's hear it for Ben!"
  },
  {
   "before": "S9",
   "line": "This one's a duet - sing along with Lou and Kat if you know the words."
  },
  "..."
 ],
 "host_notes": [
  "Kat's two turns - the duet with Lou and The Scientist - both wrap by about 20:56, ahead of her 21:00 cutoff.",
  "The set runs about 71 minutes against the 90-minute booking, so there's time in hand even with Kat leaving early.",
  "..."
 ]
}

task: order - Bar open mic, overbooked - rebuild

Input:

{
 "task": "order",
 "today": "2026-10-20",
 "signups": "Carlos | Hotel California - Eagles | 6:30 | 2 | solo\nDee | Mr. Brightside - The Killers | 3:45 | 5 | solo\nCarlos | Wonderwall - Oasis | 4:20 | 3 | solo\nFinn | H ... (760 characters)",
 "facts": "{\"night\":\"Thursday open mic\",\"setting\":\"bar\",\"slots\":15,\"singers\":10,\"start\":\"21:00\",\"booking_min\":55,\"changeover_sec\":60,\"runtime_min\":82.2,\"overrun_min\":27.2,\"first_timers\":[\"Mo\"],\"slow_slots\":4,\"big_slots\":6}",
 "slot_list": "[{\"id\":\"S1\",\"singer\":\"Carlos\",\"song\":\"Hotel California - Eagles\",\"minutes\":6.5,\"len_est\":false,\"energy\":2,\"energy_est\":false,\"type\":\"solo\",\"first_timer\":false},{\"id\":\"S2\",\"singer\":\"Dee\",\"song\":\"Mr. Brightside - The Killers\",\"minutes\":3.8,\"len_est\":false,\"energy\":5,\"energy_est\":false,\"type\":\"solo\",\"first_timer\":false},\"... 13 more\"]",
 "page_reads": "[{\"level\":\"warn\",\"code\":\"second-turn\",\"at\":[\"S3\"],\"text\":\"Carlos gets a second turn at S3 before everyone has had a first.\"},{\"level\":\"warn\",\"code\":\"second-turn\",\"at\":[\"S6\"],\"text\":\"Dee gets a second turn at S6 before everyone has had a first.\"},\"... 9 more\"]",
 "page_verdict": "rebuild",
 "notes": "Bar open mic. First turns go in sign-up order where possible; second turns only if time allows. The bar stops the mic at 21:55 sharp."
}

Reply:

{
 "task": "order",
 "title": "Thursday Open Mic Running Order",
 "verdict": "rebuild",
 "summary": "This is a rebuild: the sheet has 'Wonderwall' signed up twice, opens on a slow song and closes on the quietest one, and runs 82 minutes against a 55-minute booking - 27 minutes over, too much to talk away. Cutting the repeated Wonderwall sign-up and the four second turns (Hank, Finn, Gia and Dee's return trips) brings the night to about 53 minutes, inside the 21:55 cutoff, while keeping everyone's first turn. The new order opens warm on Finn's Hallelujah, brings Mo's first time up early, then climbs through three peaks - Sweet Caroline, Mr. Brightside and Bohemian Rhapsody, each eased by a breather - and closes on Rolling in the Deep, the biggest energy left on the sheet since there is no group or duet number to hand the close to.",
 "order": [
  {
   "slot": "S4",
   "role": "opener",
   "says": "Finn | Hallelujah",
   "why": "Opens the night at mid energy, since the sheet's own first slot (Hotel California) is too slow to open on."
  },
  {
   "slot": "S15",
   "role": "build",
   "says": "Mo | Someone Like You",
   "why": "Mo's first time up, moved early so the first-timer goes on right after the mid-energy opener, not after a showstopper."
  },
  {
   "slot": "S12",
   "role": "peak",
   "says": "Leo | Sweet Caroline",
   "why": "The first big number, kept lower than the peaks that follow so the energy keeps climbing."
  },
  {
   "slot": "S1",
   "role": "breather",
   "says": "Carlos | Hotel California",
   "why": "A calmer song after the first peak, before the next build."
  },
  "..."
 ],
 "cuts": [
  {
   "slot": "S3",
   "says": "Carlos | Wonderwall",
   "why": "'Wonderwall' is on the sheet twice; this is Carlos's second turn while Ivy's S8 is her only turn, so this copy goes and Ivy's stays."
  },
  {
   "slot": "S14",
   "says": "Hank | Tennessee Whiskey",
   "why": "Still over the booking after the repeat cut; this is Hank's second turn and the latest-signed second turn on the sheet, so it goes first."
  },
  "..."
 ],
 "cues": [
  {
   "before": "S4",
   "line": "Let's get the night going - Finn, come on up with Hallelujah."
  },
  {
   "before": "S15",
   "line": "First time at the mic tonight - let's hear it for Mo, singing Someone Like You."
  },
  "..."
 ],
 "host_notes": [
  "The mic stops at 21:55 sharp; this order runs about 53 minutes, so keep the gaps between songs tight.",
  "If there's spare time at the end, Dee's cut second turn (Valerie) was the last one trimmed and the easiest to add back."
 ]
}

Build the input yourself

Load the script in node (global.window = {}; require("./setkit.js")), then:

const K = SetKit;
const st = K.state({
  today: "2026-10-20", night: "Friday karaoke", start: "20:00", booking: "90", changeover: "45", setting: "room",
  newbies: "Ben", notes: "",
  signups: "Theo | Sweet Caroline - Neil Diamond | 3:25 | 4 | solo\nBen | Creep - Radiohead | 3:55 | 2 | solo\nEveryone | Don't Stop Me Now - Queen | 3:35 | 5 | group"
});
console.log(st.slots, st.an.flags, st.verdict);
const { input } = K.buildInput(st, { today: "2026-10-20" });
const wire = K.toWire(input);           // POST this to /estimate and /run
// later: const reply = K.normalize(K.parseReply(job.output.output));
// const { order, cuts, an, notes } = K.check(reply, st);

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://mic-rotation.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":"mic-rotation"}' | 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, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "order", "today": "2026-10-20", "signups": "Theo | Sweet Caroline - Neil Diamond | 3:25 | 4 | solo ...", "facts": "{...}", "slot_list": "[...]", "page_reads": "[...]", "page_verdict": "reshuffle"}
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 mic-rotation:order:<inputHash>:a<attempt>, and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="mic-rotation:order:$(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: mic-rotation:order:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json