# API

The Learnt API lets your own tools and AI agents build and run almost everything in a workspace: the course and its modules, branding, client organizations, learners and progress. Signing up, billing, creating API keys and connecting a custom domain are done by a person in the Course Editor. It uses the same rules as the Course Editor, so anything the API writes is checked exactly as if you'd typed it in.

- Base URL: `https://learnt.tech/api/v1`
- OpenAPI description: [learnt.tech/docs/openapi.json](https://learnt.tech/docs/openapi.json)
- Using an AI app? Connect it through the [MCP server](https://learnt.tech/docs/mcp), or install the [Learnt skill](https://learnt.tech/docs/agent-skill) in a coding agent.
- Every response with a body is JSON, except the CSV export; a successful delete may answer 204 with no body.

## Keys

Create a key in the Course Editor under **Developer access**. Learnt shows the whole key once, when you create it, so store it straight away. Keys start with `lt_live_`.

Send it on every request:

```
Authorization: Bearer lt_live_…
```

- A key belongs to one workspace, and everything it does happens in that workspace.
- Choose **Only read** for a key that should never change anything, such as one used for reports.
- A key can't change billing or create other keys.
- Revoking a key in Developer access stops it on its next request.
- Each key can make 120 requests a minute.
- Learner invitations count against the same daily limit as the Course Editor's: 25 in any 24 hours during the trial, 500 once the paid subscription starts.
- If the trial ends or the subscription stops, the API refuses what the Course Editor refuses (changes, and reading the modules) until billing is fixed. The refusal's `code` says which.

## Errors

A refused request returns a 4xx status and a body like this:

```json
{
  "error": "This API key is read-only",
  "code": "read_only_key",
  "fix": "Use a key created with write access for this call.",
  "docs": "https://learnt.tech/docs/errors#read_only_key"
}
```

The `code` never changes, so code and agents can rely on it. Every code is listed on the [errors page](https://learnt.tech/docs/errors).

## Course

Each workspace has one course. More than one course per workspace is planned.

- `GET /course` returns `id`, `title`, `description` and `published`.
- `PATCH /course` takes any of `title` (1–200 characters), `description` (up to 2,000 characters; longer text is cut) and `published` (`true` shows the course to enrolled learners).

## Modules

Modules are the course's pages, in order. Write them in markdown (`"markdown"`) or HTML (`"html"`). Either way the text is cleaned the same way the editor cleans it: scripts and outside images are removed. YouTube and Vimeo embeds are kept when sent as HTML iframes from `www.youtube-nocookie.com/embed/…` or `player.vimeo.com/video/…`; other video links stay plain links.

- `GET /modules` lists the modules, in order, with each one's `html`.
- `POST /modules` takes `title` plus `markdown` or `html`, and an optional `position`. It creates a module. The `position` doesn't move other modules, so to put a module in a particular place use `POST /modules/reorder`.
- `PATCH /modules/{id}` changes `title`, `markdown` or `html`, and `position`. `"reviewed": true` clears an imported module's "Draft from your PDF" tag.
- `DELETE /modules/{id}` deletes a module.
- `POST /modules/reorder` takes `"ids"`, every module's id in the new order.

### Import a whole course from markdown

`POST /import/markdown` takes `"markdown"`, one document in which each `# ` heading starts a new module. The modules are added after any you already have.

- Text before the first heading is used as the course description only if you also send `"use_preamble_as_description": true`.
- Use `##` and `###` for headings inside a module.
- One request can hold up to 100 modules and 200,000 characters.

```json
{ "markdown": "# Welcome\nWhat this course covers.\n\n# Your first week\n## Three things to do\n- Meet everyone\n- Write down how work gets done" }
```

### Pictures

`POST /images` is a `multipart/form-data` request with `file` (JPG, PNG, WEBP or GIF, up to 4 MB) and `course_id` (from `GET /course`). It returns the picture's `url`, which you can use in a module's markdown: `![Alt text](url)`. Pictures hosted anywhere else are removed from modules.

## Branding

- `GET /branding` returns `name`, `primary_color`, `logo_url`, `support_email`, the certificate signer, and the look of learner pages: `header_style`, `page_background`, `font_pairing`, `background_image_url` and `cover_image_url`.
- `PATCH /branding` takes any of `name`, `primary_color` (`#RRGGBB`), `support_email`, `certificate_signer_name`, `certificate_signer_title`, `header_style` (`white` or `brand`), `page_background` (`plain`, `tint` or `picture`) and `font_pairing` (`modern`, `classic`, `friendly` or `plain`). Send `null` to go back to the default. `background_image_url` and `cover_image_url` accept `null` to remove a picture.
- `POST /branding/logo`, `POST /branding/background` and `POST /branding/cover` are `multipart/form-data` with `file`, and set the logo, the page background picture and the course cover picture. Set `page_background` to `picture` to show the background picture.

Your own web address is set up in the Course Editor, because it needs DNS records at your domain provider. See [Branding and domains](https://learnt.tech/docs/branding-and-domains).

## Client organizations

These are the companies that buy your training. Each one groups its learners, has its own progress report, and can have an access end date (access ends after that date, in UTC).

- `GET /client-orgs` lists them, with learner counts.
- `POST /client-orgs` takes `name` and an optional `access_ends_on` (`YYYY-MM-DD`).
- `PATCH /client-orgs/{id}` takes `name` and/or `access_ends_on` (`null` removes the end date).
- `DELETE /client-orgs/{id}` deletes one. Its learners stay enrolled, with no organization.

## Learners

- `POST /learners` takes `"learners"`, a list of `{ "name", "email", "client_org" }`. `client_org` is optional and is a client organization's name; a new name creates that organization.
  - Each new learner gets your branded invitation, which signs them straight in.
  - The response lists who was `invited`, who was `already_enrolled`, any `errors`, and anyone `not_sent` because today's invitation limit was reached.
- `GET /learners` lists learners with their progress. Each has a `user_id` (the `{id}` in the paths below) and `access`: `active`, or `removed` for someone whose access you removed.
- `DELETE /learners/{id}` removes a learner's access. Their progress is kept.
- `POST /learners/{id}/restore` gives a removed learner access again. Inviting a removed learner's address again does the same.
- `POST /learners/{id}/resend` sends the invitation again (once every 10 minutes per learner).
- `POST /learners/{id}/remind` sends that learner a reminder now: the "your training is waiting" one if they haven't opened the course yet, else the "pick up where you left off" one. It counts toward the daily invitation limit (reminders may use at most half of it, `reminder_limit_reached`), works at most once per learner in any 24 hours, and is refused while the course is a draft (`reminders_course_draft`), for a learner who finished (`learner_complete`) or turned reminders off (`reminders_stopped`). The reply says which `kind` was sent and whether it was delivered (`emailDelivered`).
- Learner rows (`GET /learners`, `GET /progress`) also carry `last_reminded_at`, `next_reminder_at` (when the automatic reminder is due, or `null`) and `reminders_stopped`.
- `GET /limits` returns today's invitation limit and how much of it is used. Reminders count toward it and may use at most half of it.

## Reminders

Learners get up to two automatic reminders in your name: 7 days after the invitation if they haven't opened the course, and when progress stops for 3 to 6 days. Details: [Learners and client organizations](https://learnt.tech/docs/learners-and-organizations#reminders).

- `GET /settings/reminders` returns `{ "reminders_enabled": true, "applies": true }`.
- `PATCH /settings/reminders` takes `{ "reminders_enabled": false }` (or `true`) and switches automatic reminders for the whole workspace.

## Questions

Questions learners ask from a module page. Details: [Learners and client organizations](https://learnt.tech/docs/learners-and-organizations#learner-questions).

- `GET /questions` lists open questions, newest first; `?status=all` includes answered ones. Each has `id`, `learner_name`, `learner_email`, `company` (their client organization), `module_title`, `message`, `asked_at`, `delivered` (emailed to you) and `answered`. The reply also has `modules` (per-module counts), `open` and `total`.
- `PATCH /questions/{id}` takes `{ "answered": true }` (or `false` to open it again). It sends nothing; replies happen by email.
- `GET /settings/questions` returns `{ "questions_enabled": true, "applies": true, "sent_to": "support_address" }` (`sign_in_email` when no support address is set).
- `PATCH /settings/questions` takes `{ "questions_enabled": false }` (or `true`).

## Progress

- `GET /progress` returns each learner's modules completed out of the total. Add `?client_org={id}` (the `id` from `GET /client-orgs`) for one client organization, or `?client_org=none` for learners without one. An id that isn't one of your client organizations is refused with `not_found`. Each row has the same `access` field as `GET /learners`.
- `GET /progress.csv` returns the same as a CSV file, ready to send to a client.

## Export

- `GET /export/markdown` returns the whole course as one markdown file (`text/markdown`), the same file as **Download course** in the Course Editor: each module starts with `# Title`, quizzes follow their module as HTML-comment notes, and pictures stay as links. `POST /import/markdown` reads the same format and skips the quiz notes. It works with a read-only key, and on a locked workspace.
