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
- Using an AI app? Connect it through the MCP server, or install the Learnt 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
codesays which.
Errors
A refused request returns a 4xx status and a body like this:
{
"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.
Course
Each workspace has one course. More than one course per workspace is planned.
GET /coursereturnsid,title,descriptionandpublished.PATCH /coursetakes any oftitle(1–200 characters),description(up to 2,000 characters; longer text is cut) andpublished(trueshows 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 /moduleslists the modules, in order, with each one'shtml.POST /modulestakestitleplusmarkdownorhtml, and an optionalposition. It creates a module. Thepositiondoesn't move other modules, so to put a module in a particular place usePOST /modules/reorder.PATCH /modules/{id}changestitle,markdownorhtml, andposition."reviewed": trueclears an imported module's "Draft from your PDF" tag.DELETE /modules/{id}deletes a module.POST /modules/reordertakes"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.
{ "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: . Pictures hosted anywhere else are removed from modules.
Branding
GET /brandingreturnsname,primary_color,logo_url,support_email, the certificate signer, and the look of learner pages:header_style,page_background,font_pairing,background_image_urlandcover_image_url.PATCH /brandingtakes any ofname,primary_color(#RRGGBB),support_email,certificate_signer_name,certificate_signer_title,header_style(whiteorbrand),page_background(plain,tintorpicture) andfont_pairing(modern,classic,friendlyorplain). Sendnullto go back to the default.background_image_urlandcover_image_urlacceptnullto remove a picture.POST /branding/logo,POST /branding/backgroundandPOST /branding/coveraremultipart/form-datawithfile, and set the logo, the page background picture and the course cover picture. Setpage_backgroundtopictureto 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.
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-orgslists them, with learner counts.POST /client-orgstakesnameand an optionalaccess_ends_on(YYYY-MM-DD).PATCH /client-orgs/{id}takesnameand/oraccess_ends_on(nullremoves the end date).DELETE /client-orgs/{id}deletes one. Its learners stay enrolled, with no organization.
Learners
POST /learnerstakes"learners", a list of{ "name", "email", "client_org" }.client_orgis 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 wasalready_enrolled, anyerrors, and anyonenot_sentbecause today's invitation limit was reached.
GET /learnerslists learners with their progress. Each has auser_id(the{id}in the paths below) andaccess:active, orremovedfor someone whose access you removed.DELETE /learners/{id}removes a learner's access. Their progress is kept.POST /learners/{id}/restoregives a removed learner access again. Inviting a removed learner's address again does the same.POST /learners/{id}/resendsends the invitation again (once every 10 minutes per learner).POST /learners/{id}/remindsends 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 whichkindwas sent and whether it was delivered (emailDelivered).- Learner rows (
GET /learners,GET /progress) also carrylast_reminded_at,next_reminder_at(when the automatic reminder is due, ornull) andreminders_stopped. GET /limitsreturns 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.
GET /settings/remindersreturns{ "reminders_enabled": true, "applies": true }.PATCH /settings/reminderstakes{ "reminders_enabled": false }(ortrue) and switches automatic reminders for the whole workspace.
Questions
Questions learners ask from a module page. Details: Learners and client organizations.
GET /questionslists open questions, newest first;?status=allincludes answered ones. Each hasid,learner_name,learner_email,company(their client organization),module_title,message,asked_at,delivered(emailed to you) andanswered. The reply also hasmodules(per-module counts),openandtotal.PATCH /questions/{id}takes{ "answered": true }(orfalseto open it again). It sends nothing; replies happen by email.GET /settings/questionsreturns{ "questions_enabled": true, "applies": true, "sent_to": "support_address" }(sign_in_emailwhen no support address is set).PATCH /settings/questionstakes{ "questions_enabled": false }(ortrue).
Progress
GET /progressreturns each learner's modules completed out of the total. Add?client_org={id}(theidfromGET /client-orgs) for one client organization, or?client_org=nonefor learners without one. An id that isn't one of your client organizations is refused withnot_found. Each row has the sameaccessfield asGET /learners.GET /progress.csvreturns the same as a CSV file, ready to send to a client.
Export
GET /export/markdownreturns 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/markdownreads the same format and skips the quiz notes. It works with a read-only key, and on a locked workspace.