Users
The Users API returns and manages the learners, instructors, managers, and admins in
your tenant. Reading (GET), creating (POST), updating (PATCH), and
deactivating (DELETE, a soft delete) users are all available. Reads need
the read scope, creates and updates the write scope, and deactivation the
admin scope. You can still provision users in bulk via
SCIM 2.0 or CSV import.
The User Object
{
"id": "a3f2c1d0-4e5b-6789-abcd-ef0123456789",
"email": "jane.doe@acme.com",
"first_name": "Jane",
"last_name": "Doe",
"role": "learner",
"is_active": true,
"external_id": "EMP-00412",
"created_at": "2026-01-15T09:00:00Z"
}| Field | Type | Notes |
|---|---|---|
id | uuid | User UUID |
email | string | Unique within the tenant |
first_name | string | First name |
last_name | string | Last name |
role | string | learner, instructor, manager, group_admin, ecommerce_admin, admin, owner — see Role vocabulary |
is_active | boolean | false = deactivated (cannot sign in) |
user_type | string | Audience attribute: internal (default), partner, customer, contractor, or volunteer — an attribute for filtering/reporting, never a permission source (LMS-780) |
external_id | string | null | Your system’s identifier (e.g. HRIS employee id), if set |
created_at | ISO 8601 | When the user was created |
Soft-deleted users are never returned.
Role vocabulary
Every user carries one primary role. These are the seven RBAC v2 personas:
| Role | Description |
|---|---|
learner | Consumes assigned and self-selected learning |
instructor | Facilitates sessions and grades, scoped to their own courses |
manager | Assigns and monitors training for their direct reports |
group_admin | Administers users and content within their assigned groups |
ecommerce_admin | Manages the storefront, pricing, and orders |
admin | Full tenant administration |
owner | Top of the tenant hierarchy; the only role that can assign admin |
Assigning manager, group_admin, ecommerce_admin, admin, or owner requires
an API key with the admin scope. A write-scoped key can only create learner
and instructor users.
The retired author role
author was retired in RBAC v2. It is still accepted on write during the
deprecation window, so existing integrations keep working without change — but it
is no longer stored as a primary role. A request with "role": "author":
- stores the user with primary role
learner, and - grants them the seeded “Content Author” custom role, which carries the same
permissions the old
authorrole had (course create / update / publish / archive, report read, SCORM manage).
This mirrors exactly what the one-time RBAC v2 migration did to existing rows. The
user keeps the capability; role stops reporting a value that no longer exists.
Read responses may still return author for users created before the migration who
have not yet been migrated.
Migrate at your convenience: send the v2 role you actually want instead of
author. For a content author, that isinstructorplus the “Content Author” custom role, orinstructoralone if course publishing is not needed.
Endpoints
List Users
GET /api/v1/usersReturns a paginated list of users in the tenant, most recently created first.
Authentication required: API key with the read scope
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number. Default: 1 |
per_page | integer | No | Results per page. Default: 50, max: 200 |
role | string | No | Filter by role. Accepts every value in Role vocabulary, plus author to find rows created before the RBAC v2 migration |
Example Request
curl "https://{your-subdomain}.embaylms.com/api/v1/users?role=learner&per_page=25" \
-H "Authorization: Bearer ems_live_0000000000000000000000000000"Example Response
{
"data": [
{
"id": "a3f2c1d0-4e5b-6789-abcd-ef0123456789",
"email": "jane.doe@acme.com",
"first_name": "Jane",
"last_name": "Doe",
"role": "learner",
"is_active": true,
"external_id": "EMP-00412",
"created_at": "2026-01-15T09:00:00Z"
}
],
"meta": {
"page": 1,
"per_page": 25,
"total": 342
},
"error": null
}Common Errors
Errors use the standard envelope with a numeric error.code matching the HTTP
status — see Errors.
| HTTP Status | Description |
|---|---|
401 | Missing or invalid API key |
403 | Key lacks the read scope, or the API is not included in your plan |
429 | Rate limit or monthly usage cap exceeded — see Rate Limits |
Mostly available (
LMS-573).POST /users,GET /users/{id},PATCH /users/{id},DELETE /users/{id}(deactivate), and the additionalGET /userslist filters (group_id,is_active,created_after,created_before,sort_by,sort_order) are live. Writes require thewritescope; deactivate requiresadmin.GET /users/{id}/transcriptis live (LMS-955). Still planned — not yet available (returns404today):GET /users/{id}/enrollments(useGET /api/v1/enrollments?user_id={id}).
Additional List Filters
The following GET /api/v1/users query parameters are available:
| Parameter | Type | Description |
|---|---|---|
is_active | boolean | Filter by active status: true or false |
user_type | string | Filter by audience attribute: internal, partner, customer, contractor, or volunteer (LMS-780) |
group_id | UUID | Filter users belonging to a specific group |
created_after | ISO 8601 | Filter users created after this timestamp |
created_before | ISO 8601 | Filter users created before this timestamp |
sort_by | string | Field to sort by: created_at, last_name, email. Default: created_at |
sort_order | string | asc or desc. Default: desc |
The write API also extends the User object with group_ids, custom_fields,
last_login_at, and updated_at fields, as shown in the examples below.
Create User
POST /api/v1/usersCreates a new user in the tenant. API-created users have no password set — they authenticate via SSO, or set a password through the password-reset (invite-link) flow. No email is sent on creation.
Authentication required: write scope
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | User’s email address. Must be unique within the tenant |
first_name | string | Yes | First name |
last_name | string | Yes | Last name |
role | string | Yes | learner, instructor, manager, group_admin, ecommerce_admin, admin, owner — see Role vocabulary. Assigning manager, group_admin, ecommerce_admin, admin, or owner requires the admin scope |
is_active | boolean | No | Whether the user can sign in. Default: true |
external_id | string | No | Your system’s identifier (e.g. HRIS employee id) |
user_type | string | No | internal (default), partner, customer, contractor, or volunteer |
group_ids | UUID[] | No | Array of group UUIDs to assign on creation (max 100) |
custom_fields | object | No | Key-value pairs matching your tenant’s configured custom fields |
locale | string | No | User’s preferred language: en or fr-CA. Default: en |
Example Request
curl -X POST https://{your-subdomain}.embaylms.com/api/v1/users \
-H "Authorization: Bearer ems_live_0000000000000000000000000000" \
-H "Content-Type: application/json" \
-d '{
"email": "john.smith@acme.com",
"first_name": "John",
"last_name": "Smith",
"role": "learner",
"external_id": "EMP-00413",
"group_ids": ["b4c3d2e1-5f6a-7890-bcde-f01234567890"],
"custom_fields": { "employee_id": "EMP-00413" }
}'Example Response
{
"data": {
"id": "c5d4e3f2-6a7b-8901-cdef-012345678901",
"email": "john.smith@acme.com",
"first_name": "John",
"last_name": "Smith",
"role": "learner",
"is_active": true,
"external_id": "EMP-00413",
"group_ids": ["b4c3d2e1-5f6a-7890-bcde-f01234567890"],
"custom_fields": { "employee_id": "EMP-00413" },
"created_at": "2026-06-05T10:00:00Z",
"updated_at": "2026-06-05T10:00:00Z"
},
"meta": null,
"error": null
}HTTP Status: 201 Created
Common Errors
| HTTP Status | Description |
|---|---|
400 | Missing required field or invalid value |
409 | A user with this email already exists in the tenant |
422 | Creating this user would exceed your plan’s seat cap — see Plan limits |
Plan limits
Creating an active user consumes a seat. If your plan carries a hard seat cap
and you are already at it, the request is rejected with 422 and the error object
carries a machine-readable entitlement_blocked payload so you can act on it
without parsing the message:
{
"data": null,
"meta": null,
"error": {
"code": 422,
"message": "The free plan allows 5 active users. Upgrade to add more.",
"entitlement_blocked": {
"code": "ENTITLEMENT_BLOCKED",
"flag_or_limit": "seat_cap",
"current_tier": "free",
"required_tier": "starter"
}
}
}Creating a user with "is_active": false is never gated — an inactive account
consumes no seat. Paid plans have no hard seat cap. This is the same limit the
admin UI enforces, so the API and the app behave identically.
Get User
GET /api/v1/users/{id}Returns a single user by their UUID.
Authentication required: read scope
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | UUID | Yes | User UUID |
Example Request
curl "https://{your-subdomain}.embaylms.com/api/v1/users/a3f2c1d0-4e5b-6789-abcd-ef0123456789" \
-H "Authorization: Bearer ems_live_0000000000000000000000000000"Example Response
{
"data": {
"id": "a3f2c1d0-4e5b-6789-abcd-ef0123456789",
"email": "jane.doe@acme.com",
"first_name": "Jane",
"last_name": "Doe",
"role": "learner",
"is_active": true,
"group_ids": ["b4c3d2e1-5f6a-7890-bcde-f01234567890"],
"custom_fields": { "employee_id": "EMP-00412" },
"last_login_at": "2026-06-04T14:22:11Z",
"created_at": "2026-01-15T09:00:00Z",
"updated_at": "2026-06-04T14:22:11Z"
},
"meta": null,
"error": null
}Common Errors
| HTTP Status | Description |
|---|---|
404 | No user with this ID in the tenant |
Update User
PATCH /api/v1/users/{id}Updates one or more fields on an existing user. Only fields included in the request body are changed.
Authentication required: write scope
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | UUID | Yes | User UUID |
Request Body
All fields are optional. Include only fields you want to change.
| Field | Type | Description |
|---|---|---|
first_name | string | First name |
last_name | string | Last name |
role | string | learner, instructor, manager, group_admin, ecommerce_admin, admin, owner — see Role vocabulary. Assigning manager, group_admin, ecommerce_admin, admin, or owner requires the admin scope |
is_active | boolean | true (can sign in) or false (deactivated) |
external_id | string | null | Your system’s identifier; null to clear |
custom_fields | object | null | Replaces the user’s custom fields object; null to clear |
locale | string | en or fr-CA |
Group membership is not managed here — use the
Groups API (POST / DELETE /api/v1/groups/{id}/members) to add or
remove a user from groups.
Example Request
curl -X PATCH "https://{your-subdomain}.embaylms.com/api/v1/users/a3f2c1d0-4e5b-6789-abcd-ef0123456789" \
-H "Authorization: Bearer ems_live_0000000000000000000000000000" \
-H "Content-Type: application/json" \
-d '{ "role": "instructor", "custom_fields": { "department": "L&D" } }'Example Response
{
"data": {
"id": "a3f2c1d0-4e5b-6789-abcd-ef0123456789",
"email": "jane.doe@acme.com",
"first_name": "Jane",
"last_name": "Doe",
"role": "instructor",
"is_active": true,
"external_id": "EMP-00412",
"custom_fields": { "employee_id": "EMP-00412", "department": "L&D" },
"last_login_at": "2026-06-04T14:22:11Z",
"created_at": "2026-01-15T09:00:00Z",
"updated_at": "2026-06-05T10:05:00Z"
},
"meta": null,
"error": null
}Deactivate User
DELETE /api/v1/users/{id}Deactivates a user. This is a soft delete — the user’s record, learning history, completions, and certificates are preserved. The user cannot log in after deactivation.
Authentication required: admin scope
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | UUID | Yes | User UUID |
Example Request
curl -X DELETE "https://{your-subdomain}.embaylms.com/api/v1/users/a3f2c1d0-4e5b-6789-abcd-ef0123456789" \
-H "Authorization: Bearer ems_live_0000000000000000000000000000"Example Response
HTTP Status: 204 No Content
Get User Enrollments
GET /api/v1/users/{id}/enrollments⚠️ Planned — not yet available (returns
404today).
Returns all enrollments for a specific user.
Live alternative:
GET /api/v1/enrollments?user_id={id}— see Enrollments.
Authentication required: read scope
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | UUID | Yes | User UUID |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | No | Filter: active, completed, dropped, expired, waitlisted, pending_approval |
page | integer | No | Default: 1 |
per_page | integer | No | Default: 50, max: 200 |
Example Request
curl "https://{your-subdomain}.embaylms.com/api/v1/users/a3f2c1d0-4e5b-6789-abcd-ef0123456789/enrollments?status=active" \
-H "Authorization: Bearer ems_live_0000000000000000000000000000"Example Response
{
"data": [
{
"id": "d6e5f4a3-7b8c-9012-defa-123456789012",
"course_id": "e7f6a5b4-8c9d-0123-efab-234567890123",
"course_title": "Workplace Safety Fundamentals",
"status": "active",
"progress_percent": 65,
"enrolled_at": "2026-05-01T08:00:00Z",
"due_date": "2026-07-01T00:00:00Z",
"completed_at": null,
"score": null
}
],
"meta": { "page": 1, "per_page": 50, "total": 8 },
"error": null
}Get User Transcript
GET /api/v1/users/{id}/transcriptReturns the full learning transcript for a user — every course and learning-path enrollment with status, dates, best score, time spent, certificate and CEU. It is the same data as the learner’s Transcript page and its PDF/CSV exports (LMS-955).
Authentication required: read scope
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | UUID | Yes | User UUID |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
completed_after | ISO 8601 | No | Only completions on/after this instant (drops unfinished records) |
completed_before | ISO 8601 | No | Only completions on/before this instant |
status | string | No | completed, in_progress, other (dropped/expired), or all (default) |
type | string | No | course, path, or all (default) |
year | integer | No | Completion year (enrollment year for unfinished records) |
format | string | No | json (default) or csv (triggers a download; RFC 4180, UTF-8 with BOM) |
Example Request
curl "https://{your-subdomain}.embaylms.com/api/v1/users/a3f2c1d0-4e5b-6789-abcd-ef0123456789/transcript" \
-H "Authorization: Bearer ems_live_0000000000000000000000000000"Example Response
{
"data": {
"user_id": "a3f2c1d0-4e5b-6789-abcd-ef0123456789",
"generated_at": "2026-09-11T10:00:00Z",
"totals": { "completed": 12, "in_progress": 2, "time_spent_seconds": 41400, "ceu_total": 6.5, "ceu_label": "CEU", "certificates": 9 },
"items": [
{
"id": "d6e5f4a3-7b8c-9012-defa-123456789012",
"type": "course",
"item_id": "e7f6a5b4-8c9d-0123-efab-234567890123",
"title": "WHMIS 2015",
"status": "completed",
"started_at": "2026-02-01T09:00:00Z",
"completed_at": "2026-03-15T11:30:00Z",
"due_date": null,
"expires_at": "2027-03-15T00:00:00Z",
"score_percent": 92,
"time_spent_seconds": 5400,
"progress_percent": 100,
"certificate_id": "f8a7b6c5-9d0e-1234-fabc-345678901234",
"certificate_number": "CERT-2026-AB12C",
"ceu_value": 1.5,
"ceu_label": "CEU"
}
]
},
"meta": { "total": 14 },
"error": null
}Bulk Create / Update Users
⚠️ Planned — not yet available (returns
404today; part of the LMS-539 bulk API). For bulk operations that are live today, usePOST /api/v1/enrollments/bulk(see Enrollments) or provision users via SCIM 2.0.
POST /api/v1/users/bulkCreates or updates up to 500 users in a single request. Matched on email — if a user with the given email exists, they are updated; otherwise, a new user is created.
Authentication required: write scope
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
users | User[] | Yes | Array of user objects (max 500) |
send_invites | boolean | No | Send invitation emails to newly created users. Default: false |
on_conflict | string | No | update (default) or skip — what to do when a user already exists |
Each user object in the array accepts the same fields as Create User.
Example Request
curl -X POST https://{your-subdomain}.embaylms.com/api/v1/users/bulk \
-H "Authorization: Bearer ems_live_0000000000000000000000000000" \
-H "Content-Type: application/json" \
-d '{
"users": [
{
"email": "alice.martin@acme.com",
"first_name": "Alice",
"last_name": "Martin",
"role": "learner",
"custom_fields": { "employee_id": "EMP-00414" }
},
{
"email": "bob.chen@acme.com",
"first_name": "Bob",
"last_name": "Chen",
"role": "learner",
"custom_fields": { "employee_id": "EMP-00415" }
}
],
"send_invites": false
}'Example Response
{
"data": {
"created": 1,
"updated": 1,
"skipped": 0,
"errors": [],
"users": [
{
"email": "alice.martin@acme.com",
"id": "09a8b7c6-1d2e-3456-abcd-567890123456",
"result": "created"
},
{
"email": "bob.chen@acme.com",
"id": "1a2b3c4d-5e6f-7890-bcde-678901234567",
"result": "updated"
}
]
},
"meta": null,
"error": null
}If individual rows fail validation, they are reported in errors and the rest of the batch is processed:
{
"errors": [
{
"row": 3,
"email": "invalid-not-an-email",
"message": "email: Invalid email address"
}
]
}HTTP Status: 207 Multi-Status when there are partial errors; 201 when all succeed.
Common Errors
| HTTP Status | Description |
|---|---|
400 | users array is empty or exceeds 500 |
429 | Bulk endpoint limit exceeded |