← Dry Erase
API

Drive Dry Erase from your own code

The one metered task, board, is available over HTTP. It takes rough notes explaining something - split into numbered points - and how many frames are wanted, and returns a whiteboard plan: which points go on which frame, a heading, one to three doodles with short labels, what is drawn between them, and a caption, all from the notes' own words.

The division of labour first: the page draws the board; the model plans it. The page splits the notes into points (up to twelve, 160 characters each), keeps back any point holding an email address, phone number or handle before anything is sent, and draws every frame stroke by stroke. After the reply it checks the plan against the notes. Your code can do the same with the engine files the page loads (see "Build the input yourself").

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": "..." } }

The token is minted for this app (the guest call takes {"slug":"dry-erase"}), 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 (task, topic, points, point_count, frames) 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 planned board is metered and needs a personal sign-in.
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, the input and the rules

Every run sends task: board (the one task is board); a missing or different one is still answered as board. All fields are strings.

FieldRequiredWhat it holds
taskyesAlways board.
topicyesWhat the notes explain, in the person's words (up to 80 characters).
pointsyesNumbered lines N. the point, at most twelve. Build it with BoardKit.buildInput, which splits the notes and keeps back points holding contact details.
point_countyesHow many points points has, as a string.
framesyesFrames wanted, 3 to 6, as a string.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

The fit is checked in this order: thin (fewer points than frames - one frame per point), then squeezed (more than twice as many points as frames - two points on every frame, the side remarks left out, never the first or last point), else fits (every point drawn, one or two to a frame).

The doodle shelf (icon ids): person, people, house, box, clock, coin, heart, bulb, gear, cloud, sun, drop, flame, leaf, cup, phone, book, bolt, magnifier, check, cross, question, star, target, chat, key, chart-up, chart-down, cycle, wave. Links between doodles: none, versus, so, plus, then.

The reply

One JSON object:

{"task": "board", "fit": "fits", "title": "...", "frames": [{"points": [1], "heading": "...", "doodles": [{"icon": "key", "label": "..."}], "link": "none", "caption": "..."}], "left_out": []}
FieldWhat it holds
fitthin, squeezed or fits, fixed by the point and frame counts.
frames[]In the notes' order: points (the point numbers drawn), heading (2-6 words), doodles (1-3 of {icon, label}, labels made only of that frame's words), link and caption (6-20 words).
left_outPoint numbers not drawn - only for a squeezed board.

The page reads the reply with ReplyKit.normalize and checks it with ReplyKit.check, which lists, under "The page's check", a wrong fit or frame count, a point repeated, skipped or out of order, a label word the frame's notes never say, a doodle off the shelf, and any number the notes never gave.

Worked examples - every sample

These are the free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover all three fits - a board that fits, a thin one and a squeezed one with side remarks left out.

How a lock and key work

Input:

{
 "task": "board",
 "topic": "How a lock and key work",
 "points": "1. Inside the lock are little pins in pairs.\n2. When there is no key, the pins block the cylinder from turning.\n3. The bumps on the key push each pair of pins up to just the right height.\n4. Then the gap between the pins lines up with the edge of the cylinder.\n5. So the cylinder can turn and the lock opens.",
 "point_count": "5",
 "frames": "4"
}

Reply:

{
 "task": "board",
 "fit": "fits",
 "title": "How a lock and key work",
 "frames": [
  {
   "points": [
    1
   ],
   "heading": "Pins inside the lock",
   "doodles": [
    {
     "icon": "gear",
     "label": "Pins in pairs"
    }
   ],
   "link": "none",
   "caption": "Inside the lock are little pins in pairs."
  },
  {
   "points": [
    2
   ],
   "heading": "Pins block the cylinder",
   "doodles": [
    {
     "icon": "cross",
     "label": "No key"
    },
    {
     "icon": "gear",
     "label": "Pins block cylinder"
    }
   ],
   "link": "so",
   "caption": "When there is no key, the pins block the cylinder from turning."
  },
  {
   "points": [
    3
   ],
   "heading": "Key bumps push the pins",
   "doodles": [
    {
     "icon": "key",
     "label": "Key bumps"
    },
    {
     "icon": "gear",
     "label": "Pins up"
    }
   ],
   "link": "so",
   "caption": "The bumps on the key push each pair of pins up to just the right height."
  },
  {
   "points": [
    4,
    5
   ],
   "heading": "Gap lines up, lock opens",
   "doodles": [
    {
     "icon": "check",
     "label": "Gap lines up"
    },
    {
     "icon": "key",
     "label": "Lock opens"
    }
   ],
   "link": "so",
   "caption": "Then the gap lines up with the edge of the cylinder, so the cylinder can turn and the lock opens."
  }
 ],
 "left_out": []
}

How a library hold works

Input:

{
 "task": "board",
 "topic": "How a library hold works",
 "points": "1. You reserve the book on the library website.\n2. When a copy comes back, they put it on the hold shelf with your name on a slip.\n3. You have a week to pick it up, or it goes to the next person waiting.",
 "point_count": "3",
 "frames": "5"
}

Reply:

{
 "task": "board",
 "fit": "thin",
 "title": "How a library hold works",
 "frames": [
  {
   "points": [
    1
   ],
   "heading": "Reserve the book",
   "doodles": [
    {
     "icon": "book",
     "label": "Book"
    }
   ],
   "link": "none",
   "caption": "You reserve the book on the library website."
  },
  {
   "points": [
    2
   ],
   "heading": "Copy goes on hold shelf",
   "doodles": [
    {
     "icon": "book",
     "label": "Copy"
    },
    {
     "icon": "box",
     "label": "Hold shelf"
    }
   ],
   "link": "so",
   "caption": "When a copy comes back, they put it on the hold shelf with your name on a slip."
  },
  {
   "points": [
    3
   ],
   "heading": "A week to pick up",
   "doodles": [
    {
     "icon": "clock",
     "label": "Week"
    },
    {
     "icon": "person",
     "label": "Next person"
    }
   ],
   "link": "versus",
   "caption": "You have a week to pick it up, or it goes to the next person waiting."
  }
 ],
 "left_out": []
}

How a vending machine checks a coin

Input:

{
 "task": "board",
 "topic": "How a vending machine checks a coin",
 "points": "1. You drop the coin in the slot and it rolls down a little ramp.\n2. A magnet near the ramp slows the coin down.\n3. Fake coins made of steel stick to the magnet or slow down too much.\n4. My cousin once tried a button and it just rattled straight back out.\n5. The machine measures how big and how heavy the coin is.\n6. Honestly I find this part oddly satisfying to listen to.\n7. If the coin passes, a little gate opens and it drops into the cash box.\n8. If it fails, it falls out at the return cup instead.\n9. Then the screen shows how much credit you have.",
 "point_count": "9",
 "frames": "3"
}

Reply:

{
 "task": "board",
 "fit": "squeezed",
 "title": "How a machine checks a coin",
 "frames": [
  {
   "points": [
    1,
    2
   ],
   "heading": "Coin rolls down and slows",
   "doodles": [
    {
     "icon": "coin",
     "label": "coin"
    },
    {
     "icon": "gear",
     "label": "magnet"
    }
   ],
   "link": "then",
   "caption": "You drop the coin in the slot and it rolls down a little ramp, then a magnet slows it down."
  },
  {
   "points": [
    5,
    7
   ],
   "heading": "Machine measures coin then gate opens",
   "doodles": [
    {
     "icon": "coin",
     "label": "coin"
    },
    {
     "icon": "box",
     "label": "cash box"
    }
   ],
   "link": "so",
   "caption": "The machine measures the coin, and if it passes, the gate opens and it drops into the cash box."
  },
  {
   "points": [
    8,
    9
   ],
   "heading": "Return cup and credit screen",
   "doodles": [
    {
     "icon": "cup",
     "label": "return cup"
    },
    {
     "icon": "phone",
     "label": "credit screen"
    }
   ],
   "link": "then",
   "caption": "If it fails, it falls out at the return cup, and then the screen shows how much credit you have."
  }
 ],
 "left_out": [
  3,
  4,
  6
 ]
}

Build the input yourself

Load the engine in node (global.window = global; require("./boardkit.js"); require("./replykit.js")), then:

const K = BoardKit, R = ReplyKit;
const built = K.buildInput(K.form({ topic: "How a lock and key work", notes: "Inside the lock are pins.\nThe key pushes them up.", frames: "3" }));  // built.ok is false, with built.note, until the notes are usable
const wire = K.toWire(built.input);   // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.check(reply, built).notes        // the page's re-check of the plan

Form keys: topic, notes (a line or sentence per point, up to twelve) and frames (3 to 6). The marker colour and hand edits are the page's business and are not sent.

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://dry-erase.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":"dry-erase"}' | 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": "board", "topic": "How a lock and key work", "points": "1. Inside the lock are little pins in pairs.\n2. The bumps on the key push the pins up.\n3. So the cylinder can turn.", "point_count": "3", "frames": "3"}
# - see the field table
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 dry-erase:<task>:<inputHash>:a<attempt> (the one task is board), 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="dry-erase:board:$(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: dry-erase:board:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json