Skip to Content
Edit on GitHub

Custom Features & Webhooks

The build guide showed how to read the API as a frontend. This guide is about going further: driving LearnHouse programmatically — creating and managing content from your own scripts and services, and reacting to what happens inside LearnHouse with webhooks.

Work with the API
API tokens, request conventions, and recipes for creating and querying content programmatically.
Consume webhooks
Subscribe to dozens of events, verify signatures, and react to learning activity in real time.

Two ways to authenticate

User session (JWT)API token
ForActing as a logged-in user (a frontend)Server-to-server automation, scripts, integrations
HowPOST /api/v1/auth/login → bearer tokenA long-lived token created in org settings
Prefixa JWTlh_…
PlananyPro

For everything in this guide, use an API token. It’s a single bearer credential you attach to every request:

curl "http://localhost:1338/api/v1/courses/org_slug/{org_slug}/page/1/limit/10" \
  -H "Authorization: Bearer lh_your_api_token_here"

API tokens work on resource routes (courses, chapters, activities, collections…) but not on routes that require a real user session — /users/*, /orgs/*, /roles/*, and webhook management all reject lh_ tokens with 403. Use a courses read to smoke-test a token.

Creating an API token

API tokens are a Pro feature. Create one in your organization settings, or via the API itself (with a user session that has roles permissions):

curl -X POST "http://localhost:1338/api/v1/orgs/{org_id}/api-tokens" \
  -H "Authorization: Bearer <user_jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My integration",
    "description": "Server-side automation",
    "expires_at": null
  }'

The response includes the full token exactly once — store it securely, it can’t be retrieved again:

{
  "token": "lh_8f3c…the only time you'll see this…",
  "token_uuid": "apitoken_…",
  "name": "My integration",
  "token_prefix": "lh_8f3c4d2a",
  "org_id": 1
}

Tokens support fine-grained permissions via a rights object (per-resource read/write scopes) and an optional expires_at. Set rights to grant a token only the access it needs. The full schema is on the interactive Swagger UI at http://localhost:1338/docs (development mode). List, revoke, and regenerate tokens under /api/v1/orgs/{org_id}/api-tokens.

Request conventions

A few things that are true across the whole API:

  • Base path — every endpoint is under /api/v1/.
  • AuthAuthorization: Bearer <token> (JWT or lh_ token).
  • Reads are JSONGET returns JSON; lists are plain arrays unless documented otherwise.
  • Writes are often multipart — create/update endpoints that can take a file (courses, activities, avatars) accept multipart/form-data, not JSON. Endpoints without files (collections, trails, users) take JSON.
  • Errors — non-2xx responses are { "detail": "..." }. Always read detail.
  • UUIDs vs ids — resources expose a stable {resource}_uuid (e.g. course_…, activity_…); numeric ids are used by a few routes (notably org_id).

Recipe: create a course programmatically

Courses are created with multipart form data (so a thumbnail can ride along). org_id is a query parameter:

curl -X POST "http://localhost:1338/api/v1/courses/?org_id=1" \
  -H "Authorization: Bearer lh_your_api_token" \
  -F 'name=Introduction to Python' \
  -F 'description=A beginner-friendly Python course' \
  -F 'about=Learn Python from scratch.' \
  -F 'public=true'

name, description, and about are all required on course creation — omitting about returns 422. Creating courses, chapters, and activities requires a role with create permission (Admin / Maintainer / Instructor), not a plain member account.

Then add structure — a chapter (JSON), then activities inside it:

# Create a chapter
curl -X POST "http://localhost:1338/api/v1/chapters/" \
  -H "Authorization: Bearer lh_your_api_token" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Getting started", "org_id": 1, "course_id": 42 }'

For bulk or AI-assisted course creation from existing media, see the Migration guides — there’s a three-call flow that builds an entire course tree in one transaction.

Recipe: search and list content

# Paginated list of courses
curl "http://localhost:1338/api/v1/courses/org_slug/{org_slug}/page/1/limit/20" \
  -H "Authorization: Bearer lh_your_api_token"
 
# Full-text search (max 50 per page)
curl "http://localhost:1338/api/v1/courses/org_slug/{org_slug}/search?query=python&page=1&limit=10" \
  -H "Authorization: Bearer lh_your_api_token"

Discoverability: the OpenAPI spec

Because the API is built with FastAPI, a complete machine-readable spec is available:

  • OpenAPI JSONhttp://localhost:1338/openapi.json (served on all instances)
  • Swagger UIhttp://localhost:1338/docs (development mode only)
  • ReDochttp://localhost:1338/redoc (development mode only)

Point a client generator at openapi.json to produce a typed SDK in your language of choice.

The interactive HTML docs (/docs and /redoc) are served in development mode only. The raw /openapi.json spec is generated by FastAPI and available on all instances.

Next: react to events

Polling the API to find out “did anyone finish a course?” is wasteful. Webhooks push those events to you the moment they happen — enrollment, completion, signups, and dozens more.

Consume webhooks