Skip to content

Partner API

PulseLMS exposes a small partner REST API so you can read training data and enroll people from your own systems: an HRIS, a data warehouse, an onboarding workflow, and so on. It's authenticated with a per-tenant API key and every response is scoped to that key's organization only, held to the exact same tenant isolation as the app.

  • Base URL: https://learn.mypulsetech.com/api/public/v1
  • Interactive docs (Swagger): /api/docs
  • OpenAPI 3.1 document: /api/openapi.json

App API vs. partner API

This page documents the partner API (/api/public/v1), authenticated by an API key. The PulseLMS web app uses a separate app API (/api/v1) authenticated by your SSO session; that's not for external integrations.


Creating an API key

API keys are managed by an org admin.

  1. Go to Settings → API keys.
  2. Click Create key, give it a name, and choose its scopes (below).
  3. Copy the key immediately; it's shown once and never again. Store it in a secret manager, not in source control.
  4. Use it as a bearer token (below). Revoke a key any time from the same screen; revoked keys stop working instantly.

Keys look like pls_<prefix>_<secret>. PulseLMS stores only a hash of the secret, which is why it can't be shown again.

Scopes

Scope Grants
read Read published courses, enrollment summaries, and completions.
enroll:write Everything in read, plus enrolling users via POST /enrollments.

Grant the narrowest scope that does the job. A read-only sync should use a read-only key.


Authentication

Send the key on every request, either way:

Authorization: Bearer pls_<prefix>_<secret>

or

X-API-Key: pls_<prefix>_<secret>

Missing or invalid keys return 401; a valid key that lacks the required scope returns 403. Errors use the standard envelope:

{ "error": { "code": "insufficient_scope", "message": "this key lacks the 'enroll:write' scope" } }

Endpoints

Public catalog (no auth)

Fetch the public catalog for an organization by its alias. This endpoint needs no key, but only courses an author explicitly marked public are returned, and only if the organization has enabled its public catalog.

GET /api/public/v1/catalog?org=<alias>
{
  "org": "acme",
  "requested_org": "acme",
  "courses": [
    { "id": "…", "title": "Workplace Safety 101", "description": "…" }
  ]
}

An organization's alias can change. When it does, the old alias goes on working, and you do not have to update a saved URL. requested_org echoes the alias you asked for and org is the organization's current one, so the two differing is how you notice a rename.

List published courses

GET /api/public/v1/courses        Authorization: Bearer pls_…   (scope: read)
{
  "courses": [
    { "id": "…", "title": "…", "status": "published", "description": "…" }
  ]
}

Enrollment summary

Counts across the organization, useful for a dashboard tile.

GET /api/public/v1/enrollments/summary        (scope: read)
{ "enrollments": 420, "completed": 315, "in_progress": 88, "completion_rate": 75 }

Completions

Completed enrollments, for syncing training records back to your systems.

GET /api/public/v1/completions        (scope: read)
{
  "completions": [
    { "user_sub": "…", "course_id": "…", "completed_at": "2026-07-20T09:12:00+00:00" }
  ]
}

Enroll a user

Enroll a user in a published course. Requires the enroll:write scope.

POST /api/public/v1/enrollments        (scope: enroll:write)
Content-Type: application/json

{ "course_id": "<uuid>", "user_sub": "<user-id>" }
  • Returns 201 with the new enrollment, or 200 if the user was already enrolled; the call is idempotent, so it's safe to retry.
  • Returns 404 if the course doesn't exist or isn't published.
  • On a new enrollment, the learner receives an assigned notification.
{ "enrollment_id": "…", "course_id": "…", "user_sub": "…", "status": "in_progress" }

user_sub

user_sub is the user's stable identity subject from your SSO: the same identifier the user signs in with. The user must already exist in the organization (via SSO / JIT provisioning or an invite).

Identify the key's organization

Returns which organization a key belongs to, plus the feature flags relevant to integrations. The Discord bot uses this to verify a key during /setup.

GET /api/public/v1/whoami        (scope: read)
{
  "tenant_id": "…",
  "org": { "alias": "acme", "name": "Acme Inc.", "edition": "community" },
  "scopes": ["read", "enroll:write"],
  "flags": { "discord": true, "api_access": true }
}

Discord bot endpoints

These power the Discord bot. They require the read scope and the organization's discord feature flag (otherwise they return 403). <discord_id> is a Discord user's numeric ID.

Endpoint Purpose
POST /discord/link-requests Start an account-link handshake. Body { "discord_id": "…", "discord_username": "…" }. Returns a short code and a confirm URL the member opens (while signed in) to finish linking. If the user is already linked, returns { "already_linked": true, … }.
GET /discord/links/<discord_id> Look up the PulseLMS account linked to a Discord user. Returns { "linked": true, "user_sub": "…" } or { "linked": false }.
DELETE /discord/links/<discord_id> Remove a Discord user's account link.
GET /discord/enrollments/<discord_id> A linked Discord user's course enrollments (title + status). 404 if the user isn't linked.
GET /discord/completions Completions for linked users only (with discord_id), the poll feed for completion rewards and celebration posts.
GET /discord/expiring-certificates?days=<n> Linked users' active certificates expiring within n days (default 30), for recert nudges.
GET /discord/gamification/<discord_id> A linked user's XP, level, and badges. Also requires the gamification flag. 404 if not linked.
GET /gamification/leaderboard Tenant leaderboard (rank, name, points). Requires the gamification flag.

The GET /courses response also carries allow_self_enroll, difficulty, and est_minutes so the bot can offer self-enroll only where it's allowed.

Course payloads carry listed. When it is false the course is unlisted: still published and assignable, but deliberately kept out of the member catalog and out of learner search results. Treat it the same way: do not surface an unlisted course in a browse or discovery view.

Account linking is a two-sided handshake: the code minted here is confirmed by the member inside PulseLMS (after they sign in), so a Discord ID is only ever bound to the identity that actually proves it. Enrollment itself uses the standard POST /enrollments endpoint above.


Pairing with webhooks

The API is the pull side; webhooks are the push side. A common pattern:

  • Register a webhook for enrollment.completed and certificate.issued so PulseLMS notifies you the moment something happens.
  • Use GET /completions to backfill or reconcile if your listener was down.
  • Use POST /enrollments to assign training from your own onboarding flow.

Webhook deliveries are signed with HMAC-SHA256; see notifications & events for verification code.


Reference