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
- Sign in to your EmbayLMS tenant as an admin.
- Navigate to Settings → Integrations → API Keys (the API Keys tab of the unified Integrations menu).
- Click Create API Key.
- Enter a descriptive name (e.g.,
HRIS Integration - Production). - Select the required scopes (see Scopes below).
- 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| Part | Value | Description |
|---|---|---|
ems | Fixed prefix | Identifies the key as an EmbayLMS API key |
live | Environment | live for production, test for sandbox (if enabled) |
a1b2c3... | Secret | Random 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_0000000000000000000000000000Requests 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/tokenclient-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.
| Scope | Description |
|---|---|
read | Read access to resources (GET endpoints). Required by every read endpoint. |
write | Create and update resources (POST, PATCH endpoints — users, enrollments incl. bulk, courses, modules, groups, membership) |
admin | Delete/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_0000000000000000000000000000Use a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.) in production environments.
Rotate Keys Every 90 Days
- Create a new key with the same scopes.
- Deploy the new key to your integration.
- Verify the integration is working with the new key.
- 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"]