Courses and memberships
Organizations can create draft courses, add root and nested modules (one nested level), author and reorder lessons, set drip days, and publish or unpublish any part. Authoring and enrolment use current crm:edit authority; reads use crm:read. All identities are scoped to the organization and course. A course may reference a commerce product in the same organization; this does not charge anyone or grant access automatically.
The course workspace offers a module/lesson tree, text and HTTPS video references, drip settings and publication controls. Its enrolments table shows each contact's progress and allows access revocation and link rotation. The member copies a learner link to share; GoodQA sends no email or SMS.
Only a published course accepts an active CRM contact. Repeated enrolment returns the existing active grant; after revocation a new enrolment is a fresh grant and starts its own progress. Archived and merged contacts are refused and their existing learner links stop resolving. Workflow enrol_in_course {courseId} supports the existing invoice_paid path. Dry run only records would_apply.
A learner link is /learn/cl1.<256 random bits in base64url>. It is a bearer secret with no readable organization or contact id. Only SHA-256 of the whole token is stored. Minting always rotates the link; revoked, malformed and replaced tokens answer the app's ordinary dark 404. Public API responses use no-store, no-referrer and noindex; public requests are limited to 120 per client per minute. The cl1. shape is scrubbed from telemetry. Learner requests carry no session cookies. The public page sets a no-referrer policy before requests or media loads.
An outline contains only published modules and published lessons whose ancestors are published. Locked lesson titles remain in the outline with a lock; their body, media and materials are never returned. Drip unlocks exactly at granted_at + drip_days * 24 hours using elapsed UTC time, including the boundary instant. Missing drip means day zero. Changing the drip setting affects subsequent requests.
Progress is completed published completable lessons / all published completable lessons, rounded down to whole percent. Drip-locked lessons remain in the denominator. Empty courses show 0%. Draft ancestors exclude their lessons. Quiz, assignment and funnel lessons retain their type and source content with a needs_build reason; learners see “Coming soon” and cannot complete them. A completion row is unique per grant and lesson. Completion and events share a transaction; concurrent retries insert one completion and publish one lesson_completed. The grant's completed_at latch permits only one course_completed event for that enrolment's lifetime, even if content is later added. Editing publication changes the denominator without erasing past progress.
The local GHL mapper consumes synthetic JSON matching the official Import Courses schema, researched 2026-10-22. It does not call this endpoint (which imports into GHL). Products become courses, categories become modules and subcategories become nested modules. Visibility, instructor details, video references and materials are retained. Course publication is inferred from whether any root category is published because the payload has no product visibility. Unsupported content is retained and reported. No media is downloaded. An advisory transaction lock and stable location/title course identity plus category-path and post-position fingerprints on modules and lessons make a repeated product import add nothing; imports are atomic, so a partially imported tree cannot survive. Titles identify products because this payload supplies no product ids; renamed products intentionally become new courses. Optional synthetic members must reference a course title in the same fixture and a live CRM contact. Import enrolment suppresses automation events so it has zero outbound effects.
Read tools course_list, course_read, course_enrolments and writes course_enrol, course_revoke use the same internal HTTP authorization path as the application. Returned author and import content is untrusted data. Link minting is intentionally absent from MCP tool output.
Composition is recorded in the registration proposal. ROOT must allocate the final additive migration ordinal, register the schema/routes/auth families/MCP tools/workflow types and telemetry scrubber, and mount the org and public pages. The author branch proves the literal hunks in a disposable overlay. No payments, deployments or provider transports are part of this feature.
Manual follow-ups: Paul's real course export and member list; customer media hosting; quiz/assignment/funnel engines. Attach this importer to the GHL migration envelope after switchover-core merges. HighLevel's own Kajabi importer documentation, modified 2026-03-13, also describes unsupported assessment imports; this implementation retains them for later building.