Course-builder API · v1
Create courses, modules and lessons from any application — an automation, your own backend, or an AI that generates a whole curriculum — with one authenticated call.
Everything lives under your institute’s own host. If your institute isacme.klickcourse.com, the API base is https://acme.klickcourse.com/api/v1. Create an API key in your admin (Admin → API keys), then:
curl https://acme.klickcourse.com/api/v1/admin/courses/import \
-H "Authorization: Bearer kc_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Intro to Widgets",
"slug": "intro-to-widgets",
"modules": [
{ "title": "Getting started", "lessons": [
{ "title": "Welcome", "type": "text", "contentHtml": "<p>Hello!</p>" }
]}
]
}'
# → 200 { "courseId": "…", "moduleCount": 1, "lessonCount": 1, "structure": {…} }That single call builds the whole course tree in one atomic transaction and returns it as a draft.
Send your key as a bearer token: Authorization: Bearer kc_…. The key is bound to the institute it was minted in, and it only works on that institute’s host — presenting it on another institute’s domain fails. Keys are shown once at creation and stored only as a hash; if one leaks, revoke it in your admin and it stops working immediately.
Keep keys server-side. Never ship a kc_ key in a browser, mobile app, or public repo.
Keys carry explicit scopes:
| Scope | Grants |
|---|---|
| course:write | Create courses, modules, lessons; update & publish a course; import a full tree; draft landing-page metadata. |
| crm:read | Read-only activity feeds under /admin/crm/* — enrollments, quiz attempts, certificates, course reviews — for CRM pipelines (n8n, Zapier, HubSpot, or an MCP-driven agent). |
Keys deliberately cannot write to students, enrollments, payments, certificates or account credentials — those stay behind an admin sign-in with passkey step-up.
Four institute-wide, cursor-style feeds power build-your-own CRM sync. Each returns rows oldest first; pass ?since=<ISO> (exclusive) with the last timestamp you processed, plus ?limit= (default 100, max 500).
GET /admin/crm/enrollments # cursor: created_at
GET /admin/crm/quiz-attempts # cursor: submitted_at (module checks + finals)
GET /admin/crm/certificates # cursor: issued_at (revoked excluded)
GET /admin/crm/reviews # cursor: updated_at (check "status" — edits & moderation re-emit)Prefer zero code? The built-in HubSpot integration (Admin → HubSpot) pushes the same data into HubSpot contacts automatically — these feeds are for custom pipelines and other CRMs.
POST /admin/courses/import — build a whole course in one transaction. Invalid input rolls the entire course back; nothing half-builds.
{
"title": "string (required)",
"slug": "lowercase-hyphenated (required, unique per institute)",
"priceMinor": "0", // minor units, string; default "0" (free)
"descriptionHtml": "<p>…</p>", // sanitized server-side
"maxAppliedCreditPct": 0,
"publish": false, // true = go live now; false = draft (default)
// landing page (all optional) — creates the course fully-formed
"coverUrl": "https://…/thumb.jpg", // course thumbnail (https image URL)
"subtitle": "One-line promise",
"instructorName": "…", "instructorBio": "…",
"instructorAvatarUrl": "https://…/avatar.jpg",
"skills": ["…"], "tools": ["…"],
"progressionMode": "open", // or "quiz_gated": module N+1 unlocks when
// module N is cleared (pass its check, or
// finish its lessons if it has no check)
"modules": [
{ "title": "Module title", "lessons": [
{
"title": "Lesson title",
"type": "video | audio | document | text",
"contentHtml": "<p>…</p>", // any type; below the player on video/audio.
// <a class="kc-cta" href> renders as a CTA button
"embedUrl": "https://www.youtube.com/embed/…", // YouTube/Vimeo allowlist
"videoUrl": "https://yoursite.com/clip.mp4", // self-hosted → Mux ingest (see below)
"durationSeconds": 300, // optional, non-negative integer
"isPreview": false
}
],
// optional module check — a short quiz owned by this module. Unlimited
// retakes, no certificate; under quiz_gated it unlocks the next module.
"quiz": {
"name": "Module 1 check", // default: "<module title> check"
"passThresholdPct": 80, // 1-100, default 80
"questions": [{
"prompt": "…",
"explanation": "Shown to the learner AFTER submitting — the why.",
"options": [{ "text": "Right", "isCorrect": true }, { "text": "Wrong" }]
}]
}
}
]
}Returns { courseId, moduleCount, lessonCount, moduleQuizzes, progressionMode, publishedVersion, structure }. Re-posting the same slug returns 409 Conflict, so retries are safe.
Self-hosted video? Pass videoUrl (a public https mp4) instead of embedUrl on a video lesson. Mux pull-ingests it into a signed, enrollment-gated, completion-tracked asset — the same first-class video as an upload. The import response adds an ingests array with each lesson’s status (processing until Mux finishes). You can also ingest into an existing lesson via POST /admin/lessons/{lessonId}/ingest-video. YouTube/Vimeo still use embedUrl. Existing lessons are editable with the key too: PATCH /admin/lessons/{lessonId} updates title, contentHtml (e.g. add a CTA button below a video), embedUrl, and isPreview.
Because the import endpoint takes a complete course tree as JSON, an AI can author the whole thing. Ask a model to produce the tree, then post it — no special endpoint required:
// 1) Ask an LLM for a course tree (pseudo)
const tree = await llm.json(`Design a course on "${topic}".
Return { title, slug, modules: [{ title, lessons: [{ title, type, contentHtml }] }] }`);
// 2) Import it
await fetch("https://acme.klickcourse.com/api/v1/admin/courses/import", {
method: "POST",
headers: { Authorization: `Bearer ${KC_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ ...tree, publish: false }), // review as draft, then publish
});Prefer publish:false so a human reviews the AI draft before it goes live. After import, call POST /admin/courses/{id}/generate-metadatato have KlickCourse draft SEO/landing copy from the course’s own content. A ready-made n8n recipe (webhook → import) ships with the platform.
Pass category: "AI READY" to file the course under a catalog section — the name is matched against your existing categories (case-insensitively) and created if it’s new, so re-running an import never spawns a duplicate section. Imported courses land at the END of the catalog order; move them with POST /admin/courses/{id}/move.
All paths are relative to https://<institute>.klickcourse.com/api/v1 and require the course:write key.
| Method | Path | Purpose |
|---|---|---|
| POST | /admin/courses/import | Atomic whole-tree create |
| POST | /admin/courses | Create a course |
| PATCH | /admin/courses/:id | Update a course |
| POST | /admin/courses/:id/modules | Add a module |
| POST | /admin/modules/:moduleId/lessons | Add a lesson |
| POST | /admin/courses/:id/generate-metadata | AI-draft landing copy |
| GET | /admin/courses/:id/structure | Read the course tree |
| POST | /admin/courses/:id/publish | Publish (go live) |
| POST | /admin/courses/:id/quiz | Create the course quiz |
| POST | /admin/modules/:moduleId/quiz | Create a module check |
| GET | /admin/courses/:id/module-quizzes | Read all module checks |
| POST | /admin/quizzes/:quizId/questions | Add a quiz question |
| PATCH | /admin/quizzes/:quizId/questions/reorder | Reorder questions |
| DELETE | /admin/quizzes/:quizId | Delete a module check |
| GET | /admin/courses/:id/quiz-attempts | Poll graded attempts (module passes → CRM) |
| PATCH | /admin/quizzes/:quizId/template | Set the certificate template |
| POST | /admin/courses/:id/unpublish | Unpublish → draft |
| POST | /admin/courses/:id/archive | Archive (retire from catalog) |
| DELETE | /admin/courses/:id | Delete (draft/archived only) |
| GET | /admin/categories | List catalog categories |
| POST | /admin/categories | Create a category |
| PATCH | /admin/categories/:id | Rename a category |
| DELETE | /admin/categories/:id | Delete a category (courses survive) |
| POST | /admin/categories/:id/move | Reorder a category section |
| POST | /admin/courses/:id/move | Reorder a course in the catalog |
Standard HTTP status codes with a JSON { message } body:
| 400 | Invalid input (missing field, bad lesson type, negative duration) |
| 401 | Missing / invalid / revoked API key |
| 403 | Key lacks the required scope |
| 404 | Course not found in this institute |
| 409 | Slug already exists — safe to treat as 'already created' |
Requests are rate-limited at the edge per key. Design for retries with exponential backoff; a 409 on a repeated slug is a safe idempotency signal, not a failure.
A machine-readable spec is published at /openapi.json — import it into Postman, an SDK generator, or an AI tool-use definition.