Courses spaces
A course teaches something in order: modules of lessons a learner works through. A piece is a lesson, backed by a page, so everything you can do in a documentation page you can do in a lesson. What makes it a course rather than a tree of pages is an ordered outline, quizzes with recorded attempts, a learner's progress, and a certificate that somebody else can verify.
A courses space answers at /co/ on the platform (/co/{orgId}/{spaceId}), or at the root of your own domain. Its reader has these pages:
Address | What it is |
|---|---|
| The landing: the hero, the facts, the start button and the outline. |
| A lesson, with the course rail beside it. |
| A module's own page: its cover, description, lessons and progress. |
| A module assessment. |
| My learning: the learner's courses, paths and certificates. |
| The organization's catalogue of public courses and published paths. |
| A learning path's page. |
| Where Stripe returns a buyer. |
| A certificate's public verify page (and |
| A learning-path certificate's verify page. |
| The embeddable course card and buy button (below). |
The verify addresses are public by design: a certificate nobody else can check is a picture.
The outline, quizzes and certificate
Call | What it does |
|---|---|
| The modules and their lessons, in order. |
| Attach or replace a lesson's quiz. |
| A module assessment. |
| Attempts on a quiz. |
| The learner's certificate. |
| A public, verifiable PDF of a certificate. |
A module (a collection) can carry a description and a cover, which its own page and the outline draw.
Quizzes
A question has a type:
| What the learner does | How it is marked |
|---|---|---|
| Picks one or several of | Against |
| Picks True or False. | Against |
| Types an answer. | Matched against |
| Writes free text. | By a person: the attempt waits for review. |
| Uploads up to five files. | By a person. |
| Puts | Automatically. |
| Pairs each of | Automatically. |
Each question may carry points (its weight, default 1), an explanation, an image and choiceImages. A quiz's settings:
Key | Default | What it does |
|---|---|---|
| off | A new question order per attempt. |
| off | A new choice order per attempt. |
| none | Each attempt draws this many questions from the bank, and is graded on them. |
| none | A countdown (30 s to 4 h) that submits what is answered at zero. |
| off | A multi-select earns (right − wrong) / correct instead of nothing. |
Essay and file answers land in Quiz answers to grade (GET …/course/quiz-reviews, PATCH …/quiz-reviews/{attemptId}). Per-question analytics are at GET …/course/quizzes/{quizId}/analytics, and questions can be imported from CSV or GIFT (POST …/course/authoring/quiz-import). Webhooks gain quiz.failed and quiz.review_needed.
Lesson kinds
A lesson is still a page whose prose is its body; its kind adds the thing course platforms have a lesson type for, and the kind's requirement is what Complete waits for, checked on the server. Set it in the studio under Lesson types, or as metadata.lesson on the page.
| What it adds | Complete when |
|---|---|---|
| Nothing (the default). | The learner marks it. |
|
|
|
|
| Marked. |
| A PDF read in place, with | Marked. |
| Up to 300 slide images with alt text. | Marked. |
| Up to 600 page images. | Marked. |
|
| Handed in, or graded at or above |
| A | The learner commented on the lesson |
| One of your forms ( | The form is sent. |
|
| Checked in at that event, when one is linked; otherwise marked. |
| One of the course's quizzes ( | Marked; the quiz records its own attempts. |
| A SCORM, xAPI or cmi5 package uploaded to the lesson ( | The package reports completion. |
minLevel on any lesson holds it until the learner reaches that community level. GET …/course/lessons/{slug}/requirement tells a learner where they stand and why not yet. Assignments are graded in Grading (GET …/course/assignments, PATCH …/assignments/{id} with grade, feedback and graded or returned). Wistia and Bunny video links embed like YouTube and Vimeo.
Captions can be transcribed for a video uploaded to the course (POST …/course/lessons/{slug}/captions/generate) with your organization's own speech-to-text provider; see What you need to set up.
Learners
A learner is enrolled the first time they open a lesson (or press Start), and the course remembers the lesson they were on, so its button reads Start, Continue or Review. A reader who is not signed in can still mark lessons done; that progress is kept in their browser and added to their account when they sign in.
Call | What it does |
|---|---|
| Enrol, or record the lesson the learner is on. |
| Add progress made signed out. |
| The signed-in learner's courses in the organization. |
| Every course, score and certificate, as a transcript. |
| The roster: progress, last active, attempts, certificate. |
| One learner's record. |
| Started → each lesson → completed, steepest drop marked. |
| Revoke ( |
The roster, funnel and rules need courses.manage (Maintainers and Admins hold it; PAT scope courses:manage), because they are personal data about learners. A revoked certificate's verify page and PDF answer 410. An owner can also mark a lesson done for a learner, reset their progress, issue a certificate by hand, or unenrol them.
Learner rules (settings.courseLearning, studio: Learner email & XP):
Key | Default | What it does |
|---|---|---|
|
| Email |
|
| Email |
|
| One |
|
| Progression XP for a lesson (or module assessment); |
|
| Progression XP for finishing the course; |
Webhooks: course.enrolled, lesson.completed, quiz.passed, quiz.failed, quiz.review_needed, course.completed, certificate.issued.
What a learner gets
Beside the lesson: private notes and bookmarks (lesson.notes), a Focus button that hides everything but the lesson, previous / complete / next pinned to a phone's screen (lesson.phoneNav), the next video opening after a short countdown (lesson.autoplayNext), a print view, and Save for offline (lesson.offline), which keeps the lesson and its media on the device unless the lesson forbids downloads. Course news can arrive as a push notification.
A learner can also take the course's audio and video lessons as a private podcast feed (POST …/course/feed answers its address): any podcast app can follow it, it shows only what that learner may open, and resetting it kills the old address. Learners write reviews (1–5 stars; the landing shows them and an aggregate rating), ask the instructor a question about a lesson, see a course leaderboard, and, with directory on, who else is taking the course.
A learner's place in each lesson video is kept on their account (GET/PUT …/course/lessons/{slug}/media); opening a lesson records the day toward a streak; GET …/course/me/stats?tz= answers the streak, the XP and the last 14 days. Whether any of it is drawn is theme.courses.gamification (streaks, xp, mastery, celebration), all off unless you or the path preset turn them on.
Access, pacing and enrolment
settings.courseAccess (studio: Access & pacing; GET/PUT …/course/authoring/access, courses.manage) holds two kinds of rule, and the difference matters:
Key | Default | Kind | What it does |
|---|---|---|---|
|
| access |
|
|
| pacing | Each entry names a |
|
| pacing |
|
|
| pacing | The course's linked prerequisite courses must be completed first. |
Access rules hold the lesson's body back everywhere: the page, its Markdown, PDF and comments answer 403 course.locked, and search, export, llms-full.txt and MCP leave the lesson out. Pacing rules lock the lesson in the reader and refuse progress on it, but are not secrecy. A lesson whose freePreview field is on is open to everyone under every rule.
The same object carries the enrolment rules. Every key unset is the old behaviour: always open, unlimited, for ever, every lesson.
Key | Default | What it does |
|---|---|---|
|
| New learners from this UTC day. |
|
| No new learners after the end of this UTC day. Learners who started keep going. |
|
| At most this many learners start. |
|
| A learner's access ends this many days after they start (lock reason |
|
| What finishing means; the certificate and the completion webhook follow it. |
|
| A certificate reads "Expired" after this many months (recertification). |
|
| A full or closed course takes names instead of refusing them. |
|
| A learner whose verified address is at one of these domains is enrolled (and given a seat in a paid course) the first time they open it. |
|
| Learners see who else is taking the course (their cohort only, when they have one). |
The outline reports enrolment.state as open, early, closed or full (with seatsLeft on a capped course), judged by the database clock.
Cohorts, waitlist and seats
In the studio this is Cohorts and seats.
Cohorts (
POST …/course/cohorts):name,startsOn,endsOn, an enrolment window (enrolOpensAt,enrolClosesAt), aseatCap, andchatfor a private discussion among its members. Readers see open cohorts on the landing (GET /public/…/course/cohorts) and join one; staff move a learner between cohorts.Waitlist (
GET …/course/waitlist,POST …/waitlist/{entryId}/admit): when a place opens, the learner is mailedcourse.waitlist-open.Bulk enrol (
POST …/course/enrolments/bulk): up to 5,000 addresses, optionally into a cohort, with a seat in a paid course (grant, default on) and a welcome mail (notify, default on). Addresses with no account are invited to sign up.Seat groups for a company buying places (
POST …/course/seat-groups):name,seats, amanagerEmail. People join with the group's code (POST …/course/seats/join); the manager invites people and sees their progress (GET …/course/seats/managed).
The older cohort mode in course details still works: cohort.startsOn makes every module a week from that day, and cohort.eventsSpaceId lists an EVENTS space's next five events on the landing. Course details also carry topics (catalogue filter words) and testimonials.
Building a course faster
Build faster in the studio holds the shortcuts:
Call | What it does |
|---|---|
| Type an outline ( |
| Draft an outline from a |
| Read a Teachable or Thinkific export, or a CSV ( |
| Up to 100 videos into a module, one lesson each. |
| Copy another course's lessons into this one. |
| The course as a SCORM 1.2 package for another LMS. |
See Importers for the import formats. Other languages holds the landing's words in other languages; a reader on that language's address sees them over the originals.
Selling a course
Connect the organization's own Stripe account first, the same connection event tickets use (organization settings → Payments; see Payments and the shop). The money goes to your balance and the keys are write-only.
settings.courseCommerce (studio: Price & coupons and Checkout; PUT …/course/authoring/commerce):
Key | Default | What it does |
|---|---|---|
| off, 0, | Sell the course. Lessons that are not free previews open only to a learner holding a seat, on every surface ( |
| 30 | The self-refund window; 0 = only you refund. |
|
| Your terms, under the buy button. |
| on | Checkout offers a code field. |
| off | Stripe Tax computes and adds VAT or sales tax (and asks for a billing address). |
| off | Ask for a billing address. |
| off | A numbered Stripe invoice (PDF) for every one-time purchase. |
|
| Up to 10 questions of your own ( |
|
| Where a buyer lands after paying, instead of the course. |
| off | A money-back guarantee badge beside the button. |
| off | "Buy as a gift": the recipient redeems a 12-character code ( |
| off | Offer PayPal beside the card checkout, for one-time purchases. |
| 0 | Mail |
| off | Honour affiliate links ( |
POST …/course/checkout answers a Stripe Checkout address (or a seat at once when a code covers the price). The organization's one Stripe webhook, /public/payments/stripe/{orgId}/webhook, shared with event tickets, grants the seat to a buyer who closed the tab and closes the course after a refund made in Stripe.
Offers (studio: Offers and codes; POST …/course/offers) are several ways to buy one course:
|
| Also takes |
|---|---|---|
| the price | |
| each instalment |
|
| each period |
|
| the suggestion |
|
Any offer can carry regional prices by two-letter country code, features for the pricing table, highlighted, an order bump ({ name, priceCents } ticked on the checkout and added to the same charge) and a one-click upsell of another course (upsellSpaceId, upsellCents) offered right after the purchase. A learner on a plan or subscription cancels or resumes it themselves (POST …/course/purchase/cancel, …/resume) and reads their invoices (GET …/course/purchase/invoices).
Bundles and affiliates are organization-wide (/orgs/{orgId}/course-store/…): a bundle sells 2–50 courses at one price and shows the saving; an affiliate has a code, a commission (default 20 %), and counts of clicks, sales and what you paid them. POST …/course/coupons/bulk makes up to 1,000 single-use codes PREFIX-XXXXXX. Instructors can take a revenue share (PUT …/course/instructors) and you record payouts to instructors or affiliates (POST …/course/payouts). Revenue (GET …/course/revenue, and revenue/export.csv) shows gross, refunds, net, tax, commission, active subscriptions, monthly recurring revenue, and sales by day, offer, coupon and affiliate, with abandoned checkouts and conversion. Buyers lists every seat; you refund one or grant a free one. Webhooks: course.purchased, course.refunded.
Do not confuse your Stripe webhook with POST /extensions/stripe/webhook. That one is Forumulate's own receiver for marketplace purchases, so there is nothing for you to configure, and course seats and event tickets never go through it.
Mail, reports and automation
Course mail in the studio:
Broadcasts (
POST …/course/broadcasts): a subject and body toall,active,completed,not_startedor onecohort, now or atsendAt.Sequences (
POST …/course/sequences): steps of{ delayDays, subject, body }started byenrolled,completed,purchasedorinactive.Unlock mail: a learner is mailed
course.unlockedwhen a dripped lesson opens for them.The email tool sync (below).
Reports: a video heatmap per lesson (GET …/course/reports/video: the average share watched and, per 1 %, viewers and plays), a compliance report with its CSV (…/reports/compliance, …/compliance.csv), and scheduled report mail, weekly or monthly, to up to 20 addresses (POST …/course/report-schedules).
The courses group of email templates gains course.assignment-graded, course.unlocked, course.broadcast, course.sequence, course.receipt, course.cart-reminder, course.question-answered, course.waitlist-open, course.report, course.gift and course.seat-invite.
The landing
A/B tests and embeds in the studio:
Landing variants (
POST …/course/landing-variants): aname, aweightand your ownheadline,subtitleandctaLabel. Readers are split by weight; each variant counts views and conversions.The course card and buy button:
<iframe src="/co/{orgId}/{spaceId}/embed?embed=1">on your own site shows the cover, name, promise and price, and a button that opens the course and its checkout in a new tab.
landing.countdown counts down to the next deadline the course has (enrolment closing or a cohort's start). The landing's structured data gains the pricing table's offers and, with reviews, an aggregate rating. When your site loads Meta Pixel or GA4 (Analytics), purchases and enrolments are reported as their standard conversion events.
How a course reads: theme.courses
Every key is optional and layers theme → space → lesson. Change them with PATCH /orgs/{orgId}/spaces/{spaceId}/theme or in the studio under Design → Courses: every setting. A lesson is a docs page, so its page furniture (the crumb, the contents rail, "Was this helpful?") is theme.docs; what docs has no word for is here.
Under your keys sit the default layer (a hero with the facts and the cover, the curriculum in the lesson's rail, "Complete and continue", a "Next up" card, a brand-coloured certificate), then the preset (standard, tutorial, video, cohort, path; PUT …/preset), then the look.
Looks are patches you can mix
A look is a patch of visual keys that no preset sets, so any look composes with any preset (school on cohort is the cohort layout in school dress). Where a look and a preset both named a key, the preset would win; there is no such key today.
| What it sets |
|---|---|
| The default layer only (unset means this). |
| No default layer: the course as it was before it had one. |
|
|
|
|
|
|
Three combinations no template ships:
{ "courses": { "look": "academy", "certificate": { "layout": "classic", "border": "double" } } }Kajabi's full-width landing, rings and one-question-at-a-time quizzes, with a traditional diploma.
{ "courses": { "look": "minimal-text", "progress": { "style": "ring" }, "quiz": { "options": "tiles" } } }A quiet, narrow text course whose progress and quizzes are still friendly.
{ "courses": { "look": "school", "landing": { "countdown": false }, "lock": { "card": "banner", "showOutline": true } } }The school dress without the countdown, where a locked lesson shows a banner and the outline of what opens.
Keys
The landing, outline, lesson, progress, certificate and video families:
Key | Values (default) | What it does |
|---|---|---|
|
| The landing's arrangement. |
|
| The start button. |
|
| The cover. |
|
| How wide the landing runs. |
|
| The start button on a phone. |
| boolean | A countdown to enrolment closing or a cohort's start. |
| on | Parts of the landing. |
|
| How the lessons list. |
|
| Lesson numbers. |
|
| Row spacing; the |
| booleans | What each row shows. |
|
| The course rail. |
|
| Where the course's percent shows. |
|
| Completing and moving on. |
|
| The lesson column. A lesson's own width setting beats it. |
| booleans | Lesson furniture (above). |
|
| Progress bars and done marks. |
| on, on, | The player. |
|
| How a locked lesson shows in the outline. |
| booleans | A cohort's week headings and live sessions. |
|
| Testimonials and the certificate's share row. |
| booleans; | How a paid course shows its price. |
| off; | The game layer. |
The surfaces that had no keys before:
Key | Keys and values |
|---|---|
|
|
|
|
My learning | The |
| A path's page: |
|
|
| A locked lesson: |
The certificate
theme.courses.certificate, read by the print view and the PDF alike:
Key | Values (default) | What it does |
|---|---|---|
|
| The design. |
|
| The page, in print and in the PDF. |
|
| The type. |
|
| The frame. |
| colours (brand; white; near-black) | Heading and frame; the paper; the text. |
| on | The site's logo. |
| none | Artwork behind it (an uploaded image). |
| off | A QR code of the verify address, drawn locally. |
| off | The learner's average quiz score; the course's length. |
| none |
|
| none | A line of your own under the course title. |
|
| The certificate page's width. |
| none | Where each part sits (see below). |
Certificate designer in the studio is drag-and-drop: it writes positions, the centre of each part (logo, heading, name, course, line, issued, signature, verify, qr) in percent of the page. A part with no position stays where the layout puts it. The certificate is written in the learner's language, carries its expiry when certificateValidMonths is set, and offers Add to LinkedIn and Copy link. Finishing every course in a published learning path earns a path certificate with its own verify page.
One lesson, its own look
A lesson's customization takes the courses namespace (other type namespaces are refused with a 422 that names them). Lessons wear the course front page's piece customization under their own, so a course styled once on its landing carries that look into every lesson; SEO and JSON-LD are not inherited.
{ "customization": { "theme": { "courses": { "lesson": { "width": "wide", "sidebar": "drawer" } } } } }Theme outlets
Outlet | Where |
|---|---|
| The course index, above the lessons. |
| The landing, under the hero. |
| A lesson, between its title and its body. |
| A lesson, under its body and completion controls. |
| A certificate page, under the certificate. |
See Theme components.
Catalogue and learning paths
GET /public/orgs/{orgId}/courses lists the organization's public courses as catalogue cards, and …/catalog is their page (theme.courses.catalog). A spaceFeed block with "source": "courses" draws them on any front page, sorted latest, oldest or top, filterable with facets: ["level", "topic"].
A learning path is courses in an order. It is organization-level, because it crosses course spaces, and managing paths needs courses.manage (studio: Learning paths):
curl -X POST https://forumulate.com/api/orgs/$ORG/course-paths \
-H "Authorization: Bearer $FORUMULATE_TOKEN" -H "Content-Type: application/json" \
-d '{ "slug": "frontend-foundations", "title": "Frontend foundations",
"courses": ["<courseSpaceId>", "<courseSpaceId>"], "published": true }'slug is lowercase letters, digits and dashes (up to 80); courses is one to thirty course space ids in order. An unpublished path is your draft. A path has its own page at …/path/{slug}; My learning shows a bar across each path (GET /orgs/{orgId}/me/course-paths).
LTI 1.3 Advantage
Register your LMS as described in Single sign-on, and a course launches straight from it with the learner signed in. On top of the launch:
Your tool key. Each organization gets its own RS256 key on first need; the LMS reads the public half from
/lti/{orgId}/jwks.GET /orgs/{orgId}/ltinow answersjwksUrlanddeepLinkUrlto give the LMS.Grade passback. Set
authTokenUrl(the LMS's OAuth token endpoint) onPUT /orgs/{orgId}/lti. A learner who arrived with a line item has each finished lesson sent back as a score, and finishing the course sends 100, "Completed". A failure is logged, never shown to the learner.Deep linking. An instructor adding Forumulate in the LMS picks courses in a picker (
/o/{orgId}/lti-deep-link); they come back as resource links with a 100-point line item each.
What you need to set up
Feature | What you need | Where | Until then |
|---|---|---|---|
Selling a course | Your own Stripe account. | Organization settings → Payments (Payments and the shop). | The course is free; |
Stripe Tax ( | Stripe Tax switched on in your own Stripe dashboard. | Stripe, then Checkout. | Stripe will not open a checkout with automatic tax until it is set up; leave |
PayPal | A PayPal REST app's client id and secret, | Organization settings → Payments ( | Learners do not see the PayPal button. One-time purchases only. |
Mailchimp or Kit sync | A Mailchimp API key and audience id, or a Kit (ConvertKit) API key and tag. | Course mail ( | Nothing is sent. Learners are added on |
LTI grade passback | Your LMS registration plus its | Launches work; no grades go back. | |
AI outline, AI captions | An AI provider for the organization; captions need one with speech-to-text (OpenAI). | Settings → AI (AI and search). | The buttons answer that no provider is connected. Captions you upload still work. |
Meta Pixel / GA4 | Your pixel or measurement id. | No conversions are reported. |
The Zapier app turns course webhooks into Zapier triggers and adds two actions, enrol people (optionally with a seat and a cohort) and create a coupon; the WordPress plugin puts the course card and buy button on a WordPress site. See Zapier, Terraform, React and WordPress.
In the API
Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.
Course
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the outline in a space |
| |
| Complete the lesson |
| |
| Replace the enrolment in a space |
| |
| Merge the progress |
|
Course Certificate Public
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Verify a course certificate |
| |
| Get the PDF in a space (course certificate) |
|
Course Learners
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Replace the certificate in a space |
| |
| List learners in a space |
| |
| File of learners |
| |
| Get the funnel in a space |
| |
| Get the learning in a space |
| |
| Replace the learning in a space |
|
Course Quiz
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the quiz in a space (authoring lessons) |
| |
| Replace the quiz in a space (lessons) |
| |
| Get the quiz in a space (authoring modules) |
| |
| Replace the quiz in a space (modules) |
| |
| Delete the quiz |
| |
| Get the quiz policy in a space |
| |
| Replace the quiz policy in a space |
| |
| List details in a space |
| |
| Replace details in a space |
| |
| Get the quiz in a space (lessons) |
| |
| Get the quiz |
| |
| Create an attempt in a space |
| |
| Get the certificate in a space (course) |
|
Course Reports
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the video in a space |
| |
| Get the compliance in a space |
| |
| File of compliance |
| |
| Get the detail in a space |
|
Lti Admin
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the LTI in an organization |
| |
| Replace the LTI in an organization |
|
My Course Paths
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List course paths in an organization (me) |
|
My Learning
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List courses in an organization (me) |
|
My Transcript
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the learning transcript in an organization |
|
Org Paypal
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the paypal in an organization |
| |
| Replace the paypal in an organization |
| |
| Delete the paypal in an organization |
| |
| Test the paypal |
|
Public Course Catalog
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List courses in an organization (public reader) |
| |
| List course paths in an organization (public reader) |
|
Public Course Cohorts
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List cohorts in a space (public reader) |
|