Edit

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

MethodCalls
lh.orgs.get(slug)GET /api/v1/orgs/slug/{org_slug}
lh.orgs.getByUuid(uuid)GET /api/v1/orgs/uuid/{org_uuid}

Courses

MethodCalls
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 navigation

listAll 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

MethodCalls
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

MethodCalls
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.

app/lessons/[name]/page.tsx
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.

MethodCalls
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.

MethodCalls
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

MethodCalls
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

MethodCalls
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….