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.
- Go to Settings → API keys.
- Click Create key, give it a name, and choose its scopes (below).
- Copy the key immediately; it's shown once and never again. Store it in a secret manager, not in source control.
- 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:
or
Missing or invalid keys return 401; a valid key that lacks the required scope returns 403. Errors use the standard envelope:
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.
{
"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¶
Enrollment summary¶
Counts across the organization, useful for a dashboard tile.
Completions¶
Completed enrollments, for syncing training records back to your systems.
{
"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
assignednotification.
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.
{
"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.completedandcertificate.issuedso PulseLMS notifies you the moment something happens. - Use
GET /completionsto backfill or reconcile if your listener was down. - Use
POST /enrollmentsto assign training from your own onboarding flow.
Webhook deliveries are signed with HMAC-SHA256; see notifications & events for verification code.
Reference¶
- Swagger UI:
/api/docsto try calls interactively. - OpenAPI JSON:
/api/openapi.jsonto generate clients or import into Postman/Insomnia. - Admin setup lives in the integrations guide.