← Recording to Notes
Recording to Notes API
Tokens

Drive Recording to Notes from your own code

The one metered run is available over HTTP. Task notes takes the transcript of a lecture or a meeting - ideally with a time at the start of each line - and returns structured notes: a summary, an outline, key points, definitions, decisions, action items (with owner and due date as said), review and open questions, and lines worth checking against the audio. Every item carries t, the time of the transcript line where it was said, so your app can link each point back to the recording.

The division of labour: the browser transcribes and scans; the model writes; the page re-checks the reply. Speech recognition runs either on the user's device (Whisper base in stt-whisper.js, windows cut at quiet points by audio.js), where the audio is never uploaded, or on SkillSafe with Whisper large-v3 turbo (stt-cloud.js, described under Transcribe audio). Transcript parsing (timed lines, SRT, WebVTT, plain text), the scan for task, definition and decision lines, the run input and the reply checks come from notekit.js, which runs in node as well. NoteKit.check snaps each cited time to the transcript line that contains it, unlinks times past the end of the recording, and names task-or-deadline lines that got no action item.

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":"recording-to-notes"}), 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 transcript 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 run 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 and the input

Every run sends task: "notes". The declared input schema carries scalars only, so every value is a string (NoteKit.toWire does this). The schema requires transcript.

FieldTypeMeaning
taskstringAlways notes.
formatstringlecture (outline, definitions, review questions, assignments) or meeting (agenda, decisions, action items with owners).
title, contextstringOptional. Context is a hint such as the course or the attendees; the model uses names only as they appear in the transcript.
notes_languagestringsame as transcript, or a language name such as Spanish.
timed, durationstringyes when each transcript line starts with [m:ss] or [h:mm:ss]; duration is the last line's time.
transcriptstringOne line per stretch of speech. Up to about 90,000 characters; the page cuts longer transcripts from the middle and says so in clip_note.
prescanstring (JSON)The page's scan: word and line counts, named speakers, and the task, definition, decision and emphasis lines with their times.
clip_note, retry_notestringWhat was cut, if anything; and, on the one reformat retry, why the first reply could not be read.

Transcribe audio on SkillSafe

A second, separate run turns audio into text with Whisper large-v3 turbo. Upload one audio file (WAV, MP3, M4A, WebM, OGG, FLAC or AAC, at most 10 MB) with POST /v1/app-api/files as multipart field file, then run with the reserved fields $model and $files. The reply's output.output is the transcript as plain text, without timestamps. It is billed per second of audio (about 6.2 credits a minute at this app's markup, rounded up to a whole credit per run); the run reserves a fixed hold of about 270 credits and releases the rest. The page cuts a recording into 16 kHz mono WAV parts of about a minute at quiet moments, runs each part on its own, deletes each upload afterwards, and stamps every part's first line with the part's start time.

curl -s -X POST https://api.skillsafe.ai/v1/app-api/files -H "Authorization: Bearer YOUR_TOKEN" \
  -F "file=@part-001.wav;type=audio/wav"                     # -> data.file.file_id
curl -s -X POST https://api.skillsafe.ai/v1/app-api/run -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" -H "Idempotency-Key: my-recording-part-001" \
  -d '{"$model":"whisper","$files":["FILE_ID"],"task":"transcribe"}'
# -> {"status":"succeeded","charged_credits":2,"output":{"output":"Okay everyone, today we cover ..."}}

The estimate for this body warns missing required field 'transcript'; that warning belongs to the notes contract and does not apply here. Then send the stitched transcript as task notes.

The reply

One JSON object: task, format, title, summary, outline [{t, heading, points}], key_points [{t, text}], definitions [{t, term, meaning}], decisions [{t, text}], action_items [{t, task, owner, due}], questions [{t, text, kind}] (kind review or open), unclear [{t, text}]. t is a time string such as "14:05", or null for an untimed transcript. Parse with NoteKit.parseReply, then normalize, then check(notes, transcript, prescan).

Worked examples

notes - economics lecture (lecture format)

Input:

{
 "task": "notes",
 "format": "lecture",
 "title": "Intro Micro - Week 4: Price elasticity",
 "context": "ECON 101",
 "notes_language": "same as transcript",
 "timed": "yes",
 "duration": "6:42",
 "transcript": "[0:00] Okay, let's get started. Last week we drew demand curves and I promised that today we would ask how much quantity actually moves when price moves. That is the whole topic for today, price elasticity of demand.\n[0:27] So, price elasticity of demand is defined as the percentage change in quantity demanded divided by the percentage change in price. Percentage, not units. If you use units, your answer depends on whether you measure coffee in cups or in gallons, and that is useless.\n[0:55] Because demand slopes down, the number comes out negative. Economists are lazy, so we usually drop the minus sign and talk about the absolute value. When I say elasticity is two, I mean minus two.\n... 11 more lines",
 "prescan": "{\"words\":611,\"segments\":14,\"speakers\":[],\"commitment_lines\":[\"[5:06] There is also the midpoint method, which you will use in the problem set.\",\"[5:34] For next...",
 "clip_note": ""
}

Reply (from the stand-in model, unedited, lists shortened here):

{
 "task": "notes",
 "format": "lecture",
 "title": "Intro Micro - Week 4: Price elasticity",
 "summary": "This lecture introduces price elasticity of demand: its formula, the elastic/inelastic/unit elastic cases, and what determines it (substitutes, necessity vs. luxury, budget share, time). It explains why elasticity differs from slope along a straight-line demand curve, and connects elasticity to total revenue using a transit fare example. The midpoint method for elasticity calculations is introduced ahead of the problem set. The lecture closes with reading and problem set assignments and previews next class on cross price and income elasticity.",
 "outline": [
  {
   "t": "0:00",
   "heading": "Introduction",
   "points": [
    "Recaps last week's demand curves.",
    "Today's topic: price elasticity of demand - how much quantity moves when price moves."
   ]
  },
  {
   "t": "0:27",
   "heading": "Defining price elasticity of demand",
   "points": [
    "Elasticity = percentage change in quantity demanded divided by percentage change in price.",
    "Must use percentages, not units, so the measure doesn't depend on units like cups vs. gallons.",
    "Demand sloping down makes the value negative; economists usually drop the minus sign and use the absolute value."
   ]
  },
  "... 9 more"
 ],
 "key_points": [
  {
   "t": "0:27",
   "text": "Price elasticity of demand = % change in quantity demanded \u00f7 % change in price."
  },
  {
   "t": "0:55",
   "text": "Economists drop the minus sign and talk about elasticity in absolute value terms."
  },
  "... 10 more"
 ],
 "definitions": [
  {
   "t": "0:27",
   "term": "price elasticity of demand",
   "meaning": "Percentage change in quantity demanded divided by percentage change in price."
  },
  {
   "t": "1:22",
   "term": "elastic",
   "meaning": "Absolute value of elasticity greater than one; quantity responds more than proportionally to price."
  },
  "... 4 more"
 ],
 "decisions": [],
 "action_items": [
  {
   "t": "1:22",
   "task": "Know the three elasticity cases (elastic >1, inelastic <1, unit elastic =1) - noted for the exam.",
   "owner": "",
   "due": ""
  },
  {
   "t": "5:06",
   "task": "Watch the course site video on the midpoint method if unclear.",
   "owner": "",
   "due": ""
  },
  "... 3 more"
 ],
 "questions": [
  {
   "t": "0:27",
   "text": "How is price elasticity of demand calculated?",
   "kind": "review"
  },
  {
   "t": "1:22",
   "text": "What are the three elasticity categories and their numeric cutoffs?",
   "kind": "review"
  },
  "... 5 more"
 ],
 "unclear": []
}

notes - team meeting (meeting format)

Input:

{
 "task": "notes",
 "format": "meeting",
 "title": "Mobile app weekly sync",
 "context": "Attendees: Priya (PM), Marcus (iOS), Lena (design), Tom (QA)",
 "notes_language": "same as transcript",
 "timed": "yes",
 "duration": "6:17",
 "transcript": "[0:00] Priya: Okay, I think everyone is here. Three things today. The release date for 4.2, the onboarding redesign, and the crash on older Android phones. Marcus, do you want to start with where we are on 4.2?\n[0:26] Marcus: Sure. All the features are merged. We have two blockers left. The payment sheet sometimes double submits when you tap fast, and the dark mode colors on the settings screen are wrong. I think the payment one is maybe a day of work.\n[0:53] Tom: From QA side we ran the full regression yesterday. Thirty one test cases failed out of four hundred and twelve, but twenty six of those are the same settings screen issue. So really it is about six distinct bugs.\n... 11 more lines",
 "prescan": "{\"words\":585,\"segments\":14,\"speakers\":[\"Priya\",\"Marcus\",\"Lena\"],\"commitment_lines\":[\"[1:20] Priya: Okay, so we decided, we are going with the twelfth, and if th...",
 "clip_note": ""
}

Reply (from the stand-in model, unedited, lists shortened here):

{
 "task": "notes",
 "format": "meeting",
 "title": "Mobile app weekly sync",
 "summary": "Weekly mobile sync covering the 4.2 release, the onboarding redesign, and an Android crash. The team decided to release 4.2 on the 12th, with a Wednesday checkpoint on the payment double-submit fix that could push the date to the 19th. The 3-screen onboarding flow with just-in-time location permissions was approved but deferred to 4.3 instead of 4.2. The Android gallery crash needs more data before a decision on raising the minimum SDK, so Tom will pull device numbers by Monday; the accessibility audit timing was also left open for next week's agenda.",
 "outline": [
  {
   "t": "0:00",
   "heading": "Agenda",
   "points": [
    "Three topics: the 4.2 release date, the onboarding redesign, and the crash on older Android phones."
   ]
  },
  {
   "t": "0:26",
   "heading": "4.2 release status",
   "points": [
    "All features merged; two blockers left: payment sheet double-submits on fast taps, and dark mode colors wrong on the settings screen.",
    "QA regression: 31 of 412 test cases failed, 26 of those tied to the same settings screen issue, so about six distinct bugs.",
    "Decided: release on the 12th; if the payment fix isn't merged by Wednesday, push to the 19th.",
    "Marcus takes the double-submit bug himself; Tom reruns the payment suite once it merges."
   ]
  },
  "... 4 more"
 ],
 "key_points": [
  {
   "t": "0:26",
   "text": "Two blockers remain for 4.2: payment sheet double-submits on fast taps, and wrong dark mode colors on the settings screen."
  },
  {
   "t": "0:26",
   "text": "The payment double-submit fix is estimated at about a day of work."
  },
  "... 8 more"
 ],
 "definitions": [
  {
   "t": "2:44",
   "term": "Just in time permission",
   "meaning": "Asking for a permission, such as location, only when the user first reaches the feature that needs it (e.g., opening the map), instead of upfront during onboarding."
  }
 ],
 "decisions": [
  {
   "t": "1:20",
   "text": "Release 4.2 on the 12th; if the payment fix is not merged by Wednesday, push the release to the 19th."
  },
  {
   "t": "3:12",
   "text": "Go with the 3-screen onboarding flow and just-in-time permissions, but ship it in 4.3, not 4.2."
  }
 ],
 "action_items": [
  {
   "t": "1:49",
   "task": "Fix the payment double-submit bug and have a pull request up.",
   "owner": "Marcus",
   "due": "tomorrow end of day"
  },
  {
   "t": "1:49",
   "task": "Rerun the payment test suite once the fix is merged.",
   "owner": "Tom",
   "due": "same day the fix merges"
  },
  "... 3 more"
 ],
 "questions": [
  {
   "t": "4:33",
   "text": "How many users would be cut off if the minimum SDK is raised?",
   "kind": "open"
  },
  {
   "t": "5:24",
   "text": "Will the accessibility audit happen this quarter or next quarter?",
   "kind": "open"
  }
 ],
 "unclear": []
}

Build the input yourself

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

const K = window.NoteKit;
const tr = K.parseTranscript(fs.readFileSync("lecture.srt", "utf8"));   // timed lines, SRT, WebVTT or plain text
const scan = K.prescan(tr);
const input = K.toWire(K.buildInput({ format: "lecture", title: "Week 4" }, tr, scan).input);
// ... run it (steps 4-6), then:
const notes = K.normalize(K.parseReply(job.output.output));
console.log(K.check(notes, tr, scan).notes, K.toMarkdown({ notes, title: "Week 4" }));

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://recording-to-notes.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":"recording-to-notes"}' | 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": "notes", "format": "lecture", "title": "Week 4", "timed": "yes", "transcript": "[0:00] Okay, let's get started...\n[0:27] ...", "prescan": "{...}", ...}
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 task and the input: a retry with the same key never bills twice. The page uses recording-to-notes:<task>:<inputHash>:a<attempt> (task is always notes), 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="recording-to-notes:notes:$(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: recording-to-notes:notes:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json