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.
| Key | May |
|---|---|
| Teaching key | Use 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 |
- Channels. A key's
channelssay where it may teach: names (acme-10a) or prefixes (acme-*). A lesson runs on a channel, and the screens of that channel show it. - Keys made from keys. A key makes keys only within its own rights: the same tenant, channels inside its own, and library rights only if it has them.
- The owner's first key comes from
bin/keys.ts add --tenant owner --channels "*" --adminon the server. After that, everything goes through the API.
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.
| Field | What |
|---|---|
id | 2 to 60 characters: small letters, digits and - (stock-market-basics). std…- ids are the templates' |
name / names | Its name, and its name in other languages: {"gu": "…", "hi": "…"} |
subject | What it teaches (required) |
level | Whom it teaches, at what level: "beginners", "Std 10 (GSEB)" |
lang | The language of its lessons unless a lesson names one: gu, en, hi or sa |
style | How it teaches: its manner, its way of explaining, what it always or never does |
notes | Anything else it should know: what its learners find hard, the words to use |
library | Library categories (economics) and item names (demand_curve) that are its own |
std, board | For a school teacher: its standard (its class's library by chapter) and exam board (GSEB by default) |
record | true: its lessons are recorded (a video of the board and the voice) unless a lesson says otherwise |
from | When 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.
| Route | What |
|---|---|
GET /teachers | Your teachers |
POST /teachers | A new one → 201 {teacher}; 409 if the id is taken |
GET /teachers/:id?lang=gu | One teacher, or a template: its book's chapters, its library, and the exact instructions its AI gets |
PUT /teachers/:id | The teacher whole, made or replaced |
PATCH /teachers/:id | Only the fields sent; null removes a field |
DELETE /teachers/:id | The teacher and its book |
GET /teachers/:id/book?chapter=N or ?search=WORD | Its book: a chapter whole, or the passages with a word |
POST /teachers/:id/book?lang=gu | A 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 /templates | The school classes the library is made for: std10-science, std12-accountancy … |
GET /voices?lang=gu | The 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).
- Templates. A lesson may name a template as it is (
"teacher": "std10-science"). A teacher madefromone has that class's library, and its shared textbook until it has a book of its own. - Your own library. Add stamps, diagrams, formulas and lesson patterns that only your lessons use (a candlestick chart, your lab's set-up):
POST /library/itemswith a manage key, andDELETE /library/KIND/NAMEto remove one. Each is drawn and checked first. In your lessons, searches and teachers, your names win over the shared ones. The item's form is in library/README.md. - The library has 1,993 ready drawings: stamps, labelled diagrams, formulas and lesson patterns. They cover Std 10 to 12 in science, commerce and arts.
GET /librarylists the categories, andGET /library/search?q=WORDfinds items.
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.
| Route | What |
|---|---|
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/lines | Board language lines (text/plain): your own AI writing (see AI_GUIDE.md) |
GET /board/:ch/state | What is on the board, its ids, the free room, and how far behind the voice it is |
GET /board/:ch/look | A picture (PNG) of the board once everything sent is drawn |
POST /board/:ch/hold, /resume, /end | Pause 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/class | Who 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/film | Your 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/class | Every student who took part: when they joined and left, answers right and wrong, questions asked (typed or spoken) |
GET /lessons/:id/recording | The lesson's video (MP4, byte ranges), or {recording: {state}} while it is made |
POST /lessons/:id/recording | Record an ended lesson after all, while its folder is kept (--keep-days) |
DELETE /lessons/:id/recording | Delete 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=ID | The 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 |
- Learners.
learneris your own student id, or a class's (class-10a). Without it, the learner is the class on the channel. The next lesson for the same learner starts from what the last one found. - The teacher's memory. A lesson taught as a teacher (
teacher) goes into that teacher's journal with the learner. Each lesson's next start tells the teacher four things:- how many lessons it has taught them;
- the latest ones: when, what, with which pictures, how many questions, and how the class did;
- what it taught more than once;
- what the last lesson left out.
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.
- The year plan. The chapters in order come from your plan for the class, else your plan for every class of the teacher, else the order of the teacher's book. A chapter counts as taught when a lesson is that chapter: you say so when you start it (
"chapter": 12), or the teacher reads it from its book. At the start of each lesson the teacher hears the plan, how far the class is, what is next, any chapter past its date, and what this lesson is. - The class in the room. A student's screen says who it is:
?learner=(your id for the student) and?name=(the name the teacher says).- Who is in class. The teacher hears who is in class at the start, and who joins or leaves after each step.
- Answers by name. It hears who answered each question with what, so it praises right answers and corrects wrong ones by name.
- Each student's record. At the start, and as students join, it hears how each has done in the class's lessons with it ("Ravi: 3 of the last 4 wrong; shaky: fractions"), those who need help first. It gives them a question they can get right, and never says a record aloud.
- Questions by name. A student's question, typed or spoken, comes with the student's name, and the teacher answers them by name.
- The record. All of it is kept:
GET /lessons/:id/classfor a lesson, andGET /learners/:id?history=1for a student over all lessons.
- The class's length.
"minutes": 40on/session,/commandor/teach(or the teacher's ownminutes).- The time left. The teacher hears it after each step, and wraps up in the last 3 minutes (a recap and the close, nothing new). Your server hears
lesson.endingthen. - Content done early. It practises with the class instead of stopping.
- A hard stop at the end. At its end the board stops the class: the teacher is stopped, what was already sent (at most the sentence being said) finishes on the screens, and the lesson ends (
lesson.endedwith"reason": "time"). Lines sent after it are refused (409), and a student's question after it starts no new lesson.
- The time left. The teacher hears it after each step, and wraps up in the last 3 minutes (a recap and the close, nothing new). Your server hears
- Recording. A lesson is recorded when any of these holds:
"record": trueon/session,/commandor/teach; a teacher with"record": true(unless the lesson saysfalse); or the record button.- The video has the board and the voice, and only the parts recorded.
- Videos are made one at a time after the lesson.
- They are kept under
recordings/TENANT/LESSON.mp4. Later they move to the R2 bucket, under the same key, and the API gives them from there.
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.
- The voice never holds the class long. A sentence waits at most 8 s for its voice (the first seconds filled with a short phrase); then the board goes on with it as a caption, and the next sentences are spoken again.
- The board stays a few seconds ahead. It puts each sentence on the timeline shortly before it is due, so it can stop at the next sentence for a student. (
GET /board/:ch/statehasqueued: lines written, not yet on.) - A raised hand (
kind: "hand"): at the next sentence the board says «હા Ravi, બોલો, શું પ્રશ્ન છે?» (or «હા, બોલો…» without a name), and waits 20 s for that student's question (half way, one reminder: «Ravi, તમારો પ્રશ્ન લખો કે બોલો, હું સાંભળું છું.»); every screen hearsturn {who, name, until}and shows whose turn it is (chalk-turnon<chalk-board>;{who: null}when over). The question comes: «હા Ravi.», and the teacher answers it by name (no second greeting or praise), checks it with one MCQ, and goes on from where it stopped. None comes: «ઠીક છે Ravi, પછી પૂછજો.», the hand comes down, and that student's hand is off for 3 minutes. Several hands are taken in order. - "Explain again" (
kind: "again"), or a question saying the student did not understand: «હા Ravi, ચાલો ફરીથી સરળ રીતે સમજીએ.», then the teacher explains the last point again, more simply, by name, and checks it. - A question at any time: the same, at the next sentence. One that says it was not understood ("khabar na padi", "samjayu nahi", "I did not get it") is taken as "explain again". One outside the book (
scope: "book"): the fixed line with the student's name, and it is flagged. Gujarati or Hindi in Latin letters is understood as any question. - "Understood" (
kind: "got"): no interruption; the teacher hears how many. - What the class may use.
POST /board/:ch/controls {hand?, question?, again?}(a key for the channel;trueorfalse) turns them on or off; the teacher can too (acontrols hand=offline). While an MCQ is open all three are off. Every screen hearscontrols {hand, question, again}(chalk-controls) and greys the buttons; a call while one is off gets 409{off}. While it is a student's turn, that student may ask even with questions off. - MCQs. A screen's answer is
POST /board/:ch/answer {ask, choice, who},choicefrom 1 (1 is A); 0 is refused (400). The screens show the question's words with the choices (q). A student's screen gets the answer only when it closes (reveal {ask, answer, why}): right or wrong, the right answer, and, when the teacher gave it, why their own wrong choice is wrong. The teacher waits for the answers, then tells those who chose wrong, plainly and why. - Points. A right answer earns 10 (or the MCQ's
points, 20 in a revision round); the screen shows «સાચું! +10».GET /points?class=&learner=&lesson=&from=&to=gives each student's points, right and answered, most first;class.endedhas the lesson's. What points are for is the host's choice. - The end. When the lesson ends (its time, the host, or the Principal), nothing more is said; every screen shows «વર્ગ પૂરો થયો» (and «રેકોર્ડિંગ થોડી વારમાં તૈયાર થશે» when it was recorded) instead of the last page, with a button to watch it again;
chalk-endedreaches the host's page. - Each student, as they are. After each MCQ the teacher hears how each student does today against their earlier lessons, helps the one lower today first, and praises by name what improved (never a score aloud).
- Revision, not homework. The board's teacher sets no homework by itself. At the close it tells the class that what it learned will be asked at the start of the next class, for points; the next class with the same teacher starts with 3 to 5 MCQs on it, the shaky concepts first.
| Route | What |
|---|---|
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
| Route | What |
|---|---|
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=TOKEN | The 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=TOKEN | The 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 |
- Private lessons. On the hosted board every lesson is private: a screen shows it only with a token. Make tokens on your server, never in the students' app, since a token made in the app would need your key there.
- Answers. When the teacher asks the class a question (
ask), the screens show the choices as buttons, and the answers come back to the teacher. - Students' questions, typed or spoken.
- Asking. Under the board, a student types a question, or taps the microphone and speaks it. The browser turns the speech into text in the lesson's language (Gujarati, Hindi or English). The teacher's voice goes quiet on that screen while the student speaks. Chrome, Edge and Safari can do this; a browser that cannot shows no microphone.
- Who answers. The question waits on the channel until the teacher hears it.
- The board's own teacher answers it at its next step while it teaches, or at once when it is free.
- An AI that teaches over MCP gets it with its next
board_writeorboard_status.
- How it is answered. The teacher says whose question it is and answers it on the board: written, drawn and spoken. Then it checks the answer is understood.
- What screens see. Each screen sees its own questions, waiting and then heard. Consoles see every question's words.
- Limits. A screen may have 2 questions waiting, and a channel 20.
Your account
| Route | What |
|---|---|
GET /me | Your tenant, plan, minutes used this month, and what this key may do |
GET /keys | Your 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/:id | Revokes a key, and every viewer token it made |
GET /usage?from=YYYY-MM-DD&to=YYYY-MM-DD | Lessons, minutes, AI tokens and speech, by day. This month by default |
GET /learners | Your learners, with how many notes each has |
GET /learners/:id | What the teacher noted about one learner |
GET /learners/:id?history=1 | A 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/:id | Forgets a learner and every note about them |
GET /export | Everything of your tenant's, as one JSON file (a manage key): see below |
- The export.
{format: "chalkmind-export/1", tenant, keys, users, hours, webhooks, teachers, books, library, learners, lessons}: your account, keys, users, hours and webhooks; your teachers and their books; your own library items; your learners and every note about them; and every lesson, with what was said and written, its pages, who took part and how, and its recording'surl. It never holds a key, a password or a webhook's secret. Videos are fetched from theirurl.
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:
choice: options, and the number of the right one.short: a word, a number or a sentence, with the expected answer.work: the working, typed or as a photo of the notebook, with the expected answer or its key steps.
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.
| Route | What |
|---|---|
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/:id | Its 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/:id | Deletes 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. Up to 40. Each has its
marks(1 to 10) and itssection("A"). - Timed or on paper. With
minutes(5 to 240), it is written now, and answers are refused 5 minutes after the time is up. Without them, it is written on paper and handed in as up to 4 photos of the answer sheet (images), from the screen or your app. The AI reads every answer from the photos. - Marking. Working and answers of more than a mark get step marks, as the board's examiner gives them.
- Paper. It prints as a paper (
GET /homework/:id/paper, or the screen's link), and the teacher gets an answer key (?key=1).
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.
| Route | What |
|---|---|
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=html | The 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.
| Route | What |
|---|---|
POST /webhooks | {url, events?}: a webhook (at most 10). Its secret is shown only in this answer |
GET /webhooks | Your webhooks, and the events there are |
PATCH /webhooks/:id | {events}: what it hears, changed; its url and secret stay the same |
DELETE /webhooks/:id | Stops one |
| Event | When, and its data |
|---|---|
lesson.ending | A class with minutes has 3 minutes left; its teacher is wrapping up: lesson, channel, teacher, learner, minutes, minutes_left |
lesson.ended | A 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.ready | Its 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.failed | Its 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.noted | The teacher noted a learner, or a student's answer was noted: learner, lesson, concept, status, note |
question.asked | A student asked, typed, spoken or with a photo: lesson, channel, learner, name, text, voice, photo |
homework.set | Homework was set: homework, class, teacher, lesson, channel, title, due, questions |
homework.checked | A student's homework is marked: homework, class, learner, name, score, outOf, marks |
question.flagged | The 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.started | A lesson started on one of your channels: lesson, channel, teacher, learner, minutes (when it has a length) |
class.stalled | The Principal saw a class stall (nothing on the board for 30 s) and is taking steps: the incident (below) |
class.recovered | A stalled class goes on again, or the voice or the students came back: the incident, closed, outcome |
class.error | The 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.ended | A 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.shared | A 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.moved | Reserved for the timetable (the Principal's step 5): not sent yet |
report.weekly | Sunday evening, for each class taught that week: class, from, to, lang, summary, report, and students (each one's learner, name, summary, report) |
- What a call is. A POST of
{event, at, data}with the headersX-Chalkmind-Event,X-Chalkmind-DeliveryandX-Chalkmind-Signature: sha256=<hex>. The signature is the HMAC-SHA256 of the body with the webhook's secret: check it before you believe the call. - Retries. A call that fails is tried again after 10 s, 60 s and 5 minutes.
- Addresses. A webhook must be https, at a public address.
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.
| Problem | What 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 work | The 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 turn | The 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.
| Route | What |
|---|---|
GET /principal/status | Every 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.
| Route | What |
|---|---|
GET /tenants | Every 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/:id | One 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=ID | One tenant's use, or every tenant's without tenant |
GET /export?tenant=ID | One tenant's export |
DELETE /tenants/:id?confirm=ID | Deletes a tenant and everything of its own, for good: see below |
- Plans. A plan's
minutesPerMonthare lesson minutes. When they are used up, a new lesson is answered 402. - Pausing.
active: falsepauses a tenant, for example an unpaid plan: every call with its keys is answered 402 until it is active again. - Deleting. The tenant's id is sent twice (
?confirm=ID), because nothing comes back. Its live lessons are ended and their screens closed. Then everything of its own goes: its keys, users, hours, webhooks, teachers, books, library items, learners and their notes, and its lessons and their recordings. Keep its export first. An owner key cannot delete its own tenant, and a tenant whose keys are in the keys file (--keys) is refused (409) until they are taken out.
MCP
Any MCP client can teach on the board with the same key: Claude, claude.ai, Claude Code, or an agent platform.
- Hosted (Streamable HTTP):
https://HOST/v1/mcp, withAuthorization: Bearer KEY. Nothing needs to be installed. The first message isinitialize, and the answer'sMcp-Session-Idgoes with every later message. - On your computer (stdio):
node --experimental-transform-types bin/mcp.ts --board https://HOST --key KEY [--manage].
The tools:
- Teaching:
board_teacherslists your teachers.board_start teacher=IDbegins a lesson as one; its reply has the teacher's instructions and the students' link with a token. Thenboard_write,board_draw,board_look,board_book,board_library,board_state,board_status,board_hold,board_resume,board_remember,board_teach,board_commandandboard_end. - With a manage key:
teacher_savemakes or changes a teacher, andteacher_book_addadds a chapter to its book.
The tools are described in MCP.md.
Errors and limits
Every error is {"error": "what is wrong"}.
| Status | Meaning |
|---|---|
| 400 | Something in the request is wrong; the message says what |
| 401 | No key, or a key that may not use this channel |
| 402 | The account is paused, or the plan's minutes are used up |
| 403 | The key may not do this (it is not a manage or owner key, or the thing is not yours) |
| 404 | No such thing, or not yours |
| 409 | Taken already, or the channel's teacher is busy with an instruction |
| 413 | The body is too large |
| 429 | Too 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 |