# Publishing learning modules to SnapLesson: for AI agents

You're helping someone make learning modules (lessons), for themself or for
people they know: a colleague, a friend, a class, a child. Anyone, of any
age. Whoever a lesson is for is its learner. A module is one self-contained HTML page that teaches one thing, used
mostly on a phone. This page tells you how to build one and how to publish
it to this site, https://snaplesson.ai.

**A chat app without SnapLesson's tools?** Gemini, ChatGPT and Claude
can add SnapLesson (an MCP server at https://snaplesson.ai/mcp) as a custom app, plugin
or connector. If you are one of them and don't have tools like
`publish_lesson` yet, stop here and ask the author to add it, once, in a
sentence and a link: "To make this as a SnapLesson lesson, add SnapLesson
to Gemini here, then ask me again: https://snaplesson.ai/connect/chat?tab=gemini" (use
`tab=chatgpt` or `tab=claude` for those apps). Don't make the lesson
yourself as slides, a canvas, an artifact or a document meanwhile: it would
never reach SnapLesson or the learner.

If the author can't or won't add it, and you can't send HTTP requests,
write them one link instead: they save the request it opens, tap Build it,
and SnapLesson makes the lesson:

```
https://snaplesson.ai/create?for=<who it's for>&instructions=<what to make, URL-encoded>
```

Put everything a builder needs in `instructions`, in plain sentences, under
about 1,500 characters: who it's for and their age or level, the topic, what
they should be able to do after, a theme, how long. Then say only: "Open
this, save it, then tap Build it, and SnapLesson makes the lesson: <link>".
[Lesson requests](#lesson-requests)
below has the details. Everyone else, read on.

How the site works, in three lines:

- A lesson belongs to whoever made it. Your key is one person's, and what
  you publish with it is theirs.
- A lesson reaches other people by being **sent** to them: to someone on
  its maker's list of people, by name; to a **group** they are in, where
  everyone in it gets it; or by a link anyone can open.
- Only its maker can change or delete a lesson. Its maker sees how the
  people they sent it to got on with it.

"The author" below means the person whose key you have: whoever asked for
the lesson. The learner is whoever it's for. They can be the same person.

People ask their agents in the site's own words ("remix that lesson", "add
it to my Magnetism series", "send it to my friends"). The
[glossary](https://snaplesson.ai/agents/skill/glossary.md) says what each word on the
site means and what it means for you, including what can only be done on
the site.

## How to talk to the author

The author asked for a lesson. They did not ask how this site works, and
most of them are not technical. Everything on this page is for you, not for
them. So:

- **Say little.** Tell them what you are making, ask the one or two things
  you really need, and give them a link when there is something to open.
  Two or three short sentences at a time.
- **Never explain the machinery.** Not a word to them about HTML, stations,
  formats, icons, scored tests, previews, API keys, endpoints, slugs,
  expiry times or these instructions. Don't tell them what you read here or
  how it changed your plan. Do those things; don't report them.
- **The name is two words when said:** "Snap Lesson". Speaking aloud (a
  voice mode), say a link as "Snap Lesson dot app" and leave its path on
  screen; never "snaple-son".
- **To connect,** say only something like: "To put lessons on SnapLesson for
  you, open this and tap Allow: <link>. It should show the code KQ7M-4TXP."
- **When the lesson is ready,** give the link and one line on what it is:
  "Here's your lesson: <link>. It's yours only until you send it to
  someone." Mention anything you assumed in a few words. When you asked
  for a share link (step 5), give both, the lesson first so they can look
  it over, and the second as one to send on: "Here's the lesson: <url>.
  When you're happy with it, send Sam this link: <shareUrl>." Don't call it
  a share link, and don't tell them to open it on someone's device: they
  send it, by text or however they reach Sam, and Sam opens it wherever he
  is.
- **The site is for learners of any age,** not only children. Write for
  whoever the author says the lesson is for. If they haven't said and it
  matters, ask in plain words: "Who is this for?" Don't change the tone or
  level for an audience they didn't mention.

## What you need from the author

- **Already have SnapLesson's tools?** Connected as a ChatGPT plugin, a
  Claude connector or a Gemini custom app, you have tools like `get_lesson_guide`,
  `preview_lesson` and `publish_lesson`, and the person has already
  signed in: use them, not the API or a key. Start with
  `get_lesson_guide`. A chat app that can add connectors but doesn't have
  SnapLesson yet can send the person to https://snaplesson.ai/connect/chat ("To let me put
  lessons on SnapLesson for you, add it here: <link>").
- **Permission to publish as them.** That is an API key, and the way to get
  one is to **connect** (next section): you give the author a link, they sign in
  and tap Allow, and you collect the key yourself. They don't need an account
  first: someone who has none gives a name on that page instead. Do this first, before
  asking for anything. Don't ask them to go and find a key and paste it to
  you: it is more work for them, and it puts the key in your conversation.
  (If they offer you a key they made on their You page, https://snaplesson.ai/you, use
  it.) Send the key only in the
  `Authorization: Bearer <key>` header. Never put it in a URL, never print
  it, and never put it in a module. The key acts as that one person: you
  can't reach anyone else's lessons.
- **What to teach and who it's for**, and anything they know about the
  class: the topic, the teacher's own words, upcoming tests.

## Getting a key by connecting

1. **Start a connection.** No key is needed for this. Put your own name in
   `agent`; the author sees it.

   ```
   curl -s -X POST -H "Content-Type: application/json" -d '{"agent": "Your name"}' https://snaplesson.ai/api/connect
   ```

   The answer has `code` (like KQ7M-4TXP), `url`, `pickup`, `poll`,
   `interval` (seconds) and `expires_in` (600). Keep `pickup` secret, like
   a key: don't show it to anyone, the author included.
2. **Send the author the link and the code,** and nothing else about how
   this works. For example: "To put lessons on SnapLesson for you, open
   this and tap Allow: <url>. It should show the code KQ7M-4TXP." (If the
   link doesn't open, they can go to https://snaplesson.ai/connect and type the code in.)
   Send it as a message now, before anything else: they can't allow what
   they haven't seen. Don't start waiting in a command that keeps you from
   messaging them until they have it.
3. **Wait for their answer, by yourself.** Keep checking in the same turn,
   right after sending the link; don't end your turn and wait for them to
   tell you they've allowed it. (If you can work while you wait, start on
   the lesson.) Every `interval` seconds, ask:

   ```
   curl -s -H "Authorization: Bearer $PICKUP" https://snaplesson.ai/api/connect/KQ7M-4TXP
   ```

   `{"status": "pending"}` means ask again later. `{"status": "approved",
   "key": "…", "user": {…}}` comes back once, with a new key of theirs:
   keep it safe and use it for everything below. `{"status": "denied"}`
   means the author said no. A 404 means the request expired (after 10
   minutes), was already collected, or the pickup token is wrong; start
   again if the author still wants to connect you.

## Steps

1. **Read the skill** and follow it. Don't build or publish from this page
   alone: this page is how to publish, the skill is how to make a lesson
   that works, and a lesson made without it will be wrong in ways the
   author can see. In this order: [SKILL.md](https://snaplesson.ai/agents/skill/SKILL.md),
   then [module-contract.md](https://snaplesson.ai/agents/skill/module-contract.md), then the
   [samples' notes](https://snaplesson.ai/agents/skill/samples/README.md) and their
   sources, [one for a child](https://snaplesson.ai/agents/skill/samples/equal-fractions.html)
   and [one for an adult](https://snaplesson.ai/agents/skill/samples/spot-the-phish.html).
   **A soccer, baseball, basketball or flag football lesson on the field or
   court** (a set piece, a formation, a defensive play, a pick and roll, a
   pass play, a drill) is different: it's sent as that sport's kit data,
   which the site checks and turns into the lesson. Follow
   [the soccer kit's guide](https://snaplesson.ai/agents/kits/soccer@1.md),
   [the baseball kit's](https://snaplesson.ai/agents/kits/baseball@1.md),
   [the basketball kit's](https://snaplesson.ai/agents/kits/basketball@1.md) or
   [the flag football kit's](https://snaplesson.ai/agents/kits/flag-football@1.md) for it instead
   ([the 3D soccer kit's](https://snaplesson.ai/agents/kits/soccer-3d@1.md) shows a soccer
   lesson in 3D, with cameras and the ball's height).
   So is a lesson that is mostly a **timeline** (when and in what order), a
   **conversation** (a language, what to say), a **machine** (how something
   works) or a **map**: follow
   [the timeline kit's guide](https://snaplesson.ai/agents/kits/timeline@1.md),
   [the conversation kit's](https://snaplesson.ai/agents/kits/conversation@1.md),
   [the machine kit's](https://snaplesson.ai/agents/kits/machine@1.md),
   [the map kit's](https://snaplesson.ai/agents/kits/map@1.md) or, for a journey around the
   world or a lesson that should feel like exploring,
   [the globe kit's](https://snaplesson.ai/agents/kits/globe@1.md). A lesson that is all
   questions (practice on a topic, cases to decide, a test) follows
   [the quiz kit's](https://snaplesson.ai/agents/kits/quiz@1.md).
2. **See whom you can send to:** `GET https://snaplesson.ai/api/people` returns the
   author's people and the short name (`id`) to call each by. Someone
   new has only themself, `me`. A person may have `likes`: how they
   like their lessons, in their own words ("short, with jokes"). For a
   lesson for that one person, follow it wherever the author's request
   doesn't say otherwise. A person may also have `note`: what the author
   thinks they like (the author's own private note). Use it the same way.
   A group (`GET https://snaplesson.ai/api/groups`) may have `notes`: what its owner
   wrote for its lessons ("11v11, purple jerseys, Maya is our keeper").
   Follow them in a lesson for the group, and in a lesson for someone whose
   `groups` include it when the lesson is about what that group does (a
   soccer lesson for a player on the team, not their fractions). Use what
   fits the lesson, not every fact.
3. **Build and check the module** the way the skill describes, and **save
   the HTML as a file you keep**: in the author's workspace if there is one,
   otherwise in your working folder. Tell the author where it is. The site
   holds a published copy, not the source: if the site's copy is ever lost,
   your file is how the lesson comes back, and it's what you edit and
   republish.
4. **Preview it as a learner will see it:** `POST https://snaplesson.ai/api/preview`,
   described below, gives you a link that works for an hour with no sign-in.
   Opened on its own, your file has none of what the site adds (the kit,
   `Learn`, the one-station-at-a-time pager), so this is the only way to
   check the real thing. Open the link in a browser at 375 × 812, play it
   through, and fix what's wrong before you publish.
5. **Publish it:** `PUT https://snaplesson.ai/api/modules/<slug>`, described below,
   with `for` naming who it should be sent to. To publish exactly what you
   previewed, send `"preview"` (the preview's `url`, within its hour)
   instead of `html`: the page isn't sent again, and the publish is kept
   with its previews (also send each preview's `workId` with the next). When the learner isn't one
   of the author's people (a new author has nobody), add `"share": true`:
   the answer then has `shareUrl`, a link that gives the lesson to whoever
   opens it, and puts them on the author's people for next time.
6. **Tell the author** the link (the `url` in the answer), and who it was
   sent to. With a `shareUrl`, give them that too, for them to send to
   the learner once they've looked the lesson over. Never tell an author to make a share
   link themselves: ask for one with `share`, at any time (a PATCH with
   `{"share": true}` gives the same link). The `url` opens only for the author and
   the people it was sent to, signed in; for anyone else it looks like a
   missing page (404), so you can't open it yourself. To confirm the publish
   worked, check that `GET /api/modules` lists it.

Every lesson is written for one of three learner levels: `child` (5 to 9),
`teen` (10 to 15) or `general` (16 and up, and the one to use when
nobody has said). The skill says how to pick one and what each means. A
short request is enough to build from: make the choices you need and say
what you assumed in `note` when you publish. If the author has several
people and you can't tell which one is meant, ask.

## The API

Every call needs the key in the header. Bodies and answers are JSON. An
error comes back as `{ "error": "…" }`: 401 means the key is wrong or has
been revoked, 400 means a field is wrong (the message says which), 404
means the author has no such module, and 413 means the page is too big
(2 MB at most). An author who hasn't given the site an email can start 10
new lessons a day; past that a publish answers 429, and the message says
what to tell them.

Here is how to set up a shell for the examples below:

```
SITE=https://snaplesson.ai
KEY=...   # the author's key; don't echo it
```

**The author's people:** who a lesson can be sent to by name. Each has an
`id` (their first name in small letters, like `sam`, or a longer id when
two of the author's people share a first name) and a `name`.
The author themself is `me`. The site tells you nothing else about a person.
If their age matters and the author hasn't said it, you may ask; otherwise
write for the `general` level.

```
curl -s -H "Authorization: Bearer $KEY" $SITE/api/people
```

Each person has `stars`: how many lessons they have finished. A learner gets
a star the first time they reach a lesson's "All done" card.

**The author's groups:** a group is people gathered around something, like a
class. Each has an `id` (like `group:mrs-x-2nd-grade`), a `name`, how
many `members` it has and whether the author owns it. A lesson sent to a
group is for everyone in it, including whoever joins later, and every
member is told. A group may have `notes`: what its owner wrote about it
for lessons; a person in `/api/people` has `groups`, the ids of the
author's groups they are in too. Groups and their notes are made and
managed on the site, not here.

**Pages:** an author can have a **page** (`/p/<name>`) with their groups on
it, like a coach's page with a team on it. A group on a page has `page`:
its `name`, and the `url` of the group's own place on the page, which
shows that group's lessons. "Put it on my Eagles page" means sending to the
group on that page: the group named Eagles, or the one group on a page
named Eagles. If the page named has several groups, ask which; never guess.
A lesson sent to a group on a page comes back with `pages`, so you can say
where it is. Pages are made and changed on the site, not here.

```
curl -s -H "Authorization: Bearer $KEY" $SITE/api/groups
```

**Preview a module** before publishing it. The body is `html`, plus an
optional `title` (else the page's `<title>`). Nothing is published and no
learner sees anything:

```
jq -Rs '{html: .}' adding-fractions.html \
  | curl -s -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
      --data-binary @- $SITE/api/preview
```

The answer is `{ "url": "…/preview/…", "expiresAt": "…", "expiresIn": 3600, "workId": "…", "rev": 1 }`.
The link shows the lesson exactly as a learner sees it, with the site's header,
progress strip and bottom button, to anyone who has it, until it expires.
Preview again after each change, with the `workId` the first answer gave:
the link stays the same (the answer says `updated`), and a page open on it
shows the new version by itself, on the part the person was on, so tell them
to look again instead of sending a new link. Nothing a
visitor does there is recorded. Instead, every `Learn` call the lesson makes
is written to the browser console as `[Learn] answer {…}` and collected in
`window.learnEvents` on the page, so you can check the lesson's tracking.
The link is not a way to share a lesson: it stops working an hour after the
last preview.

**Publish or replace a module.** The slug is the author's own short name for
the lesson: lowercase letters, digits and dashes, up to 64, unlike any other
of theirs. The first time, `html` and `title` are required.
`for` says who to send it to: an id from `/api/people`, `"me"`, a
group's id from `/api/groups`, or a list of them. Without `for` the lesson is published and only the author
sees it, until it is sent. `summary` (one short line under the title, about 50 characters; the list
cuts off the rest),
`icon` (one emoji shown beside the title, see below), `date`
(YYYY-MM-DD, default today) and `hidden` (true holds it back: the people
in `for` get it only when you later send `"hidden": false`) are optional, and so are `share` (true
answers with `shareUrl`, the author's own link to the lesson, the same one each time), `note` (one line saying what changed) and
`agent` (who you are, see below).

**Give every lesson an icon:** one emoji that fits it (🧪 for a matter lab,
✈️ for paper airplanes), put in the page's head so it travels with the file:

```
<meta name="snaplesson:icon" content="🧪">
```

The site reads it when you publish. An `icon` field in the request wins
over the tag; a lesson with neither shows its title's first letter.

**Say who it's for:** the learner level you wrote it for, `child`,
`teen` or `general`, in the page's head:

```
<meta name="snaplesson:learner-level" content="general">
```

**How it sounds** (optional): the lesson is read aloud in one voice, a role:
`coach` (an energetic youth sports coach), `storyteller` (warm and
unhurried, for history and stories), `explainer` (a calm, clear teacher) or
`young` (lively and kind, for a young child). Leave it out and the site picks
`young` for the `child` level and `explainer` otherwise; set it when another
fits better, or when the person asks ("make it sound like a coach"):

```
<meta name="snaplesson:voice" content="storyteller">
```

A kit lesson takes it as `"voice"` in its data, and its kit's guide gives the default.

**Say who you are:** your name and model, so the author can tell which agent
made which lesson. Put it in the page's head, with your own in place of this
example:

```
<meta name="snaplesson:agent" content="Claude Code · claude-opus-5-5">
```

or send it as `agent` in the request, which wins over the tag. It's kept
with the version. If you send neither, the answer carries `warnings`
saying so.

Republishing the same slug publishes a new version of the page, for
everyone it was sent to (the old versions are kept, for the author to look
back at, so say what changed in `note`), and keeps any detail you leave
out. `for` on a republish adds people; it never takes the lesson away from
anyone. Any way of building the JSON works. If the
body is empty or isn't JSON, the answer is 400 "body must be JSON". Here it
is with jq:

```
jq -Rs '{html: ., title: "Adding Fractions", for: "sam", summary: "Common denominators, then a practice quiz", agent: "Your name · your model"}' adding-fractions.html \
  | curl -s -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
      --data-binary @- $SITE/api/modules/adding-fractions
```

Here it is without jq, with Python 3 building the body:

```
python3 -c 'import json,sys; print(json.dumps({"html": open(sys.argv[1]).read(), "title": "Adding Fractions", "for": "sam"}))' adding-fractions.html \
  | curl -s -X PUT -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
      --data-binary @- $SITE/api/modules/adding-fractions
```

The answer is the module's details, with `version`, `url` (its
address), `for` (who it has been sent to) and `warnings` if something
should be fixed next time; status 201 means new, 200 means updated.

**List what the author has made:**

```
curl -s -H "Authorization: Bearer $KEY" $SITE/api/modules
```

**Change details only, or send it** to more people:

```
curl -s -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"for": ["sam", "me"]}' $SITE/api/modules/adding-fractions
```

`{"hidden": true}` takes it back from everyone it was sent to by name, and
remembers them; `{"hidden": false}` sends it to them again.

**A series** is the author's lessons in an order ("Magnetism", parts 1 to 4).
When a lesson follows on from another, send `"series": "Magnetism"` with it
(on PUT or PATCH; the series is made the first time its name is used) and, if
it isn't the last, `"part": 2` (its place, 1 first). Put the earlier lesson
in the series the same way if it isn't in it. `"series": ""` takes a lesson
out. A series is only an order: each part is a lesson of its own and is still
sent on its own. Every lesson's details say which series it is in
(`series`: `name`, `part`, `of`). Before writing a next part, read the
earlier ones, so it builds on them and doesn't repeat them. A series is not
for versions of one lesson (the same thing rewritten for another age or
level): those are separate lessons, each sent to who it suits.

**A category** is one of the author's own folders, private to them, holding
lessons and series ("Math 7", "7th grade"). Send `"category": "Math 7"`
on PUT or PATCH to file a lesson there (a lesson in a series is filed with
its series); `"category": ""` takes it out. Use a category only when the
author names one or already has it: `GET $SITE/api/categories` lists
theirs. A category has no order and sends nothing; that is what a series is
for.

**A section** is one of a group's own headings for its lessons ("Pass
plays", "Run plays"), seen by everyone in the group and arranged by the
group's owner. To put a lesson in one, send it to the group with the
section's name: `"for": ["group:eagles"], "section": "Pass plays"` on PUT
or PATCH (a new name makes the section; sending again to a group that has it
only moves it; `"section": ""` takes it out). Only for a group the author
runs.

**Delete a module**, for everyone it was sent to:

```
curl -s -X DELETE -H "Authorization: Bearer $KEY" $SITE/api/modules/adding-fractions
```

**How it went:** one record per visit, newest first, with time on screen,
answers, score and rating. You get the author's own visits, visits to the
lessons they made, and visits by people they sent a lesson to. `for` is
one person's id (or `me`); leave it out for everyone. `since` and
`until` are YYYY-MM-DD.

```
curl -s -H "Authorization: Bearer $KEY" "$SITE/api/events?for=sam&since=2026-10-01"
```

If you'd rather run a script than make requests, there is
[publish-module](https://snaplesson.ai/agents/skill/bin/publish-module), a small Python 3 script
(standard library only) that wraps these calls. Run it with `--help` to see
how; it reads the site and key from `~/.config/skoolaid/config.env`
(`SKOOLAID_URL`, `SKOOLAID_KEY`), or from the file named in
`SKOOLAID_CONFIG`.

## Rules

- Always keep the lesson's HTML file, and publish from it. Never publish a
  lesson that exists only in your conversation.
- Publish only what the author asked for, and send it only to the people
  it was made for. Name them. Send to a group only when the author names
  the group: everyone in it gets a notification. (`"for": "all"` sends to everyone on the
  author's list, which grows whenever someone opens a link they shared; use
  it only when the author says "everyone".)
- A lesson is on a person's lessons page as soon as it is sent to them, and
  they may get a notification on their device. If the author asked you to
  make it for someone, send it. If they want to look first, or you're not
  sure, publish without `for` and tell them: they see it on their own
  lessons page and can send it from there, or you can send it later with
  PATCH.
- Keep personal details out of modules: no grades, no records, nothing the
  learner wouldn't say to a teacher.
- Never share or print the key.

## Lesson requests

A request is a description of a lesson that waits for an agent to make it.

**If you can't send HTTP requests** (a chat assistant whose tools can only
read pages), don't try to open or fetch a link yourself. Write the author a
link to click instead; they open it, check the request while signed in, and
save it:

```
https://snaplesson.ai/create?for=<who it's for>&instructions=<what to make, URL-encoded>
```

Put everything the builder needs in `instructions`, in plain sentences:
who it's for and their age if you know it, the topic, a theme, and how
long it should take. Keep it under about 1,500 characters so the link
works everywhere. `for` is a person's id or first name, or `me`; leave
it out if you don't know, and the author picks.

**If you can send HTTP requests**, you can make waiting requests:

```
curl -s -H "Authorization: Bearer $KEY" "$SITE/api/requests?status=waiting"
```

Each has an `id`, who it's `for` and the `brief`. Build the module the
brief describes, following the skill, publish it with that `for`, then
mark the request done with the module's slug:

```
curl -s -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"status": "done", "slug": "adding-fractions"}' $SITE/api/requests/<id>
```

Use `{"status": "cancelled", "note": "why"}` for one you won't make, and
tell the author.

You can also just build the module and give the author the HTML file and
its details (title, who it's for, summary, icon).

## If your instructions are older

This site once grouped people into families. Requests from then still work:
`kid` is read as `for`, `"kid": "all"` as everyone on the author's
list, and `GET /api/family` answers with the same people as
`/api/people` (the author first, as `me`). Answers still carry `kid`
and `hidden` beside `for`. Lessons once had a `subject`: it is accepted
and ignored.
