← Calaverita
Calaverita API
Tokens

Drive Calaverita from your own code

The one metered read is available over HTTP: give it the person a calavera is about, how you know them, their quirks, a setting, a language, a stanza count and (optionally) your own draft, and it returns a verdict, the calavera in four-line stanzas (with a line-by-line English gloss when asked), three to five close-reading notes on its rhyme, rhythm, images, turn and tone, each changed line with why, any hurtful lines it softened, and a line to send it with. A natural use is a group-chat bot or a card tool that wants a ready-to-share Día de los Muertos verse.

The division of labour first: the page owns the scan; the model writes and reads; the page re-checks the reply. The approximate syllable counts, the rhyme letters, the search for Death and for jab words, and the verdict (by the same ordered rules the model gets) come from calakit.js, which runs in node as well as in the browser. Its check function holds each reply to the form: four-line stanzas, the stanza count asked for, Death present, the person named, no jab words in the poem, a gloss that matches, changes that quote real draft lines, no internal field names in prose.

Get a token on the token page (step 2 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":"calaverita"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field such as task or who 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 calavera 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

Every run sends task: calavera - there is one lane. The verdict is decided by five ordered, non-overlapping rules; the first that matches wins:

OrderVerdictWhen
1freshdraft is empty: a new calavera is written from the quirks.
2softenA draft line mocks a body, weight, looks, health, disability, age as decline, identity, money trouble, a divorce or a real loss, or wishes real harm.
3rewriteThe draft has fewer than four lines, no rhyming pair anywhere, or no Death in any form.
4tuneA stanza with no rhyming pair, a line 3+ syllables off its stanza, or a rhyme that twists word order.
5keepEverything else: at most two small changes.

The declared input schema carries scalars only, so array-valued fields travel JSON-encoded as strings (CalaKit.toWire does this). Every value is a string.

Input fields

FieldTypeNotes
taskstringcalavera.
whostringThe person, in your words, up to 80 characters (Priya, mi tío Beto). A gendered word here is the only way the poem learns a gender.
relationstringOne of friend, coworker, boss, sibling, cousin, parent, grandparent, partner, roommate, aunt or uncle, teacher, neighbour, team or group, myself, or empty.
quirksstringOne quirk per line; past 1,500 characters whole lines are cut from the middle with a marker.
wherestringAn optional setting, up to 120 characters.
langstringen, es or es+en (Spanish with an English gloss).
stanzasstring2, 3 or 4 - quatrains for a fresh, softened or rebuilt poem.
draftstringYour draft, one line per line, blank lines between stanzas; empty for a fresh poem. Past 3,000 characters the middle lines are cut with a marker.
page_scanstring (JSON)Array of strings, one per draft line: "3. ~8 syllables, rhyme A: y él, con el carbón a un lado,".
page_leanstringThe page's verdict from the scan, by the rules above.
page_flagsstring (JSON)Array of the page's warnings (a jab word, Death missing, a thin quirk list).
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object: task, verdict, title, summary, poem ({heading, stanzas: [[4 lines], ...], gloss: [[4 lines], ...]}, gloss empty unless lang is es+en), reading ([{aspect, note}], 3-5, aspect one of rhyme, rhythm, imagery, turn, tone), changes ([{was, now, why}], empty for fresh; was is "" for a brand-new line), jabs ([{line, issue, fix}], only for soften) and send_note. Parse it with CalaKit.parseReply, then normalize(reply), then check(reply, form), which returns {notes}.

Worked examples

Replies produced against the app's prompt for the five samples (made-up people with made-up quirks; long fields and lists shortened; the full replies ship in example-result.js). Together they cover every verdict.

a coworker, no draft - fresh

Input:

{
 "task": "calavera",
 "who": "Priya",
 "relation": "coworker",
 "quirks": "replies-all to every email\nkeeps forty browser tabs open\nbooks a meeting that could have been a message\nwaters a desk pl ... (136 characters)",
 "where": "the office",
 "lang": "en",
 "stanzas": "3",
 "draft": "",
 "page_scan": "[]",
 "page_lean": "fresh",
 "page_flags": "[]"
}

Reply:

{
 "task": "calavera",
 "verdict": "fresh",
 "title": "A Calavera for Priya's Tabs and Replies",
 "summary": "This is a fresh calavera, since no draft was given, matching the page's own lean. It's built entirely from Priya's quirks and the office setting: Death comes to call, but the reply-all habit, the forty tabs and the meeting that could have been a message keep delaying her, until Gerald the desk plant gives the poem its turn. No gender is assumed for Priya, and nothing beyond the quirks, relation and setting is added.",
 "poem": {
  "heading": "A Calavera for Priya",
  "stanzas": [
   [
    "Death stepped into the office, list out,",
    "ready to call out Priya's name.",
    "But Priya was deep in reply-all,",
    "on a chain of emails all the same."
   ],
   "... (3 stanzas in all)"
  ],
  "gloss": []
 },
 "reading": [
  {
   "aspect": "rhyme",
   "note": "Each stanza closes on a clean, unforced pair - \"name\"/\"same\", \"bus\"/\"fuss\", \"alive\"/\"survive\" - so the ear always has somewhere to land."
  },
  {
   "aspect": "rhythm",
   "note": "The lines hold a steady beat throughout, giving even the sprawl of \"a chain of emails all the same\" room to breathe without rushing."
  },
  "... (5 in all)"
 ],
 "changes": [],
 "jabs": [],
 "send_note": "Even Death couldn't argue with how well Priya keeps Gerald - the reply-alls live on 🌱"
}

a tio at the barbecue, Spanish draft - keep

Input:

{
 "task": "calavera",
 "who": "mi tío Beto",
 "relation": "aunt or uncle",
 "quirks": "cuenta el mismo chiste en cada fiesta\nse cree experto en el asado\ndice \"yo lo arreglo\" y nunca arregla nada",
 "where": "el asado familiar",
 "lang": "es",
 "stanzas": "2",
 "draft": "Llegó la Flaca al asado\nbuscando a mi tío Beto,\ny él, con el carbón a un lado,\nle contó un chiste completo.\n\n«Ese chiste ya lo oí»,\ndijo la  ... (211 characters)",
 "page_scan": "[\"1. ~8 syllables, rhyme A: Llegó la Flaca al asado\",\"2. ~8 syllables, rhyme B: buscando a mi tío Beto,\",\"3. ~8 syllables, rhyme A: y él, con el carbón a un lado,\",\"4. ~8 syllables ... (436 characters)",
 "page_lean": "keep",
 "page_flags": "[]"
}

Reply:

{
 "task": "calavera",
 "verdict": "keep",
 "title": "Tío Beto Outsmarts La Flaca",
 "summary": "Verdict: keep. Your draft already has the calavera's form — La Flaca arrives in line 1, lines 2 and 4 rhyme cleanly in both stanzas (asado/lado, aburrida/vida), and every line sits close to eight syllables. The last stanza's turn already lands: offering to fix Death's scythe (\"yo te arreglo la guadaña\") is your own \"yo lo arreglo\" quirk doing the work, and it's what sends her running. Nothing needed changing.",
 "poem": {
  "heading": "Calavera de mi tío Beto",
  "stanzas": [
   [
    "Llegó la Flaca al asado",
    "buscando a mi tío Beto,",
    "y él, con el carbón a un lado,",
    "le contó un chiste completo."
   ],
   "... (2 stanzas in all)"
  ],
  "gloss": []
 },
 "reading": [
  {
   "aspect": "rhyme",
   "note": "The -ado chime linking \"asado\" and \"...a un lado\" opens the first stanza cleanly, and \"aburrida\"/\"vida\" close the second on a true -ida rhyme — both stanzas land the required 2-and-4 match without forcing a word out of order."
  },
  {
   "aspect": "rhythm",
   "note": "Lines stay close to the octosílabo throughout; \"le contó un chiste completo\" and \"y ella escapó por su vida\" keep the same even four-beat gait as their neighbors."
  },
  "... (5 in all)"
 ],
 "changes": [],
 "jabs": [],
 "send_note": "Para el tío que siempre \"lo arregla\" — ¡feliz Día de Muertos! 💀"
}

a roommate, a draft with a runaway line - tune

Input:

{
 "task": "calavera",
 "who": "Jules",
 "relation": "roommate",
 "quirks": "never washes the blender\nsings in the shower at 6 am\nlabels every shelf in the fridge",
 "where": "our flat",
 "lang": "en",
 "stanzas": "2",
 "draft": "The Reaper came at six a.m.\nto take our roommate Jules away,\nbut Jules was singing in the shower so loudly that Death just stood in the hall ... (358 characters)",
 "page_scan": "[\"1. ~8 syllables, rhyme A: The Reaper came at six a.m.\",\"2. ~9 syllables, rhyme B: to take our roommate Jules away,\",\"3. ~24 syllables, rhyme C: but Jules was singing in the showe ... (588 characters)",
 "page_lean": "tune",
 "page_flags": "[]"
}

Reply:

{
 "task": "calavera",
 "verdict": "tune",
 "title": "Jules Stalls Death With a Dirty Blender",
 "summary": "This is a tune: Death already shows up in line 1 and the first stanza already rhymes on \"away\"/\"Tuesday,\" but line 3 ran to roughly 24 syllables against the stanza's 8-9, and the second stanza had no rhyming pair at all. I trimmed line 3 to match its neighbors and swapped the second stanza's closing word so it rhymes with \"inside.\" Every joke - the shower, the blender, the shelf labels - is exactly as you wrote it.",
 "poem": {
  "heading": "A Calavera for Jules",
  "stanzas": [
   [
    "The Reaper came at six a.m.",
    "to take our roommate Jules away,",
    "but Jules sang on, still in the shower,",
    "then labelled Death's own shelf: \"Tuesday\"."
   ],
   "... (2 stanzas in all)"
  ],
  "gloss": []
 },
 "reading": [
  {
   "aspect": "rhyme",
   "note": "\"away\" and \"Tuesday\" carry the first stanza's rhyme cleanly, and the second stanza now has one too, once the close is \"cried\" to match \"inside.\""
  },
  {
   "aspect": "rhythm",
   "note": "trimming line 3 to \"still in the shower\" pulls it back to the stanza's eight-nine syllable beat; the old runaway line had thrown the whole quatrain off step."
  },
  "... (5 in all)"
 ],
 "changes": [
  {
   "was": "but Jules was singing in the shower so loudly that Death just stood in the hall and had to wait,",
   "now": "but Jules sang on, still in the shower,",
   "why": "the line ran to about 24 syllables against the stanza's 8-9, so it's trimmed to match while keeping Jules singing through Death's visit."
  },
  {
   "was": "\"I'll come back when it's clean,\" she said.",
   "now": "\"I'll come back when it's clean,\" she cried.",
   "why": "the stanza had no rhyming pair at all; swapping the closing word makes it rhyme with line 2's \"inside.\""
  }
 ],
 "jabs": [],
 "send_note": "Jules lives to label another shelf 💀"
}

a gym friend, three unrhymed lines - rewrite

Input:

{
 "task": "calavera",
 "who": "Marco",
 "relation": "friend",
 "quirks": "never skips leg day\nposts every workout online\nbrings a protein shake to dinner",
 "where": "the gym",
 "lang": "en",
 "stanzas": "2",
 "draft": "Marco goes to the gym at dawn,\nposts a selfie of every rep\nand orders a protein shake at dinner.",
 "page_scan": "[\"1. ~8 syllables, rhyme A: Marco goes to the gym at dawn,\",\"2. ~9 syllables, rhyme B: posts a selfie of every rep\",\"3. ~10 syllables, rhyme C: and orders a protein shake at dinner ... (183 characters)",
 "page_lean": "rewrite",
 "page_flags": "[\"Death is missing from the draft.\"]"
}

Reply:

{
 "task": "calavera",
 "verdict": "rewrite",
 "title": "Leg Day Saves Marco from Death",
 "summary": "This is a rewrite, since your draft ran only three lines, had no two line-endings that rhymed, and never brought Death into the poem. The new two-stanza version keeps your three quirks - leg day, posting every workout, and the dinner protein shake - and lets the shake itself become the thing Death takes instead of Marco.",
 "poem": {
  "heading": "A calavera for Marco",
  "stanzas": [
   [
    "Marco never skips leg day,",
    "and posts it live for all to see,",
    "so Death pulled up a chair to watch,",
    "curious what the feed would be."
   ],
   "... (2 stanzas in all)"
  ],
  "gloss": []
 },
 "reading": [
  {
   "aspect": "rhyme",
   "note": "The ABCB rhymes land clean in both stanzas - 'see' answers 'be', and 'shake' answers 'take', the second pair carrying the poem's whole joke."
  },
  {
   "aspect": "rhythm",
   "note": "Lines settle into a steady eight syllables apiece, like 'curious what the feed would be,' giving the poem an even, conversational pace rather than a sing-song bounce."
  },
  "... (5 in all)"
 ],
 "changes": [
  {
   "was": "Marco goes to the gym at dawn,",
   "now": "Marco never skips leg day,",
   "why": "Grounds the opening in the leg-day quirk by name and sets up a rhyme scheme the single flat line didn't have."
  },
  {
   "was": "posts a selfie of every rep",
   "now": "and posts it live for all to see,",
   "why": "Keeps the posting-online quirk and gives it a rhyme partner for the stanza's close."
  },
  "... (8 in all)"
 ],
 "jabs": [],
 "send_note": "Marco, leg day may be unskippable, but tonight it's Death buying the protein shake. 💀"
}

a sister, a draft that jokes about weight - soften (Spanish with gloss)

Input:

{
 "task": "calavera",
 "who": "mi hermana Sofi",
 "relation": "sibling",
 "quirks": "llega tarde a todo\nhabla con sus plantas\nnunca contesta los audios",
 "where": "",
 "lang": "es+en",
 "stanzas": "2",
 "draft": "La Calaca vino por Sofi\nun domingo en la mañana,\npero no pudo cargarla:\npesaba como una ballena.\n\nSofi llegó tarde al panteón,\nhabló con las ... (222 characters)",
 "page_scan": "[\"1. ~9 syllables, rhyme A: La Calaca vino por Sofi\",\"2. ~8 syllables, rhyme B: un domingo en la mañana,\",\"3. ~8 syllables, rhyme B: pero no pudo cargarla:\",\"4. ~9 syllables, rhyme ... (451 characters)",
 "page_lean": "soften",
 "page_flags": "[\"Line 4: \\\"pesaba\\\" reads as a jab at looks or weight.\"]"
}

Reply:

{
 "task": "calavera",
 "verdict": "soften",
 "title": "Sofi Keeps Her Plants, Loses Death",
 "summary": "Verdict: soften. The page's flag is right — the first stanza's punchline, \"pesaba como una ballena,\" turns Death's failed pickup into a joke about Sofi's weight, which the house rules rule out even though her lateness and plant-chats are fair game. I replaced only that line with a reason pulled from a real quirk — she wouldn't let go of her plants — so the rhyme with \"mañana\" and \"cargarla\" still holds, and left every other line exactly as you wrote it.",
 "poem": {
  "heading": "Calavera de Sofi",
  "stanzas": [
   [
    "La Calaca vino por Sofi",
    "un domingo en la mañana,",
    "pero no pudo cargarla:",
    "pues no soltaba sus plantas."
   ],
   "... (2 stanzas in all)"
  ],
  "gloss": [
   [
    "Death came for Sofi,",
    "one Sunday morning,",
    "but she couldn't carry her off:",
    "because she wouldn't let go of her plants."
   ],
   "... (2 stanzas in all)"
  ]
 },
 "reading": [
  {
   "aspect": "rhyme",
   "note": "The loose a-a drone running through 'mañana,' 'cargarla,' and now 'plantas' holds the first stanza together without forcing an exact echo."
  },
  {
   "aspect": "rhythm",
   "note": "Line 7's 'y la Flaca, cansada de esperar,' runs a beat longer than its neighbors, landing like an exasperated sigh rather than a stumble."
  },
  "... (4 in all)"
 ],
 "changes": [
  {
   "was": "pesaba como una ballena.",
   "now": "pues no soltaba sus plantas.",
   "why": "drops the weight joke and gives Death a reason to fail that comes from an actual quirk, while keeping the line's rhyme and syllable count."
  }
 ],
 "jabs": [
  {
   "line": "pesaba como una ballena.",
   "issue": "compares her body to a whale, mocking her weight.",
   "fix": "the new line keeps the same 'couldn't carry her off' joke but blames it on her refusing to put down her plants, not her body."
  }
 ],
 "send_note": "Para Sofi: ni la Calaca pudo con sus plantas 🌿"
}

Build the input yourself

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

const K = CalaKit;
const form = { who: "Jules", relation: "roommate", where: "our flat", lang: "en", stanzas: "2",
  quirks: "never washes the blender\nsings in the shower at 6 am",
  draft: "The Reaper came at six a.m.\nto take our roommate Jules away,\n..." };
console.log(K.lean(form), K.scan(form.draft, "en"));      // the page's verdict and its line-by-line scan
const wire = K.toWire(K.buildInput(form).input);           // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// const { notes } = K.check(reply, form);                 // the page's re-check of the reply
// K.svgCard(reply)                                        // a printable card
// K.esLine("Llegó la Flaca al asado")                     // 8 - approximate Spanish metric syllables

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://calaverita.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":"calaverita"}' | 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": "calavera", "who": "Priya", "relation": "coworker", "quirks": "replies-all to every email\n...", "where": "the office", "lang": "en", "stanzas": "3", "draft": "", "page_scan": "[]", "page_lean": "fresh", "page_flags": "[]"}
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 calaverita:<task>:<inputHash>:a<attempt> (there is one lane, calavera), 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="calaverita:calavera:$(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: calaverita:calavera:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json