API ReferenceAuthentication

Authentication

The EmbayLMS REST API authenticates with API keys — long-lived keys for server-to-server integrations and scripts, presented as a Bearer token:

Authorization: Bearer <api-key>

API keys are the only live authentication method. A short-lived JWT flow via OAuth2 client-credentials exchange is planned (see below) but not yet available.

The only endpoint that requires no authentication is the public GET /api/v1/plans pricing catalog.


API Keys

Creating an API Key

  1. Sign in to your EmbayLMS tenant as an admin.
  2. Navigate to Settings → Integrations → API Keys (the API Keys tab of the unified Integrations menu).
  3. Click Create API Key.
  4. Enter a descriptive name (e.g., HRIS Integration - Production).
  5. Select the required scopes (see Scopes below).
  6. Click Create.

The full API key is displayed once. Copy it immediately and store it in a secrets manager. EmbayLMS stores only the hashed value — the full key cannot be retrieved again.

Key Format

ems_live_0000000000000000000000000000
PartValueDescription
emsFixed prefixIdentifies the key as an EmbayLMS API key
liveEnvironmentlive for production, test for sandbox (if enabled)
a1b2c3...SecretRandom string — shown once, hashed on storage

The first 12 characters of the key (e.g. ems_live_000) are stored as the key’s prefix and remain visible in the portal for identification. The rest of the key is never shown again after creation.

Using an API Key

Include the full key in the Authorization header of every request:

Authorization: Bearer ems_live_0000000000000000000000000000

Requests go to your tenant’s subdomain — the key is bound to the tenant it was created in:

https://{your-subdomain}.embaylms.com/api/v1/...

JWT Bearer Tokens (Server-to-Server OAuth2)

⚠️ Planned — not yet available. The POST /api/v1/auth/token client-credentials exchange below is not implemented; calling it today returns 404. Use API keys (above) for all server-to-server auth until this ships. This section documents the intended contract ahead of implementation.

For systems that manage OAuth2 flows, you can exchange credentials for a short-lived JWT instead of using a long-lived API key.

Exchange Flow

POST /api/v1/auth/token
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "your-client-id",
  "client_secret": "your-client-secret",
  "scope": "read write"
}

Response:

{
  "data": {
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "scope": "read write"
  },
  "meta": null,
  "error": null
}

Use the access_token as a Bearer token. Tokens expire after expires_in seconds (default: 3600). Request a new token before expiry — do not cache expired tokens.


Scopes

Scopes control what an API key is permitted to do. Assign the minimum scope required for your integration.

ScopeDescription
readRead access to resources (GET endpoints). Required by every read endpoint.
writeCreate and update resources (POST, PATCH endpoints — users, enrollments incl. bulk, courses, modules, groups, membership)
adminDelete/archive/deactivate resources (DELETE endpoints) and privileged writes (e.g. assigning the admin or manager role)

Scopes are additive. A key with read write can read and write but cannot delete or access admin endpoints.

Scope is enforced on every request: GET endpoints require the read scope, POST/PATCH endpoints the write scope, and DELETE endpoints (plus privileged writes) the admin scope. A key without the required scope receives 403 (see the insufficient-scope error under Error Responses below).

Principle of least privilege: assign only the scopes your integration needs. A reporting integration that only reads users and completions needs read, nothing more.


Security Best Practices

Never Expose Keys in Client-Side Code

API keys must only be used in server-side code. Never include an API key in:

  • Frontend JavaScript / TypeScript
  • Mobile app binaries
  • Public GitHub repositories
  • Build logs or CI output

Store Keys in Environment Variables

# .env (server-side only, never committed)
EMBAYLMS_API_KEY=ems_live_0000000000000000000000000000

Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) in production environments.

Rotate Keys Every 90 Days

  1. Create a new key with the same scopes.
  2. Deploy the new key to your integration.
  3. Verify the integration is working with the new key.
  4. Revoke the old key from Settings → Integrations → API Keys.

Set a calendar reminder or automate rotation — compromised keys can be revoked immediately from the portal.

Revoke Keys Immediately on Exposure

If a key is accidentally committed to a repository or logged, revoke it immediately from Settings → Integrations → API Keys and issue a new one. Do not wait.


Error Responses

Errors use the standard envelope with a numeric error.code matching the HTTP status — see Errors for the full catalog.

401 — Invalid or Missing Key

Returned when the Authorization header is missing, or the key does not exist, is revoked, or is expired. The response includes a WWW-Authenticate: Bearer header.

{
  "data": null,
  "meta": null,
  "error": {
    "code": 401,
    "message": "Invalid or missing API key"
  }
}

403 — Insufficient Scope

Returned when the key is valid but does not have the scope the endpoint requires — for example, calling a GET endpoint with a key that lacks the read scope.

{
  "data": null,
  "meta": null,
  "error": {
    "code": 403,
    "message": "API key is missing the required 'read' scope"
  }
}

Create a new key with the required scope (or edit the key’s scopes in the portal) — the scope check is enforced on every request.


Code Examples

curl

# List users
curl https://{your-subdomain}.embaylms.com/api/v1/users \
  -H "Authorization: Bearer ems_live_0000000000000000000000000000"

Node.js

const apiKey = process.env.EMBAYLMS_API_KEY;
const baseUrl = 'https://{your-subdomain}.embaylms.com/api/v1';
 
async function fetchUsers() {
  const response = await fetch(`${baseUrl}/users`, {
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
  });
 
  if (!response.ok) {
    const error = await response.json();
    throw new Error(`API error ${response.status}: ${error.error.message}`);
  }
 
  return response.json();
}

Python

import os
import httpx
 
API_KEY = os.environ["EMBAYLMS_API_KEY"]
BASE_URL = "https://{your-subdomain}.embaylms.com/api/v1"
 
headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}
 
with httpx.Client(base_url=BASE_URL, headers=headers) as client:
    response = client.get("/users")
    response.raise_for_status()
    data = response.json()
    users = data["data"]