Errors
The EmbayLMS API uses standard HTTP status codes and returns a consistent error envelope on every failure. Errors never include internal stack traces or database details.
Error Envelope
{
"data": null,
"meta": null,
"error": {
"code": 404,
"message": "Tenant not found"
}
}| Field | Type | Description |
|---|---|---|
error.code | integer | Numeric code — always equal to the HTTP status of the response |
error.message | string | Human-readable description, safe to log and display |
On success, error is null and data carries the payload. There is no
details object — branch on error.code (or the HTTP status, they are the
same) and use error.message for diagnostics.
Status Codes
HTTP Status / error.code | When Used |
|---|---|
200 OK | Successful GET |
400 Bad Request | Invalid request parameters (e.g. missing required query parameter) |
401 Unauthorized | Missing, invalid, revoked, or expired API key (response includes WWW-Authenticate: Bearer) |
403 Forbidden | Valid key but insufficient scope (e.g. missing read), or API access not included in your plan |
404 Not Found | Resource does not exist in this tenant, or the endpoint is not implemented (see the “Planned” banners) |
429 Too Many Requests | Per-minute rate limit or monthly usage cap exceeded — retry after Retry-After seconds. See Rate Limits |
500 Internal Server Error | Unexpected server error — retry with backoff; contact support if persistent |
503 Service Unavailable | Temporary outage or maintenance |
Write endpoints additionally use 201 Created (successful create),
204 No Content (successful delete), 207 Multi-Status (bulk partial
errors — see below), and 409 Conflict (e.g. duplicate email on user
creation). Calling a still-planned endpoint (see the “Planned” banners)
returns 404.
Error Handling Examples
Node.js
async function listUsers() {
const response = await fetch('https://{your-subdomain}.embaylms.com/api/v1/users', {
headers: {
'Authorization': `Bearer ${process.env.EMBAYLMS_API_KEY}`,
},
});
const result = await response.json();
if (!response.ok) {
const { error } = result;
switch (error.code) {
case 401:
throw new Error('API key missing, invalid, or revoked — rotate the key');
case 403:
throw new Error(`Forbidden: ${error.message}`); // missing scope or plan
case 429: {
const retryAfter = parseInt(response.headers.get('Retry-After') || '1', 10);
throw new RateLimitError(retryAfter);
}
default:
throw new Error(`API error ${error.code}: ${error.message}`);
}
}
return result.data;
}Python
import httpx
class EmbayLMSError(Exception):
def __init__(self, code, message):
self.code = code
self.message = message
super().__init__(f"{code}: {message}")
def list_users(client):
response = client.get("/users")
if response.status_code == 200:
return response.json()["data"]
error = response.json().get("error") or {}
code = error.get("code", response.status_code)
if code == 429:
retry_after = int(response.headers.get("Retry-After", "1"))
raise EmbayLMSError(429, f"Rate limited — retry after {retry_after}s")
raise EmbayLMSError(code, error.get("message", "Unknown error"))Validation and Bulk Errors
When input validation fails on a write endpoint, the 400 response’s
error.message identifies the failing field(s).
Live: POST /enrollments/bulk uses 207 Multi-Status when some rows fail,
reporting individual errors per row — each row runs independently, so one bad
row never fails the batch:
{
"data": {
"created": 3,
"skipped": 2,
"errors": [
{
"index": 3,
"code": "course_not_found",
"message": "Course not found"
},
{
"index": 7,
"code": "duplicate",
"message": "User is already enrolled in this course"
}
]
},
"meta": null,
"error": null
}The HTTP status is 207 when there are partial errors, 201 when all rows
succeed. The top-level error stays null for bulk endpoints — per-row errors
are in data.errors (index is the zero-based position in the submitted array).
⚠️ Planned — not yet available.
POST /users/bulkis still part of the planned API surface and returns404today; when it ships it will follow the same207 Multi-Statusper-row error pattern.