Chalkmind

Chalkmind API

Chalkmind rents out an AI teacher that teaches live on a board. While it talks, it writes, draws, points and erases, and every screen watching sees the same board at the same moment. There is one teacher, the AI with its teaching skill. Each customer (a tenant) makes its own teachers from it, for whatever it teaches: a school class, the stock market, spoken English, a company's product. Everything is done through this API, with the tenant's key.

The base address is https://HOST/v1. Every route also works without /v1. Bodies are JSON, and a request body is at most 1 MB; a book sent as a PDF may be up to 80 MB.

Keys: who may do what

Send the key as Authorization: Bearer cm_…. A key is shown once, when it is made; only its hash is kept.

KeyMay
Teaching keyUse its channels: start lessons, teach, make viewer tokens. Read its tenant's teachers, books, lessons and use
Manage key ("manage": true)Everything a teaching key may do, plus manage its own tenant: make and revoke keys, make, change and delete teachers and their books, list and forget learners
Library key ("library": true)Add items to the shared library, and textbooks to the school class templates
Owner key ("admin": true)Make tenants, set their plans, pause them, and see every tenant's keys and use

Quick start

KEY=cm_…   # a manage key of your tenant; your channels are acme-*

# 1. a teacher of your own
curl -X POST https://HOST/v1/teachers -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{
  "id": "stock-market-basics", "name": "Stock Market Basics", "names": {"gu": "શેરબજારની પાયાની વાતો"},
  "subject": "the stock market and trading", "level": "adults who have never traded", "lang": "gu",
  "style": "Calm and practical: every idea with a chart and a small worked example in rupees.",
  "library": ["economics", "financial_markets", "share_types"]
}'

# 2. its book, chapter by chapter (or a whole PDF: Content-Type: application/pdf)
curl -X POST 'https://HOST/v1/teachers/stock-market-basics/book?lang=gu' -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' -d '{"chapter": 1, "title": "શેર એટલે શું?", "text": "…"}'

# 3. a link for the students' screens (private lessons need one)
curl -X POST https://HOST/v1/tokens -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"channel": "acme-batch1", "minutes": 240}'
# → {"token": "v1.…", "display": "/board/acme-batch1/display?t=v1.…"}

# 4. the lesson: the board's own teacher teaches it, as your teacher
curl -X POST https://HOST/v1/board/acme-batch1/command -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"text": "Teach what a share is and how its price moves", "teacher": "stock-market-basics", "fresh": true}'

Then put the display link in your app:

<iframe src="https://HOST/board/acme-batch1/display?t=v1.…" allow="autoplay" style="aspect-ratio:100/64;width:100%;border:0"></iframe>

A Flutter app uses the liveboard_display package instead: BoardView(channelUrl: 'https://HOST/board/acme-batch1', token: 'v1.…').

Your own AI can teach instead of the board's: connect it to the MCP server (see MCP).

Teachers

A teacher is the AI made into the teacher of one subject. It is told who it is, whom it teaches and how, and it gets its book and its part of the library. Every lesson named for it is taught that way.

FieldWhat
id2 to 60 characters: small letters, digits and - (stock-market-basics). std…- ids are the templates'
name / namesIts name, and its name in other languages: {"gu": "…", "hi": "…"}
subjectWhat it teaches (required)
levelWhom it teaches, at what level: "beginners", "Std 10 (GSEB)"
langThe language of its lessons unless a lesson names one: gu, en, hi or sa
styleHow it teaches: its manner, its way of explaining, what it always or never does
notesAnything else it should know: what its learners find hard, the words to use
libraryLibrary categories (economics) and item names (demand_curve) that are its own
std, boardFor a school teacher: its standard (its class's library by chapter) and exam board (GSEB by default)
recordtrue: its lessons are recorded (a video of the board and the voice) unless a lesson says otherwise
fromWhen making it: a template to start from (std10-science)

A teacher that is not a school class's is also told this: it teaches and does not advise. It never tells a learner what to buy, sell or take for their own money, health or legal matters, and it promises no results.

RouteWhat
GET /teachersYour teachers
POST /teachersA new one → 201 {teacher}; 409 if the id is taken
GET /teachers/:id?lang=guOne teacher, or a template: its book's chapters, its library, and the exact instructions its AI gets
PUT /teachers/:idThe teacher whole, made or replaced
PATCH /teachers/:idOnly the fields sent; null removes a field
DELETE /teachers/:idThe teacher and its book
GET /teachers/:id/book?chapter=N or ?search=WORDIts book: a chapter whole, or the passages with a word
POST /teachers/:id/book?lang=guA chapter {chapter, title, text}, or a whole book as application/pdf. The PDF is cut at its chapter headings: "Chapter 3", "Unit 3", "પ્રકરણ 3"
DELETE /teachers/:id/book?lang=gu[&chapter=N]A chapter, or the whole book in that language
GET /templatesThe school classes the library is made for: std10-science, std12-accountancy …
GET /voices?lang=guThe voices a teacher may have ("voice": {"gu": "ID"}): each one's id, gender, age, languages, style and the DamruVani plan it needs; and the board's own voice for each language

A teacher's "scope": "book" keeps it strictly to its book and the lesson's chapter; a question outside the book is never answered, gets one fixed line, and is flagged off_topic (see TEACHERS.md).

Lessons

A lesson runs on a channel, and there is one lesson per channel at a time. All of these routes need a key for the channel.

RouteWhat
POST /board/:ch/command{text, teacher?, lang?, fresh?, learner?, chapter?, theme?, pace?, voice?, mcqGap?} (mcqGap: the least minutes between two MCQs, 1 to 15, for room for the class's hands) ("voice": false: captions only, no voice; also on /session and /teach): the board's own teacher does it. On the board as it is, or on a fresh board with fresh: true. It remembers the instructions before ("now draw it bigger")
POST /board/:ch/teach{problem, teacher?, lang?}: the board's own teacher teaches a problem on a fresh board
POST /board/:ch/session{title?, lang?, learner?, teacher?, chapter?}: a fresh board for your own AI, which then sends lines
POST /board/:ch/linesBoard language lines (text/plain): your own AI writing (see AI_GUIDE.md)
GET /board/:ch/stateWhat is on the board, its ids, the free room, and how far behind the voice it is
GET /board/:ch/lookA picture (PNG) of the board once everything sent is drawn
POST /board/:ch/hold, /resume, /endPause and go on, on every screen; end the lesson (it is kept)
POST /board/:ch/remember{concept, status, note?, next?}: how the learner did, for the next lesson
GET /board/:ch/classWho is in class now (each student's id, name, since when), and what each did in this lesson: answers right and wrong, questions asked
POST /board/:ch/record{on}: the record button. Recording starts or stops now, every screen shows REC, and the video is made when the lesson ends
GET /lessons, /lessons/:id, /lessons/:id/filmYour tenant's lessons: their teacher, learner, lines, problems, cost, film and recording state. One lesson has its own usage: AI tokens (in, out, cache read and written) and speech (characters, clips); and its voice, to find where it went quiet: {sentences, withClip, withoutClip, waitMs: {p50, p95, max}, captionsOnly: [{from, to}], list: [{t, waitMs, clip}]}: for each spoken sentence its lesson time t (s), how long the board waited for its voice clip (ms, 0 when it was ready) and whether it had one; captionsOnly are the spans of lesson time when the voice was failing and the board showed captions only. The latest 2,000 sentences are kept; voice is null for a lesson that ended before this was kept
GET /lessons/:id/classEvery student who took part: when they joined and left, answers right and wrong, questions asked (typed or spoken)
GET /lessons/:id/recordingThe lesson's video (MP4, byte ranges), or {recording: {state}} while it is made
POST /lessons/:id/recordingRecord an ended lesson after all, while its folder is kept (--keep-days)
DELETE /lessons/:id/recordingDelete the video (a manage key), once you have your copy: its recording.state is then deleted (not failed)
GET /teachers/:id/journal?learner=ID[&search=WORD]The teacher's journal with a learner: its lessons, newest first, each with its steps, the library it used, its questions and what was noted
GET /teachers/:id/plan?learner=IDThe teacher's year plan with a class: the chapters in order, each taught or not (in how many lessons, when), what is next, and any past its date
PUT /teachers/:id/plan?learner=ID{chapters: [{n, title?, by?: "YYYY-MM-DD"}]} in the order they are taught (a manage key). Without learner, the plan for every class of the teacher. DELETE removes it

During the lesson, board_journal search=WORD finds when and how it taught something. Each teacher has its own journal and notes: the English teacher's lessons are not in the science teacher's memory.

A live class of the board's own teacher

What a class taught by the board's own teacher (/command) does with its students. Shikhar's app and the board's own buttons take the same path: POST /board/:ch/react?t=… and /board/:ch/doubt?t=… with who (the same id as the screen's ?learner=) and name.

RouteWhat
POST /board/:ch/controls{hand?, question?, again?}: what the class may use now (a key for the channel) → {set, now}
POST /board/:ch/minutes{minutes}: a live class made longer or shorter (its whole length, 5 to 240, more than it has run): its wrap-up and its end are put again, and the teacher hears the new time → {minutes, left}
GET /points?class=&learner=&lesson=&from=&to= (lesson: one lesson's points): each student's points (their latest answer to each MCQ), {students: [{learner, name, points, right, answered, lessons}], total}

Screens

RouteWhat
POST /tokens{channel, minutes? or until?, console?, learner?, screens?, mark?}. One student's token (learner, the same id as the screen's): the screen is that student whatever it says, and answers, hands and questions sent with it are theirs; with screens (1 to 5) it opens on that many screens at once, a further one closes the oldest (it shows «this class was opened on another screen») and your server hears screen.shared; mark (40 characters: a name, the end of a phone number) is a faint watermark moving over the board, which the link cannot drop. The board itself cannot be saved as a picture (no long-press or right-click menu). Otherwise: a viewer token for one channel, or for a prefix (acme-batch1-*). It lasts minutes (240 by default) or until a time (an ISO time with its zone, such as the class's end: "2026-11-07T11:00:00+05:30"), 90 days at most. A screen open when it ends is told (event: expired) and closed, and the token lets nothing in after it. It also ends when the key that made it is revoked. A console token also shows the AI's lines and problems
GET /board/:ch/display?t=TOKENThe board for a browser, an <iframe> (allowed by Content-Security-Policy: frame-ancestors: this site, https://shikhar.trinetraailabs.com and any https page; LIVEBOARD_FRAME_ANCESTORS changes it) or a classroom TV, with the students' question box under it. The other options: ?name=NAME (the student's name, which the teacher says), ?learner=ID, ?chat=0 (no question box, for a classroom TV), ?mute, ?controls=0, ?answers=0, ?teacher=0 (no teacher card), ?teacher=big (the whole figure of the built-in teacher beside the board on a screen wider than 8:5, with its voice and mouth moving; narrower or upright screens, and a teacher with a picture of its own, keep the card)
POST /board/:ch/doubt?t=TOKEN{text, who?, name?}: a student's question, from a screen of your own. → {id, state}
GET /board/:ch/doubts[?take=1]The questions waiting, for an AI that teaches from elsewhere. take=1 marks them heard. This needs a key for the channel
GET /board/:ch/events?t=TOKENThe board as Server-Sent Events, for a display of your own (a doubt event says a question waits or was heard). A student's screen gets an open MCQ without its answer, and nothing timed after it (the answer written and said) until it closes; then reveal {ask, answer}, and the rest. GET /board/:ch/film is the same for a screen. A console (a key) gets everything at once

Your account

RouteWhat
GET /meYour tenant, plan, minutes used this month, and what this key may do
GET /keysYour tenant's keys: their id, first characters, label, channels and rights (never the keys themselves)
POST /keys{label?, channels?, library?, manage?} → 201 {key, id}. The key is shown only in this answer
DELETE /keys/:idRevokes a key, and every viewer token it made
GET /usage?from=YYYY-MM-DD&to=YYYY-MM-DDLessons, minutes, AI tokens and speech, by day. This month by default
GET /learnersYour learners, with how many notes each has
GET /learners/:idWhat the teacher noted about one learner
GET /learners/:id?history=1A student's lessons, newest first: how they did in each (answers right and wrong, questions asked, spoken), with totals
GET /learners/:id?students=1[&teacher=ID]A class (the learner its lessons are taught to): each student who took part, with their lessons, answers right and wrong, their latest answers, questions asked, and what is shaky
DELETE /learners/:idForgets a learner and every note about them
GET /exportEverything of your tenant's, as one JSON file (a manage key): see below

Homework

The teacher sets homework at the end of a full lesson (or you set it): up to 10 questions. Each question is one of three kinds:

Students answer on their screens, or through your app. Choices, and a short answer that is the expected one, are marked at once. The rest are marked by the AI, which reads the photo and writes one kind sentence on each question. Each student's marks become their notes, and the next lesson's teacher hears how the class did.

RouteWhat
POST /homework{class, teacher?, channel?, title, lang?, questions, due? | due_days?} → 201 (a manage key). With channel, the screens there show it at once
GET /homework?class=&teacher=&channel=Your homework, newest first, with how many handed it in, how many are marked, and the average
GET /homework/:idIts questions (with the answers) and every student's answers, marks and feedback
POST /homework/:id/answers{learner, name?, answers: [{choice} | {text}], image?}: a student's answers from your app → {state: checked | checking, score?}
GET /homework/:id/photo?learner=ID[&n=2]A photo of a student's notebook or answer sheet
GET /homework/:id/paper[?key=1]It as a paper to print; with key=1, the answers under each question (for the teacher)
DELETE /homework/:idDeletes it and its answers (a manage key)
POST /board/:ch/homework{title, questions, due_days?, kind?, minutes?}: your own AI sets the lesson's class homework or test (the MCP tools board_homework, board_test)

Tests. A test is homework with "kind": "test", in the pattern of the class's exam board:

Questions flagged for the school

A question is flagged when it is not about the lesson, is not kind, or is personal (about the teacher, or a student's private life). The teacher answers it with one kind sentence, goes on with the lesson, and flags it for you.

A student who may need help gets one warm sentence. That is a student who is very sad, hurt, unsafe or bullied, or speaks of harming themselves. The teacher tells them they are heard, and to talk to their parents or a teacher they trust today. It never discusses it before the class. The question is flagged concern, and your server hears it at once (question.flagged, urgent: true). An AI teaching over MCP flags with board_flag, or POST /board/:ch/flag {question, kind, note?}.

Reports

A class's days, or a student's, from the lessons, the homework and tests, and the teachers' notes. A report covers the last 7 days by default, or from and to (dates in India's time, at most 93 days). Its summary is in the lessons' language, or lang.

RouteWhat
GET /reports/class/:id?from=&to=&teacher=&lang=A class (the learner its lessons are taught to): its lessons, minutes and chapters, attendance, answers right and wrong, homework and tests, what most students found shaky, each student's line, and who needs attention or is doing well; with its summary
GET /reports/student/:id?from=&to=&lang=A student: the lessons of their classes they were in and missed, their answers, homework and test marks, what they understood and should practise; with its summary
GET /reports/student/:id?format=htmlThe student's report as a page for parents
GET /flags?class=&learner=&lesson=&kind=&from=&to=Students' questions the teachers flagged, newest first: the student, the question, its kind and a note
GET /reports/classes?from=&to=The classes taught in the period (the last 7 days by default): their ids, to read their reports
POST /reports/weekly {week?}The owner: send a week's reports now (week is its Monday)

GET /flags, GET /reports/classes and the two report routes above take ?tenant=ID: another customer's flags and reports, for the owner's key (or admin login) only (403 for any other).

Every Sunday from 18:00 (India), each class taught that week has its report sent once to its tenant's server, with each student's (report.weekly).

Webhooks

Chalkmind tells your server when something happens, so it need not ask. These routes need a manage key.

RouteWhat
POST /webhooks{url, events?}: a webhook (at most 10). Its secret is shown only in this answer
GET /webhooksYour webhooks, and the events there are
PATCH /webhooks/:id{events}: what it hears, changed; its url and secret stay the same
DELETE /webhooks/:idStops one
EventWhen, and its data
lesson.endingA class with minutes has 3 minutes left; its teacher is wrapping up: lesson, channel, teacher, learner, minutes, minutes_left
lesson.endedA lesson ended: lesson, channel, title, teacher, learner, duration_s, started_at, recording; "reason" when the board ended it: "time" at the end of its minutes, "principal" when the Principal could not get it going, "teacher" when the board's own teacher ended it after its goodbye (board_end_class; the last sentence is said first, then every screen shows the class has ended and the recording is made)
recording.readyIts video is made: lesson, channel, title, teacher, learner, started_at, duration_s, seconds, bytes, url. Copy it into your own storage from url, then DELETE it here (see HOSTS.md)
recording.failedIts video could not be made: lesson, error, and the same details as recording.ready (channel, title, teacher, learner, started_at, duration_s). The lesson's recording.state is failed; POST /lessons/:id/recording tries again while its folder is kept
learner.notedThe teacher noted a learner, or a student's answer was noted: learner, lesson, concept, status, note
question.askedA student asked, typed, spoken or with a photo: lesson, channel, learner, name, text, voice, photo
homework.setHomework was set: homework, class, teacher, lesson, channel, title, due, questions
homework.checkedA student's homework is marked: homework, class, learner, name, score, outOf, marks
question.flaggedThe teacher flagged a student's question: id, lesson, cls, learner, name, question, kind (off_topic, unkind, personal, concern), note, channel; a concern with urgent: true
class.startedA lesson started on one of your channels: lesson, channel, teacher, learner, minutes (when it has a length)
class.stalledThe Principal saw a class stall (nothing on the board for 30 s) and is taking steps: the incident (below)
class.recoveredA stalled class goes on again, or the voice or the students came back: the incident, closed, outcome
class.errorThe voice is failing (captions only), half the students or more dropped at once, or the Principal's steps did not get a stalled class going (outcome: "gave_up"): the incident
class.endedA lesson ended: lesson, channel, teacher, learner, duration_s, reason (host, time, principal or teacher), and points: each student's {learner, name, points, right, answered} in it
screen.sharedA student's one-screen token was opened on a further screen (the older one was closed): channel, lesson, learner, screens, at. /principal/status counts them per class (shared)
class.movedReserved for the timetable (the Principal's step 5): not sent yet
report.weeklySunday evening, for each class taught that week: class, from, to, lang, summary, report, and students (each one's learner, name, summary, report)

The Principal

The Principal watches every live class in the board server (every 5 s while a class is live) and takes safe steps when one goes wrong, one at a time, each kept with its reason (docs/SCHOOL.md §5, §6). It never deletes anything, changes keys or billing, restarts the server, or says more to the class than one fixed line.

ProblemWhat it does
Stalled: nothing on the board for 30 s, outside an MCQ and a hold by you. A class with minutes (also when held after its AI failed), or any lesson while the board's teacher is at workThe board's own teacher: nudge (go on with the next step), then replan (stop it, start the step again, shorter), then hold (one fixed line, «One minute, please: we go on in a moment», a 20 s hold, then on), then end the class cleanly. 30 s between steps. Another AI (MCP): you are told, nothing is done to the class
The voice failing (captions only, which the board does by itself)You are told
Half the students or more gone at once (four or more before)You are told
A hand up 60 s with no turnThe teacher is reminded, once a minute
Many "explain again" (three, or a third of the class)The teacher goes over it again, more slowly

An incident is {incident, kind, severity, lesson, channel, opened, closed?, outcome?, steps, snapshot}: kind is stalled, voice, dropped, hand or again; outcome is recovered, ended or gave_up; snapshot has counts only, never a student's words.

RouteWhat
GET /principal/statusEvery live class of yours now: class (its channel), lesson, teacher, state (teaching, waiting, stalled, held), quiet_s, voice, students, left_min, and its open incidents. The owner's key sees every tenant's, and the server (database writes, memory, screens, the Principal's tick)
GET /principal/incidents?from=&to=&class=Incidents, newest first (those still open first)
POST /principal/act{class, step} with a manage key: a step by hand. nudge, replan, remind_hand and again_slowly need the board's own teacher (409 for another AI's class); hold, resume and end work on any class. A class ended so refuses more lines (409)

Logging in with Google

/login has "Sign in with Google" (GET /auth/google?next=/admin). Google sends the browser back to /auth/google/callback, where the board checks the one-time state and the ID token (issuer, this app, not expired, a verified email). An email named in LIVEBOARD_ADMIN_EMAILS comes in as the owner's admin (its user made if need be) and goes to /admin; anyone else only to an account that exists, at /account. The server's env needs GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET and LIVEBOARD_ADMIN_EMAILS (deploy/env.example); the redirect URI set in Google's console is https://DOMAIN/auth/google/callback.

The owner

These routes need an owner key.

RouteWhat
GET /tenantsEvery tenant, its plan, and its minutes this month
POST /tenants{id, name, plan?, minutesPerMonth?, channels?} → 201 {tenant, key}. The tenant's first key manages it; its channels are ID-* by default. Give that key to the customer
GET /tenants/:idOne tenant: its plan, its use this month, and how many keys and teachers it has
PATCH /tenants/:id{name?, active?, plan?, billing?, minutesPerMonth?, maxLive?, maxTeachers?}. null is no limit: maxLive is how many classes may be live at once, maxTeachers how many teachers it may make
GET /usage?tenant=IDOne tenant's use, or every tenant's without tenant
GET /export?tenant=IDOne tenant's export
DELETE /tenants/:id?confirm=IDDeletes a tenant and everything of its own, for good: see below

MCP

Any MCP client can teach on the board with the same key: Claude, claude.ai, Claude Code, or an agent platform.

The tools:

The tools are described in MCP.md.

Errors and limits

Every error is {"error": "what is wrong"}.

StatusMeaning
400Something in the request is wrong; the message says what
401No key, or a key that may not use this channel
402The account is paused, or the plan's minutes are used up
403The key may not do this (it is not a manage or owner key, or the thing is not yours)
404No such thing, or not yours
409Taken already, or the channel's teacher is busy with an instruction
413The body is too large
429Too many: instructions to the AI an hour (120 by default), pictures a minute, or screens on a channel (500). Retry after the seconds in Retry-After