Skip to content

API reference

The registry exposes a small HTTP API. Interactive docs (Swagger) are always at /docs on a running instance; this page is the summary. Auth is a Bearer JWT from /auth/login or /auth/register.

Public (used by the mobile apps)

GET /lms and GET /lms/{id}

These two return a redacted view — no owner email, block reason, or review and health state — and /lms/{id} resolves only approved platforms. An owner reads their own full record from /my/lms/{id}. In portal mode /lms is empty and /lms/{id} is 404, because a deployment-wide list would expose every tenant.

Method Path Purpose
GET /api/v1/config Directory mode + provider branding
GET /api/v1/directory?q=&featured=&limit= Search / curated list
GET /api/v1/directory/{id} Full platform detail (theme, feature flags)
POST /api/v1/reports File a complaint (optionally with a base64 screenshot)

/api/universal-login/lms/search, /…/{id}, and /…/reports are aliases of the same handlers.

Per-organization directories (portal mode only)

In portal mode the same public endpoints live under a per-organization prefix, and the site root serves no catalog at all:

Method Path Purpose
GET /p/{slug}/api/v1/config Always directory_mode: "curated", plus that organization's name, tagline and logo
GET /p/{slug}/api/v1/directory That organization's published platforms, ordered by sort_order
GET /p/{slug}/api/v1/directory/{id} Full detail — 404 if the id belongs to another organization
POST /p/{slug}/api/v1/reports Always 403; provider directories have no complaint channel

/p/{slug}/api/universal-login/lms/search and /…/{id} are aliases.

The same directory is also published as one self-contained file:

Method Path Purpose
GET /p/{slug}/directory.json The whole directory in one document: provider branding plus every platform, with platforms[] entries byte-identical to /api/v1/directory/{id}. Add ?download=1 for a file download.
GET /p/{slug}/directory.zip The document plus every image this registry hosts, with the image fields rewritten to file names, and a README. For a build that ships its list and never asks the network.
GET /directory.json The same document for a curated deployment, which serves one organization. 404 in search and portal modes.

Images hosted elsewhere are not packed into the zip and keep their addresses — the registry does not fetch arbitrary URLs on request.

An unknown slug and a suspended organization both answer 404 Unknown directory, so the responses can't be used to enumerate the providers on a portal.

The apps need no change for this: they resolve their paths relative to DIRECTORY_URL, so setting it to https://host/p/acme yields https://host/p/acme/api/v1/config.

At the site root in portal mode, /api/v1/config reports directory_mode: "portal" (the console reads it to decide which workspace to render) while /api/v1/directory returns 404 with an explanation — so a build that was pointed at the bare origin fails loudly instead of showing an empty list.

File a complaint

POST /api/v1/reports
{
  "lms_id": 12,
  "base_url": "https://learn.example.com",
  "category": "inappropriate",
  "message": "…",
  "reporter_email": "learner@example.com",
  "platform": "ios",
  "app_version": "2.4.0",
  "screenshot_base64": "data:image/jpeg;base64,…"
}

category is one of inappropriate, scam, impersonation, spam, broken, other. Provide lms_id or base_url. Rate-limited per IP.

Owner (authenticated)

Method Path Purpose
POST /lms Submit a platform (from the wizard)
GET /my/lms My platforms — in portal mode, my organization's
GET /my/lms/{id} Full record for one of mine (what the wizard loads to edit)
PUT /my/lms/{id} Edit one of mine. Moderation fields (status, featured, admin_reviewed) are not accepted
POST /my/lms/{id}/request-review Ask for a re-review after fixing a block
POST /validate-lms Check a URL + OAuth client are reachable. Requires auth in portal mode

Anything that belongs to another owner or organization answers 404, not 403, so ids can't be probed.

Provider (portal mode only)

Method Path Purpose
GET /provider/org My organization, its directory_url and platform counts
PATCH /provider/org Update name, tagline, logo, contact email. slug is not accepted — it is compiled into shipped apps

The organization is resolved from the account, never from the request, so a token minted for one provider cannot be aimed at another by editing a path or a body.

Sign-up in portal mode goes through the ordinary POST /auth/register with an extra required organization_name (and an optional organization_slug); the account and the organization are created together.

Admin (authenticated, admin role)

Method Path Purpose
GET /admin/overview Aggregate counts
GET /admin/lms Search / filter / paginate platforms
POST /admin/lms/{id}/block · /unblock Remove / restore in the catalog
POST /admin/lms/{id}/recheck Re-probe health
PATCH /admin/lms/{id}/review Mark reviewed
GET /admin/lms/{id}/events Moderation timeline (audit)
GET /admin/reports Triage inbox (filters: status/category/severity/platform)
GET /admin/reports/by-lms Grouped by platform (+ distinct reporters)
GET /admin/reports/stats Counts for the live badge
PATCH /admin/reports/{id} Set status + note
POST /admin/lms/{id}/notify-owner Compose / send the owner notice
GET /admin/lms/{id} Full record for one platform
GET POST /admin/users, /admin/users/{id}/role Manage the team
GET /admin/orgs (portal) Every organization, its directory URL and owner
PATCH /admin/orgs/{id} (portal) Suspend or restore one organization

The full field-level contract lives in SPEC.md in the repository root.