Resources
Resources wrap the calls most integrations need. Each method returns typed data and throws a
LearnHouseError on failure. Every method takes a last options argument with
signal, headers, timeoutMs, retry and skipAuth.
Organizations
| Method | Calls |
|---|---|
lh.orgs.get(slug) | GET /api/v1/orgs/slug/{org_slug} |
lh.orgs.getByUuid(uuid) | GET /api/v1/orgs/uuid/{org_uuid} |
Courses
| Method | Calls |
|---|---|
lh.courses.list(orgSlug, { page, limit, includeUnpublished }) | GET /api/v1/courses/org_slug/{org_slug}/page/{page}/limit/{limit} |
lh.courses.listAll(orgSlug, { pageSize, includeUnpublished }) | The same endpoint, page after page, as an async iterator |
lh.courses.count(orgSlug) | GET /api/v1/courses/org_slug/{org_slug}/count |
lh.courses.get(courseUuid) | GET /api/v1/courses/{course_uuid} |
lh.courses.getMeta(courseUuid, { slim, withUnpublishedActivities }) | GET /api/v1/courses/{course_uuid}/meta: the course with its chapters and activities |
const course = await lh.courses.getMeta('course_…', { slim: true }) // slim: no activity content, enough for navigationlistAll yields every course of the org. It stops after the first short page, so it never fetches past the end:
for await (const course of lh.courses.listAll('your-school')) {
console.log(course.name)
}pageSize defaults to 100, the most the API returns per page. See Pagination.
Chapters
| Method | Calls |
|---|---|
lh.chapters.get(chapterId) | GET /api/v1/chapters/{chapter_id} |
lh.chapters.listForCourse(courseUuid) | The chapters of GET /api/v1/courses/{course_uuid}/meta?slim=true |
Activities
| Method | Calls |
|---|---|
lh.activities.get(activityUuid) | GET /api/v1/activities/{activity_uuid}, including its content |
lh.activities.listForChapter(chapterId) | GET /api/v1/activities/chapter/{chapter_id} |
lh.activities.resolve({ activityId }) | GET /api/v1/activities/{activity_uuid}. Takes the id with or without its activity_ prefix |
lh.activities.resolve({ courseId, activityName }) | Finds the activity by name in GET /api/v1/courses/{course_uuid}/meta?slim=true, then loads it |
An activity's content is the document <LearnHouseActivity> renders.
Load an activity to render
resolve loads an activity the same way <LearnHouseActivity> does: by id, or by name inside a course. Ids work with
or without their prefix, so you can paste them from a LearnHouse URL (/course/<id>/activity/<id>). Names match
without regard to case, spacing or punctuation.
const byId = await lh.activities.resolve({ activityId: 'activity_…' })
const byName = await lh.activities.resolve({ courseId: 'course_…', activityName: 'How payouts work' })An unknown name throws a LearnHouseError with status 404 and code ACTIVITY_NOT_FOUND. If you already have the
course meta, pass it as courseMeta and the name lookup skips its own request.
Fetch on the server, render in the browser
Load the activity on your server with an API token, then pass it to the component as activity. The browser never
calls the LearnHouse API, so you need no CORS setup, and the token stays on your server.
import { createClient } from '@learnhouse/sdk'
import { LearnHouseActivity } from '@learnhouse/react'
const lh = createClient({ baseUrl: process.env.LEARNHOUSE_URL!, token: process.env.LEARNHOUSE_TOKEN! })
export default async function Lesson({ params }: { params: Promise<{ name: string }> }) {
const { name } = await params
const activity = await lh.activities.resolve({ courseId: 'course_…', activityName: decodeURIComponent(name) })
return <LearnHouseActivity activity={activity} courseId="course_…" baseUrl={process.env.LEARNHOUSE_URL} />
}Keep courseId and baseUrl on the component: media blocks use them to build image and video URLs.
Search
| Method | Calls |
|---|---|
lh.search.org(orgSlug, query, { page, limit }) | GET /api/v1/search/org_slug/{org_slug} |
lh.search.orgPages(orgSlug, query, { pageSize }) | The same endpoint, page after page, as an async iterator |
Each search result type (courses, activities, users…) is paged on its own. orgPages yields one SearchResult per page
and stops once every type comes back shorter than pageSize (default and maximum 50):
for await (const page of lh.search.orgPages('your-school', 'payouts')) {
console.log(page.courses.length, page.activities?.length)
}Search is rate limited per user. If you hit the limit, the SDK waits for the time the API asks, within maxDelayMs.
See Rate limits.
Progress
Progress belongs to a learner, so these need a signed-in learner, not an API token.
| Method | Calls |
|---|---|
lh.trail.get() | GET /api/v1/trail/: the learner's courses and progress |
lh.trail.getForOrg(orgId) | GET /api/v1/trail/org/{org_id}/trail |
lh.trail.startCourse(courseUuid) | POST /api/v1/trail/add_course/{course_uuid} |
lh.trail.leaveCourse(courseUuid) | DELETE /api/v1/trail/remove_course/{course_uuid} |
lh.trail.completeActivity(activityUuid) | POST /api/v1/trail/add_activity/{activity_uuid}: same as "Mark as complete" |
lh.trail.uncompleteActivity(activityUuid) | DELETE /api/v1/trail/remove_activity/{activity_uuid} |
Users
| Method | Calls |
|---|---|
lh.users.session() | GET /api/v1/users/session: the signed-in user and their roles |
lh.users.getByUuid(userUuid) | GET /api/v1/users/uuid/{user_uuid} |
Auth
| Method | Calls |
|---|---|
lh.auth.login({ email, password, orgSlug }) | POST /api/v1/auth/login |
lh.auth.loginMfa({ mfaToken, code, isBackupCode }) | POST /api/v1/auth/login/mfa |
lh.auth.refresh({ refreshToken }) | GET /api/v1/auth/refresh (never retried) |
lh.auth.logout() | DELETE /api/v1/auth/logout |
lh.auth.me() | GET /api/v1/auth/me |
See Authentication.
Media URLs
Course thumbnails, block images, videos and files live under your instance's content/ path. lh.media builds those
URLs exactly the way the LearnHouse web app does (it is the same code):
const org = await lh.orgs.get('your-school')
const course = await lh.courses.get('course_…')
const thumbnail = lh.media.getCourseThumbnailMediaDirectory(org.org_uuid!, course.course_uuid!, course.thumbnail_image!)If files are served from somewhere else (a CDN or bucket), pass mediaUrl to createClient. The helpers also exist
standalone, without a client:
import { media } from '@learnhouse/sdk'
media.getUserAvatarMediaDirectory({ baseUrl: 'https://learn.your-school.com' }, userUuid, fileId)Need an endpoint that isn't here? Every API endpoint is callable, typed, with
lh.GET / lh.POST….