Courses spaces

Embed
Preview of the card
<a href="https://forumulate.com/guides/courses-spaces"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/30ca9eb5-edc3-4003-a75e-a5af764c8607/social-card.svg?v=3e7ae33eec8b" alt="Courses spaces" width="600" height="315"></a>

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

/co/{orgId}/{spaceId}

The landing: the hero, the facts, the start button and the outline.

…/{lesson-slug}

A lesson, with the course rail beside it.

…/module/{module-slug}

A module's own page: its cover, description, lessons and progress.

…/module/{module-slug}/quiz

A module assessment.

…/me

My learning: the learner's courses, paths and certificates.

…/catalog

The organization's catalogue of public courses and published paths.

…/path/{path-slug}

A learning path's page.

…/checkout

Where Stripe returns a buyer.

…/certificate/{verifyId}

A certificate's public verify page (and /pdf).

/co/paths/{orgId}/certificate/{verifyId}

A learning-path certificate's verify page.

…/embed?embed=1

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

GET /orgs/{orgId}/spaces/{spaceId}/course/outline

The modules and their lessons, in order.

PUT /orgs/{orgId}/spaces/{spaceId}/course/authoring/lessons/{pageId}/quiz

Attach or replace a lesson's quiz.

PUT /orgs/{orgId}/spaces/{spaceId}/course/authoring/modules/{collectionId}/quiz

A module assessment.

GET /orgs/{orgId}/spaces/{spaceId}/course/quizzes/{quizId}/attempts

Attempts on a quiz.

GET /orgs/{orgId}/spaces/{spaceId}/course/certificate

The learner's certificate.

GET /public/orgs/{orgId}/spaces/{spaceId}/course/certificate/{verifyId}/pdf

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:

type

What the learner does

How it is marked

choice

Picks one or several of choices (the default).

Against correctIndexes.

truefalse

Picks True or False.

Against correctIndexes.

short

Types an answer.

Matched against accepted, ignoring case and spacing.

essay

Writes free text.

By a person: the attempt waits for review.

file

Uploads up to five files.

By a person.

ordering

Puts choices in their right order.

Automatically.

matching

Pairs each of choices with its matches entry.

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

shuffleQuestions

off

A new question order per attempt.

shuffleChoices

off

A new choice order per attempt.

poolSize

none

Each attempt draws this many questions from the bank, and is graded on them.

timeLimitSeconds

none

A countdown (30 s to 4 h) that submits what is answered at zero.

partialCredit

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.

kind

What it adds

Complete when

standard

Nothing (the default).

The learner marks it.

video

url, an optional hlsUrl stream, poster, up to 20 captions, up to 50 timeline cues (a question the video pauses on), watermark (the learner's address drifting over the picture), allowDownload.

requireWatchedPercent of it is watched.

audio

url, a transcript, allowDownload.

Marked.

pdf

A PDF read in place, with height and allowDownload.

Marked.

slides

Up to 300 slide images with alt text.

Marked.

flipbook

Up to 600 page images.

Marked.

assignment

instructions, allowFiles, allowText, dueDays.

Handed in, or graded at or above passGrade.

discussion

A prompt.

The learner commented on the lesson minPosts times.

survey

One of your forms (formId).

The form is sent.

live

startsAt, durationMinutes, a joinUrl, a recordingUrl, or an event in an EVENTS space (eventSpaceId, eventSlug).

Checked in at that event, when one is linked; otherwise marked.

exam

One of the course's quizzes (quizId), shown as the lesson.

Marked; the quiz records its own attempts.

package

A SCORM, xAPI or cmi5 package uploaded to the lesson (POST …/course/lessons/{slug}/package).

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

PUT /orgs/{orgId}/spaces/{spaceId}/course/enrolment

Enrol, or record the lesson the learner is on.

POST /orgs/{orgId}/spaces/{spaceId}/course/progress/merge

Add progress made signed out.

GET /orgs/{orgId}/me/courses

The signed-in learner's courses in the organization.

GET /orgs/{orgId}/me/learning-transcript

Every course, score and certificate, as a transcript.

GET /orgs/{orgId}/spaces/{spaceId}/course/learners (and learners.csv)

The roster: progress, last active, attempts, certificate.

GET /orgs/{orgId}/spaces/{spaceId}/course/learners/{userId}/detail

One learner's record.

GET /orgs/{orgId}/spaces/{spaceId}/course/funnel

Started → each lesson → completed, steepest drop marked.

PUT /orgs/{orgId}/spaces/{spaceId}/course/learners/{userId}/certificate

Revoke ({"revoked":true}) or restore a certificate.

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

welcomeEmail

true

Email course.welcome when a learner enrols.

completionEmail

true

Email course.completed, with the certificate link, on completion.

nudgeAfterDays

null

One course.nudge reminder after this many days away; null = off.

xpPerLesson

5

Progression XP for a lesson (or module assessment); 0 = none.

xpPerCourse

50

Progression XP for finishing the course; 0 = none.

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

audience

everyone

access

signedIn or groups (with groups: [groupId]): lessons are for those readers only.

drip[]

[]

pacing

Each entry names a module or a lesson and opens it on a date or afterDays after the learner starts. A lesson drip releases "a lesson a day" inside an open module.

order

free

pacing

linear: each lesson opens when the one before it (or the module's assessment) is complete.

requirePrerequisites

false

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

enrolOpensOn

null

New learners from this UTC day.

enrolClosesOn

null

No new learners after the end of this UTC day. Learners who started keep going.

seatCap

null

At most this many learners start.

accessDays

null

A learner's access ends this many days after they start (lock reason expired).

completion

{ "minPercent": 100, "requireQuizzes": false }

What finishing means; the certificate and the completion webhook follow it.

certificateValidMonths

null

A certificate reads "Expired" after this many months (recertification).

waitlist

false

A full or closed course takes names instead of refusing them.

autoEnrolDomains

[]

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.

directory

false

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), a seatCap, and chat for 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 mailed course.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, a managerEmail. 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

POST …/course/authoring/outline

Type an outline (modules of lessons, each with an optional body and video) and get every page at once.

POST …/course/authoring/ai-outline

Draft an outline from a topic, audience, module and lesson counts and a language, with your AI provider.

POST …/course/authoring/import

Read a Teachable or Thinkific export, or a CSV (format, content).

POST …/course/authoring/bulk-videos

Up to 100 videos into a module, one lesson each.

POST …/course/authoring/copy-from

Copy another course's lessons into this one.

GET …/course/export/scorm

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

enabled, priceCents, currency

off, 0, usd

Sell the course. Lessons that are not free previews open only to a learner holding a seat, on every surface (course.locked, reason purchase).

refundDays

30

The self-refund window; 0 = only you refund.

refundText

""

Your terms, under the buy button.

coupons

on

Checkout offers a code field. ?coupon=CODE on any course address fills it in.

tax

off

Stripe Tax computes and adds VAT or sales tax (and asks for a billing address).

collectBilling

off

Ask for a billing address.

invoices

off

A numbered Stripe invoice (PDF) for every one-time purchase.

checkoutFields

[]

Up to 10 questions of your own (text, textarea, select, checkbox).

thankYouUrl

""

Where a buyer lands after paying, instead of the course.

moneyBackBadge

off

A money-back guarantee badge beside the button.

gifts

off

"Buy as a gift": the recipient redeems a 12-character code (POST …/course/gift/redeem).

paypal

off

Offer PayPal beside the card checkout, for one-time purchases.

abandonedCartHours

0

Mail course.cart-reminder this many hours after an unpaid checkout; 0 = never.

affiliates

off

Honour affiliate links (?ref=CODE).

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:

kind

priceCents is

Also takes

one_time

the price

plan

each instalment

installments (2–24) and interval (month, year).

subscription

each period

interval and an optional trialDays.

pwyw

the suggestion

minCents (0 allows free).

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 to all, active, completed, not_started or one cohort, now or at sendAt.

  • Sequences (POST …/course/sequences): steps of { delayDays, subject, body } started by enrolled, completed, purchased or inactive.

  • Unlock mail: a learner is mailed course.unlocked when 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): a name, a weight and your own headline, subtitle and ctaLabel. 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.

look

What it sets

default

The default layer only (unset means this).

paper

No default layer: the course as it was before it had one.

school

landing { ctaStyle: "solid", coverRadius: 16, width: "wide", countdown: true }; outline.density roomy; progress { style: "thick-rounded", doneMark: "fill" }; quiz { options: "tiles", result: "breakdown" }; the My learning page as cards; catalog { layout: "grid", columns: 3 }.

academy

landing { ctaStyle: "solid", coverRadius: 0, coverAspect: "wide", width: "full" }; outline { density: "cozy", cardColumns: 3 }; progress { style: "ring", doneMark: "check" }; quiz { layout: "one-at-a-time", showProgress: true, options: "tiles" }; certificate { layout: "modern", qr: true, font: "sans" }; My learning and paths as cards; catalog { layout: "grid", columns: 4 }.

minimal-text

landing { ctaStyle: "link", coverRadius: 0, width: "narrow" }; outline { density: "compact", showKinds: false }; progress { style: "thin", doneMark: "strike" }; lesson.width narrow; quiz.result pass-fail; certificate { layout: "minimal", border: "none", font: "serif" }; lock.card inline; catalog.layout list.

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

landing.layout

minimal, hero-split, hero-cover, cohort, path (hero-split)

The landing's arrangement.

landing.ctaStyle

solid, outline, link

The start button.

landing.coverAspect, coverRadius

video, classic, square, wide; 0–48 px or full

The cover.

landing.width

narrow, default, wide, full (default)

How wide the landing runs.

landing.phoneCta

inline, sticky

The start button on a phone.

landing.countdown

boolean

A countdown to enrolment closing or a cohort's start.

landing.showStats, showInstructors, showOutcomes, showPrereqs, showPaths

on

Parts of the landing.

outline.style

checklist, accordion, numbered-chapters, timestamp-table, path-nodes, cards

How the lessons list.

outline.numbering

none, 01, 1.1

Lesson numbers.

outline.density, cardColumns

compact, cozy, roomy (cozy); 2–4

Row spacing; the cards style's columns.

outline.showDurations, showKinds, showModuleProgress, collapse

booleans

What each row shows.

lesson.sidebar

curriculum, drawer, none

The course rail.

lesson.progressBar

top, sidebar, none

Where the course's percent shows.

lesson.completion, endCard

separate, complete-and-continue; none, completed-next

Completing and moving on.

lesson.width

narrow, default, wide, full

The lesson column. A lesson's own width setting beats it.

lesson.showUpdated, showCover, printButton, focusButton, phoneNav, notes, offline, autoplayNext, chapterBadge, objectivesBox

booleans

Lesson furniture (above).

progress.style, doneMark, tone

thin, thick-rounded, ring; check, fill, strike

Progress bars and done marks.

video.speed, resume, transcript, completeAt

on, on, collapsed/open, 10–100 %

The player.

access.lockStyle, showDripDates

icon, dim, hide

How a locked lesson shows in the outline.

cohort.weekHeadings, showSessions

booleans

A cohort's week headings and live sessions.

social.testimonials, linkedIn, shareLink, lessonComments

cards, spotlight

Testimonials and the certificate's share row.

commerce.priceOnButton, refundNote, couponField

booleans; folded, open

How a paid course shows its price.

gamification.streaks, xp, mastery, celebration

off; off, card, confetti

The game layer.

The surfaces that had no keys before:

Key

Keys and values

module

layout (list, cards), showCover, showDescription, showProgress (all shown).

quiz

layout (all, one-at-a-time), options (list, tiles), result (score, pass-fail, breakdown), showProgress, showTimer (shown).

My learning

The /me page's own group (named in the studio under Courses: every setting): layout (list, cards), showPaths, showCertificates (shown).

paths

A path's page: layout (steps, cards), showCover, showFacts (shown).

catalog

layout (grid, list), columns (2–4, 3), sort (manual, newest, title, popular), filters (level, topic), showLevel, showDuration, showInstructors, showPaths.

lock

A locked lesson: card (card, inline, banner), showPreviewCta (shown), showOutline (off).

The certificate

theme.courses.certificate, read by the print view and the PDF alike:

Key

Values (default)

What it does

layout

classic, modern, minimal

The design.

orientation

landscape, portrait (landscape)

The page, in print and in the PDF.

font

serif, sans, display

The type.

border

double, single, none

The frame.

accent, paper, ink

colours (brand; white; near-black)

Heading and frame; the paper; the text.

logo

on

The site's logo.

backgroundUrl

none

Artwork behind it (an uploaded image).

qr

off

A QR code of the verify address, drawn locally.

showScore, showHours

off

The learner's average quiz score; the course's length.

signature

none

{ name, title, imageUrl }.

line

none

A line of your own under the course title.

width

default

The certificate page's width.

positions

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

courses-above-list

The course index, above the lessons.

courses-landing-hero-after

The landing, under the hero.

courses-lesson-above-body

A lesson, between its title and its body.

courses-lesson-after-body

A lesson, under its body and completion controls.

courses-certificate-footer

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}/lti now answers jwksUrl and deepLinkUrl to give the LMS.

  • Grade passback. Set authTokenUrl (the LMS's OAuth token endpoint) on PUT /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; enabled has nothing to charge with.

Stripe Tax (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 tax off.

PayPal

A PayPal REST app's client id and secret, live or sandbox.

Organization settings → Payments (PUT /orgs/{orgId}/payments/paypal with clientId, secret, mode, enabled; POST …/test checks it).

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 (PUT …/course/email-sync/{mailchimp|convertkit} with apiKey, listId, tag, onEvents, enabled).

Nothing is sent. Learners are added on enrolled (and completed if you choose); Mailchimp members are added as transactional, tagged with your tag.

LTI grade passback

Your LMS registration plus its authTokenUrl.

Single sign-on.

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.

Analytics.

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

Course_outline

GET /orgs/{orgId}/spaces/{spaceId}/course/outline

Get the outline in a space

forumulate course outline<br>client.courses.outline()

Course_complete

PUT /orgs/{orgId}/spaces/{spaceId}/course/lessons/{slug}/complete

Complete the lesson

forumulate course complete<br>client.courses.complete()

Course_enrol

PUT /orgs/{orgId}/spaces/{spaceId}/course/enrolment

Replace the enrolment in a space

forumulate course enrol<br>client.courses.enrol()

Course_merge

POST /orgs/{orgId}/spaces/{spaceId}/course/progress/merge

Merge the progress

forumulate course progress merge<br>client.courses.merge()

Course Certificate Public

Operation

Request

What it does

Shortcuts

CourseCertificatePublic_verify

GET /public/orgs/{orgId}/spaces/{spaceId}/course/certificate/{verifyId}

Verify a course certificate

client.courses.publicCertificate.verify()

CourseCertificatePublic_pdf

GET /public/orgs/{orgId}/spaces/{spaceId}/course/certificate/{verifyId}/pdf

Get the PDF in a space (course certificate)

client.courses.publicCertificate.pdf()

Course Learners

Operation

Request

What it does

Shortcuts

CourseLearners_revoke

PUT /orgs/{orgId}/spaces/{spaceId}/course/learners/{userId}/certificate

Replace the certificate in a space

client.courses.learners.revoke()

CourseLearners_roster

GET /orgs/{orgId}/spaces/{spaceId}/course/learners

List learners in a space

client.courses.learners.roster()

CourseLearners_rosterCsv

GET /orgs/{orgId}/spaces/{spaceId}/course/learners.csv

File of learners

client.courses.learners.rosterCsv()

CourseLearners_funnel

GET /orgs/{orgId}/spaces/{spaceId}/course/funnel

Get the funnel in a space

client.courses.learners.funnel()

CourseLearners_learning

GET /orgs/{orgId}/spaces/{spaceId}/course/authoring/learning

Get the learning in a space

client.courses.learners.learning()

CourseLearners_setLearning

PUT /orgs/{orgId}/spaces/{spaceId}/course/authoring/learning

Replace the learning in a space

client.courses.learners.setLearning()

Course Quiz

Operation

Request

What it does

Shortcuts

CourseQuiz_authorLessonQuiz

GET /orgs/{orgId}/spaces/{spaceId}/course/authoring/lessons/{pageId}/quiz

Get the quiz in a space (authoring lessons)

client.courses.quiz.authorLessonQuiz()

CourseQuiz_saveLessonQuiz

PUT /orgs/{orgId}/spaces/{spaceId}/course/authoring/lessons/{pageId}/quiz

Replace the quiz in a space (lessons)

client.courses.quiz.saveLessonQuiz()

CourseQuiz_authorModuleQuiz

GET /orgs/{orgId}/spaces/{spaceId}/course/authoring/modules/{collectionId}/quiz

Get the quiz in a space (authoring modules)

client.courses.quiz.authorModuleQuiz()

CourseQuiz_saveModuleQuiz

PUT /orgs/{orgId}/spaces/{spaceId}/course/authoring/modules/{collectionId}/quiz

Replace the quiz in a space (modules)

client.courses.quiz.saveModuleQuiz()

CourseQuiz_deleteQuiz

DELETE /orgs/{orgId}/spaces/{spaceId}/course/authoring/quizzes/{quizId}

Delete the quiz

client.courses.quiz.deleteQuiz()

CourseQuiz_quizPolicy

GET /orgs/{orgId}/spaces/{spaceId}/course/authoring/quiz-policy

Get the quiz policy in a space

client.courses.quiz.quizPolicy()

CourseQuiz_setQuizPolicy

PUT /orgs/{orgId}/spaces/{spaceId}/course/authoring/quiz-policy

Replace the quiz policy in a space

client.courses.quiz.setQuizPolicy()

CourseQuiz_details

GET /orgs/{orgId}/spaces/{spaceId}/course/authoring/details

List details in a space

client.courses.quiz.details()

CourseQuiz_setDetails

PUT /orgs/{orgId}/spaces/{spaceId}/course/authoring/details

Replace details in a space

client.courses.quiz.setDetails()

CourseQuiz_lessonQuiz

GET /orgs/{orgId}/spaces/{spaceId}/course/lessons/{slug}/quiz

Get the quiz in a space (lessons)

client.courses.quiz.lessonQuiz()

CourseQuiz_quiz

GET /orgs/{orgId}/spaces/{spaceId}/course/quizzes/{quizId}

Get the quiz

client.courses.quiz.quiz()

CourseQuiz_submit

POST /orgs/{orgId}/spaces/{spaceId}/course/quizzes/{quizId}/attempts

Create an attempt in a space

client.courses.quiz.submit()

CourseQuiz_myCertificate

GET /orgs/{orgId}/spaces/{spaceId}/course/certificate

Get the certificate in a space (course)

client.courses.quiz.myCertificate()

Course Reports

Operation

Request

What it does

Shortcuts

CourseReports_video

GET /orgs/{orgId}/spaces/{spaceId}/course/reports/video

Get the video in a space

client.courses.reports.video()

CourseReports_compliance

GET /orgs/{orgId}/spaces/{spaceId}/course/reports/compliance

Get the compliance in a space

client.courses.reports.compliance()

CourseReports_complianceCsv

GET /orgs/{orgId}/spaces/{spaceId}/course/reports/compliance.csv

File of compliance

client.courses.reports.complianceCsv()

CourseReports_learner

GET /orgs/{orgId}/spaces/{spaceId}/course/learners/{userId}/detail

Get the detail in a space

client.courses.reports.learner()

Lti Admin

Operation

Request

What it does

Shortcuts

LtiAdmin_get

GET /orgs/{orgId}/lti

Get the LTI in an organization

client.sso.lti.get()

LtiAdmin_set

PUT /orgs/{orgId}/lti

Replace the LTI in an organization

client.sso.lti.set()

My Course Paths

Operation

Request

What it does

Shortcuts

MyCoursePaths_mine

GET /orgs/{orgId}/me/course-paths

List course paths in an organization (me)

client.courses.myPaths.mine()

My Learning

Operation

Request

What it does

Shortcuts

MyLearning_mine

GET /orgs/{orgId}/me/courses

List courses in an organization (me)

client.courses.myLearning.mine()

My Transcript

Operation

Request

What it does

Shortcuts

MyTranscript_transcript

GET /orgs/{orgId}/me/learning-transcript

Get the learning transcript in an organization

client.myTranscript.transcript()

Org Paypal

Operation

Request

What it does

Shortcuts

OrgPaypal_status

GET /orgs/{orgId}/payments/paypal

Get the paypal in an organization

client.orgs.paypal.status()

OrgPaypal_set

PUT /orgs/{orgId}/payments/paypal

Replace the paypal in an organization

client.orgs.paypal.set()

OrgPaypal_clear

DELETE /orgs/{orgId}/payments/paypal

Delete the paypal in an organization

client.orgs.paypal.clear()

OrgPaypal_test

POST /orgs/{orgId}/payments/paypal/test

Test the paypal

client.orgs.paypal.test()

Public Course Catalog

Operation

Request

What it does

Shortcuts

PublicCourseCatalog_courses

GET /public/orgs/{orgId}/courses

List courses in an organization (public reader)

client.courses.publicCatalog.courses()

PublicCourseCatalog_paths

GET /public/orgs/{orgId}/course-paths

List course paths in an organization (public reader)

client.courses.publicCatalog.paths()

Public Course Cohorts

Operation

Request

What it does

Shortcuts

PublicCourseCohorts_cohortsOpen

GET /public/orgs/{orgId}/spaces/{spaceId}/course/cohorts

List cohorts in a space (public reader)

client.courses.publicCohorts.cohortsOpen()

The Forumulate teamUpdated