Skip to main content

Users

Base URL: https://api-sls.platzi.com/production-sls-business-domains

License States​

StateValueMeaning
FREE0Available for assignment
RESERVED1Assigned to a user but not yet activated
ACTIVE2Currently in use
USED3Was active, now consumed
EXPIRED4Plan-level expiration

Each user in the request has a licenses field:

  • licenses = 0 → No licenses assigned (user invited as INACTIVE)
  • licenses = N (N > 0) → Reserve N licenses

POST /{company_id} - Add/create Users​

Activate one or more users in a company. The request is processed asynchronously always.

Rules:

  • Each user must provide exactly one of email, username, or phone_number
  • username and phone_number are special cases. Not enabled by default.
  • Providing multiple identifiers per user is rejected
  • For username or phone_number users, a deterministic fake email is generated: {identifier}+{companyId}0@{companyId}.com
  • If an already existing user is provided, licenses are added to the user, not modified.

Note: To create users with username or phone_number, you must contact your account manager to activate the company_allow_username_phonenumber setting for your company.

Request​

POST /v3/users/{company_id}

{
"users": [
{
"email": "user@company.com",
"name": "User Name",
"licenses": 1,
"learning_path_ids": [10, 20],
"group_ids": [5],
"is_admin": false,
"next_url": "/custom/path",
"gender": "female",
"external_id": "EMP-001",
"metadata": {
"document_type": "CC",
"document_number": "1020304050",
"empresa": "Acme",
"tipo_negocio": "Retail",
"regional": "North",
"distrito": "District 1"
}
}
],
"skip_mail_send": false,
"skip_mail_validation": false
}

User Fields​

FieldTypeRequiredDescription
emailstringconditionalUser email (max 320 chars, auto-lowercased). Required unless company_allow_username_phonenumber setting is active.
usernamestringconditionalUsername identifier (max 150 chars, auto-lowercased). Only accepted when company setting is active.
phone_numberstringconditionalPhone number with country code, e.g. +573001234567 (max 20 chars, format: +{digits}). Only accepted when company setting is active.
namestringnoUser display name (max 255 chars)
licensesintyesLicenses to assign. 0 = invite without licenses (INACTIVE)
learning_path_idsint[]noLearning paths to assign after activation
group_idsint[]noGroups to assign after activation
is_adminboolnoSet user as company admin (default: false)
genderstringnoUser gender. Accepted values: male, female, no_gender (case-insensitive). Any other value is ignored (not saved) and does not block the request. Saved on the user's Platzi account; only applied when the account is created.
metadataobjectnoAttributes for this user, from the schema defined by Platzi for your company (see User Metadata). Only keys in that schema are accepted; unknown keys are rejected.
external_idstring | numbernoYour own identifier for this user (previous LMS, HR system). Max 128 chars, unique per company (see External Id).

User Metadata​

Optional attributes attached to a user. The set of accepted fields is a defined schema managed by Platzi and configured specifically for your company rather than a free-form object: this allowlist safeguards your company's data, keeps values consistent across the platform, and prevents arbitrary or malformed keys from being stored.

Each company has its own set of metadata fields — there is no shared, platform-wide list of keys. The exact fields available to you (their keys and labels) are agreed with Platzi and set up for your company.

Rules:

  • Sending a key outside your company's schema rejects the whole request with a 400.
  • If your company has no metadata schema configured, any metadata sent is rejected. Metadata is opt-in per company: it must be set up first.
  • Keys may be sent using either their identifier or their label.
  • Values are trimmed, empty values are dropped, and each value may be at most 255 characters.

Need metadata fields, or changes to the ones you have? They must be configured for your company by Platzi before they can be sent — new keys are not accepted automatically.

On re-invite, metadata is merged: it is only overwritten when the request supplies it, so a re-invite without metadata keeps the existing values. The stored values are returned by GET /{company_id} under the user_metadata field (the request accepts it as metadata; the response exposes it as user_metadata to avoid clashing with the response-level pagination metadata).

External Id​

If your company already identifies its people in another system (a previous LMS, an HR platform, an employee directory), you can attach that identifier to each user when creating them. It is stored on the user's membership in your company and returned in every user listing, so you can reconcile Platzi data against your own records — for example when importing progress history through the Progress Migration API, or when matching reports to your HR system.

Rules:

  • Fully optional. Users created without external_id behave exactly as before; nothing changes for existing integrations.
  • String or number. Both are accepted and normalized to the string form: 9101 and "9101" are the same id. Max 128 characters.
  • Unique per company. Two users in the same company cannot share an external_id. Sending an id already linked to a different user rejects the whole request with EXTERNAL_ID_TAKEN before any user is processed.
  • Idempotent on re-invite. Re-sending the same id for the same user is a no-op. Sending a new id for a user that already has one replaces it. Omitting the field keeps the stored value untouched.
  • Reassignment in one request. You can move an id from one user to another in a single request as long as the current holder receives a different id in the same users array (its old id is released first).
  • The same person can have a different external_id in each company — the id belongs to the company relationship, not to the Platzi account.

The stored value is returned by GET /{company_id} (and by this endpoint's response) under the external_id field.

Identifier Rules​

The identifier behavior depends on a company setting. Ask to Platzi organizers to enable them.

Settingemailusernamephone_numberBehavior
InactiverequiredrejectedrejectedStandard email-only invite
Activeone ofone ofone ofExactly one identifier per user

When the setting is active:

  • Each user must provide exactly one of email, username, or phone_number
  • Providing multiple identifiers per user is rejected
  • For username or phone_number users, a deterministic fake email is generated: {identifier}+{companyId}0@{companyId}.com
  • skip_mail_send and skip_mail_validation are automatically forced to true for the entire batch if any user uses username or phone_number

Request Options​

FieldTypeDefaultDescription
skip_mail_sendboolfalseSkip sending activation email. Auto-forced to true when batch contains username/phone users.
skip_mail_validationboolfalseSkip ZeroBounce email validation. Auto-forced to true when batch contains username/phone users.

Validation​

  • Identifiers must be unique within the request (duplicate emails, usernames, or phone numbers rejected)
  • Email format validated via regex (when provided)
  • Phone number format: + followed by 7-15 digits
  • external_id values must be unique within the request, and must not be linked to a different user in the company

Query Parameters​

ParameterTypeDefaultDescription
pageint1Page of the affected users to return
page_sizeint50Results per page (1–100, capped at 100)

Success Response (200)​

data is a paginated list of the CompanyUser rows set to processing (same shape as GET /{company_id} — see Response Fields below). Execution bookkeeping sits under metadata.execution; pagination metadata for data sits alongside it.

{
"data": [
{
"company_user_id": 603362,
"user_id": null,
"name": "User Name",
"email": "user@company.com",
"avatar": "https://static.platzi.com/static/website/v2/images/avatar_default.7516253fc982.png",
"start_date": "",
"end_date": "",
"state": "processing",
"error_code": null,
"error_reason": null,
"renew_months": 0,
"months_used": 0,
"total_months": 0,
"role": "student",
"profile_url": null,
"license_extension": false,
"resendable_invitation": false,
"last_invitation_sent_at": null,
"courses": 0,
"start_date_first_month": null,
"user_metadata": null,
"external_id": "EMP-001"
}
],
"metadata": {
"execution": {
"success": true,
"message": "Activation queued for 3 user(s). 3 marked as processing.",
"users_processing": 3,
"execution_arn": "arn:aws:states:us-east-1:123:execution:sm:run-id"
},
"count": 3,
"pages": 1,
"current_page": 1,
"page_size": 50
},
"errors": []
}

Newly-created users appear with user_id: null until the processing job creates their account.

Error Response (400)​

{
"data": null,
"errors": [
{
"code": "NOT_ENOUGH_LICENSES",
"message": "Not enough licenses. Required: 5, Available: 2"
}
]
}

Pre-validation Error Codes​

CodeDescription
NOT_FLEXIBLE_PLANCompany does not have a flexible plan
PLAN_EXPIREDCompany plan has expired (only when licenses are requested)
NOT_ENOUGH_LICENSESNot enough FREE licenses for the total licenses requested
INVALID_REQUESTCompany not found or malformed request body
EXTERNAL_ID_TAKENAn external_id in the request is already linked to a different user in the company

Manager Error Codes (403 / 400)​

Only apply when the caller holds the manager role.

CodeStatusDescription
manager_cannot_grant_admin403A user in the request carries is_admin: true. Refused whatever the manager's reach
permission_disabled403A user in the request carries learning_path_ids and the account switched off learning_paths.assign
out_of_scope403A group_ids entry is outside the manager's groups. The errors entry lists the refused ids in denied
no_group_scope403The manager has no groups, and users.invite is not widened to the account
GROUP_REQUIRED400The manager covers several groups and a user carries no group_ids, so the target cannot be inferred

Inviting and assigning content are separate permissions: users.invite admits the request, and learning_path_ids additionally requires learning_paths.assign. A manager who may onboard people but not hand out content gets the invite through with the paths refused — the whole request, not part of it.

Group rules apply only while users.invite is scoped to the manager's groups. Widened to the account, group_ids may name any group and may be omitted entirely, so neither no_group_scope nor GROUP_REQUIRED can occur; nothing else about the request changes.

POST /{company_id}/validate previews this endpoint and answers the same codes, except the ones about learning_path_ids — the preview body has no such field. The preview itself rides on users.view, not users.invite: a manager whose invite permission is switched off can still call it to resolve emails to existing users (the assignment flows depend on that), and their preview is then bounded by the reach of users.view. While the caller holds users.invite, the preview answers exactly what the invite would.

Sandbox Companies​

For sandbox companies (is_sandbox = true), real emails are automatically transformed: user@domain.com becomes user+sandbox@domain.com. This prevents sandbox users from affecting real user accounts. Fake emails (from username/phone invites) are not modified.


PUT /{company_id} - Update Licenses​

Update the number of reserved licenses for existing users. The request is processed asynchronously. Unlike invite (POST), licenses is the target count of RESERVED licenses (not additive). The same /{company_id} path is used — the HTTP verb distinguishes the operation: POST to invite, PUT to update.

Request​

PUT /v3/users/{company_id}/

{
"users": [
{ "company_user_id": 603362, "licenses": 2 },
{ "company_user_id": 603361, "licenses": 3 }
]
}

User Fields​

FieldTypeRequiredDescription
company_user_idintyesExisting CompanyUser ID
licensesintyesTarget number of RESERVED licenses (>= 0)

Per-User Validation​

Each user is validated individually before the async process starts. Invalid users are returned in the errors array while valid users proceed to async processing:

ConditionError
company_user_id not found in companyCould not find company user {id} (404)
CompanyUser in error stateCompany user {id} is in error state (400)
CompanyUser in processing stateCompany user {id} is in processing state (400)

Query Parameters​

ParameterTypeDefaultDescription
pageint1Page of the affected users to return
page_sizeint50Results per page (1–100, capped at 100)

Hybrid Response (200)​

data is a paginated list of the CompanyUser rows set to processing (same shape as GET /{company_id}). Execution bookkeeping sits under metadata.execution; pre-validation per-user errors ride in errors:

{
"data": [
{
"company_user_id": 603362,
"user_id": 12345,
"name": "User Name",
"email": "user@company.com",
"avatar": "https://static.platzi.com/media/avatars/user.jpg",
"start_date": "2025-01-15T00:00:00",
"end_date": "2025-12-31T23:59:59.999999",
"state": "processing",
"error_code": null,
"error_reason": null,
"renew_months": 1,
"months_used": 2,
"total_months": 3,
"role": "student",
"profile_url": "https://platzi.com/@username/",
"license_extension": false,
"resendable_invitation": false,
"last_invitation_sent_at": null,
"courses": 8,
"start_date_first_month": null
}
],
"metadata": {
"execution": {
"success": true,
"message": "Update queued for 3 user(s). 3 marked as processing.",
"users_processing": 3,
"execution_arn": "arn:aws:states:..."
},
"count": 3,
"pages": 1,
"current_page": 1,
"page_size": 50
},
"errors": [
{
"company_user_id": 999,
"message": "Could not find company user 999",
"error_code": 404
},
{
"company_user_id": 888,
"message": "Company user 888 is in error state",
"error_code": 400
}
]
}

If every requested user fails pre-validation, data is [], metadata.execution.users_processing is 0, and metadata.execution.execution_arn is null.

Pre-validation Error Codes​

CodeDescription
NOT_FLEXIBLE_PLANCompany does not have a flexible plan
PLAN_EXPIREDCompany plan limit_date < today (only when any licenses > 0)
NOT_ENOUGH_LICENSESNot enough FREE licenses for total increase needed
INVALID_REQUESTInvalid request body or empty users array

DELETE /{company_id} - Delete/Deactivate Users​

Delete or deactivate one or more users from a company. The request is processed asynchronously. The delete flag controls the mode: true (default) hard-deletes the user from the company and all associations; false deactivates (sets INACTIVE, releases licenses, preserves associations).

On a flexible plan, deactivating a user whose current month is still running only returns their RESERVED months to the company. The ACTIVE month is kept: the user stays active with access until its end_date, and becomes inactive automatically the day after. Assigning months again before then simply continues their access. Hard delete (delete=true) ends the current month immediately.

Request​

DELETE /v3/users/{company_id}

{
"company_user_ids": [603362, 603361],
"delete": true
}

Fields​

FieldTypeRequiredDefaultDescription
company_user_idsint[]yes—List of CompanyUser IDs to delete/deactivate
deleteboolnotruetrue = hard delete, false = deactivate

Validation​

  • company_user_ids must not be empty
  • company_user_ids must be unique (duplicates rejected with 400)

Per-User Validation​

Each user is validated individually before the async process starts. Invalid users are returned in the errors array while valid users proceed to async processing:

ConditionError
company_user_id not found in companyCould not find company user {id} (404)
CompanyUser in processing stateCompany user {id} is already processing (400)
CompanyUser in error state AND delete=falseCannot deactivate company user {id} in error state, use delete=true to remove (400)

Note: Error state users CAN be hard-deleted (delete=true). They cannot be deactivated (delete=false).

Query Parameters​

ParameterTypeDefaultDescription
pageint1Page of the affected users to return
page_sizeint50Results per page (1–100, capped at 100)

Hybrid Response (200)​

data is a paginated list of the CompanyUser rows set to processing. Execution bookkeeping sits under metadata.execution; pre-validation per-user errors ride in errors:

{
"data": [
{
"company_user_id": 603362,
"user_id": 12345,
"name": "User Name",
"email": "user@company.com",
"avatar": "https://static.platzi.com/media/avatars/user.jpg",
"start_date": "2025-01-15T00:00:00",
"end_date": "2025-12-31T23:59:59.999999",
"state": "processing",
"error_code": null,
"error_reason": null,
"renew_months": 1,
"months_used": 2,
"total_months": 3,
"role": "student",
"profile_url": "https://platzi.com/@username/",
"license_extension": false,
"resendable_invitation": false,
"last_invitation_sent_at": null,
"courses": 8,
"start_date_first_month": null
}
],
"metadata": {
"execution": {
"success": true,
"message": "Delete queued for 2 user(s). 2 marked as processing.",
"users_processing": 2,
"execution_arn": "arn:aws:states:..."
},
"count": 2,
"pages": 1,
"current_page": 1,
"page_size": 50
},
"errors": [
{
"company_user_id": 999,
"message": "Could not find company user 999",
"error_code": 404
}
]
}

If every requested user fails pre-validation, data is [], metadata.execution.users_processing is 0, and metadata.execution.execution_arn is null.

Pre-validation Error Codes​

CodeCause
INVALID_REQUESTCompany not found or malformed body

POST /{company_id}/administrators - Add Company Admins​

Add one or more users as company administrators. Synchronous endpoint.

Request​

POST /v3/users/{company_id}/administrators

{
"company_user_ids": [10, 20, 30]
}

Fields​

FieldTypeRequiredDescription
company_user_idsint[]yesList of CompanyUser IDs to promote

Validation​

  • company_user_ids must not be empty
  • company_user_ids must be unique (duplicates rejected with 422)

Success Response (200)​

{
"data": [
{ "company_user_id": 10, "user_id": 100, "created": true },
{ "company_user_id": 20, "user_id": 200, "created": false }
]
}

created: false means user was already an admin (idempotent).

Partial Failure Response (409)​

{
"data": [{ "company_user_id": 10, "user_id": 100, "created": true }],
"errors": [
{
"company_user_id": 999,
"error": "Company user not found or has no linked user",
"error_code": "not_found"
}
]
}

All-Fail Response (404)​

{
"data": null,
"errors": [{ "code": "NOT_FOUND", "message": "No valid company users found" }]
}

Error Codes​

CodeDescription
not_foundCompanyUser not found or has no linked user_id

DELETE /{company_id}/administrators - Remove Company Admins​

Remove one or more users as company administrators. Synchronous endpoint. Also deactivates report subscriptions for the removed admin.

Request​

DELETE /v3/users/{company_id}/administrators

{
"company_user_ids": [10, 20, 30]
}

Fields​

FieldTypeRequiredDescription
company_user_idsint[]yesList of CompanyUser IDs to demote

Validation​

  • company_user_ids must not be empty
  • company_user_ids must be unique (duplicates rejected with 422)

Success Response (200)​

{
"data": [{ "company_user_id": 10, "user_id": 100, "removed": true }]
}

Partial Failure Response (409)​

{
"data": [{ "company_user_id": 10, "user_id": 100, "removed": true }],
"errors": [
{
"company_user_id": 20,
"error": "User is not an administrator",
"error_code": "not_admin"
},
{
"company_user_id": 999,
"error": "Company user not found or has no linked user",
"error_code": "not_found"
}
]
}

All-Fail Response (404)​

{
"data": null,
"errors": [
{
"code": "NOT_FOUND",
"message": "No valid administrators found to remove"
}
]
}

Error Codes​

CodeDescription
not_foundCompanyUser not found or has no linked user_id
not_adminUser is not an administrator of this company

Manager Role​

A manager administers only the users of the company teams (groups) granted to them, instead of the whole company. Managers can invite, deactivate, assign learning paths and consume company licenses within their groups, and cannot create content, manage administrators or API keys, or delete users permanently.

Requests outside the granted groups are refused with 403 out_of_scope; a manager with no groups assigned gets 403 no_group_scope.

Permissions and their reach​

Each ability a manager has is a named permission an account administrator can switch off, and a request blocked that way answers 403 permission_disabled naming the permission. This is different from 403 manager_forbidden, which means the platform does not offer the action to the role at all and nobody in the account can change it.

Some permissions can additionally be widened to the whole account, so the manager exercises that one ability everywhere while the rest stay confined to their groups:

PermissionWhat widening it does
users.viewReads every user in the account
groups.viewReads every group
learning_paths.viewReads every learning path
objectives.viewReads every objective, and lets a bulk objective upload assign — and an objective deletion reach — anybody in the account
users.inviteInvites without naming a group, and into any group
learning_paths.assignAssigns a path to any user or group
courses.assignAssigns a course to any user or group
reports.downloadExports reports, and schedules their delivery, covering the whole account

A widened permission changes only itself. A manager whose users.invite reaches the account still cannot deactivate somebody outside their groups, and a widened manager can never grant the administrator role through an invite — manager_cannot_grant_admin applies regardless of reach.

Widening a write without widening the read it depends on produces a working permission with nothing to point it at: learning_paths.assign across the account needs users.view across the account, or the manager may assign only to people they can still see.

There is no separate endpoint to list managers — use the users listing with ?role=manager (see GET /{company_id}). Each manager row carries a company_teams array with the groups it covers.

POST /{company_id}/managers - Grant Manager Role​

Grant the group-scoped manager role to one or more users. Synchronous endpoint.

Request​

POST /v3/users/{company_id}/managers

{
"company_user_ids": [10, 20],
"company_team_ids": [301, 302]
}

Fields​

FieldTypeRequiredDescription
company_user_idsint[]yesCompanyUser IDs receiving the role (max 100)
company_team_idsint[]yesGroups the manager will cover (1-50)

Validation​

  • Both lists must be non-empty and free of duplicates (422 otherwise)
  • company_team_ids must belong to the company and be active
  • A user who is already an administrator is rejected: demote them first

Success Response (200)​

{
"data": [
{
"company_user_id": 10,
"user_id": 100,
"created": true,
"company_team_ids": [301, 302]
}
]
}

Error Codes​

CodeStatusDescription
INVALID_TEAM400A group is not from this company, or is not active
ALREADY_ADMIN404The user already administers this company
not_found404CompanyUser not found or has no linked user_id

Partial success returns 409 with both data and errors, as in the administrators endpoints.

PUT /{company_id}/managers - Replace Manager Groups​

Replace the groups an existing manager covers. Same body and validation as POST. A user without an active grant is reported with not_manager.

Success Response (200)​

{
"data": [
{ "company_user_id": 10, "user_id": 100, "company_team_ids": [303] }
]
}

DELETE /{company_id}/managers - Revoke Manager Role​

Revoke the role. Deletes the grant and its group links, and deactivates the user's report subscriptions so they stop receiving reports built from the old scope.

Request​

DELETE /v3/users/{company_id}/managers

{
"company_user_ids": [10]
}

Success Response (200)​

{
"data": [{ "company_user_id": 10, "user_id": 100, "removed": true }]
}

Error Codes​

CodeStatusDescription
not_manager404The user holds no manager grant here
not_found404CompanyUser not found or has no linked user_id

GET /{company_id}/managers/me - Own Manager Scope​

Returns the caller's own scope. Answers for every role, so a client can call it before deciding what to render.

Success Response (200)​

{
"data": {
"is_manager": true,
"company_team_ids": [301, 302],
"company_teams": [{ "id": 301, "name": "North Office", "color": "#112233" }]
}
}

An administrator or organizer gets {"is_manager": false, "company_teams": []}.


GET /{company_id} - List Company Users​

Returns a paginated, filterable list of company users. Read-only synchronous endpoint.

Request​

GET /v3/users/{company_id}?page=1&page_size=50&state=active&search=john&ordering=-name

Query Parameters​

ParameterTypeDefaultDescription
pageint1Page number (>= 1)
page_sizeint50Results per page (1–100, capped at 100)
statestring—Filter by state: matriculate, active, inactive, error, processing
searchstring—Case-insensitive search on name and email
rolestring—'admin' or 'student'
teamstring—Filter by team name (active teams only)
learning_path_idint—Filter by active enrollment in a learning path
include_groupsstringfalse'true' attaches each row's groups (see Response Fields)
license_extensionstring—'true'/'false' — pending license extension
resendable_invitationstring—'true'/'false' — eligible for resend (10-day cooldown)
company_user_idstring—Comma-separated CompanyUser IDs to include
not_company_user_idstring—Comma-separated CompanyUser IDs to exclude
user_idstring—Comma-separated auth user IDs to include
orderingstring-idMulti-field ordering, prefix - for DESC

Ordering Fields​

Supported values for the ordering parameter. Prefix with - for descending order.

name, email, state, end_date, renew_months, months_used, months_active, resendable_invitation, courses, id (default)

Response (200)​

{
"data": [
{
"company_user_id": 603362,
"user_id": 12345,
"name": "John Doe",
"email": "john@company.com",
"avatar": "https://static.platzi.com/media/avatars/john.jpg",
"start_date": "2025-01-15T00:00:00",
"end_date": "2025-12-31T23:59:59.999999",
"state": "active",
"error_code": null,
"error_reason": null,
"renew_months": 2,
"months_used": 5,
"total_months": 6,
"role": "admin",
"profile_url": "https://platzi.com/@johndoe/",
"license_extension": false,
"resendable_invitation": false,
"last_invitation_sent_at": null,
"courses": 15,
"start_date_first_month": "2025-01-15T00:00:00",
"user_metadata": {
"document_type": "CC",
"document_number": "1020304050",
"regional": "North"
},
"external_id": "EMP-001"
}
],
"metadata": {
"count": 150,
"pages": 8,
"current_page": 1,
"page_size": 20
}
}

Response Fields​

FieldDescription
nameDisplay name. Shows 'Nuevo Estudiante' if not set
emailUser's email address
avatarAvatar URL or default avatar
start_dateStart of current activity window
end_dateEnd of current activity window (extended by reserved months)
error_codeRaw error code (string) or null when there is no error (see Error Code Mapping below)
error_reasonHuman-readable explanation of error_code, or null when there is no error
renew_monthsNumber of RESERVED licenses
months_usedNumber of ACTIVE + USED licenses
total_monthsrenew_months + months_active + months_used
role'admin' or 'student'
profile_urlPlatzi profile URL or null
license_extensionTrue if user has a pending license extension request and is active or inactive
resendable_invitationTrue if eligible for invitation resend (10-day cooldown)
start_date_first_monthStart date of first license (active/inactive states only)
user_metadataCustom attributes for the user, or null when none were set. Same keys as the request metadata (see User Metadata)
external_idYour own identifier for this user, or null when none was set (see External Id)
groupsGroups this user belongs to, as [{id, name, color}]. Only present when include_groups=true, and then on every row — [] means the user belongs to no group. Distinct from company_teams, which appears on manager rows and names the groups they administer

Group Membership​

The listing does not carry groups by default. Ask for it explicitly:

GET /v3/users/{company_id}?include_groups=true&page=1&page_size=50

Each row then gains a groups array:

{
"company_user_id": 603362,
"user_id": 12345,
"name": "John Doe",
"groups": [{ "id": 36046, "name": "Backend", "color": "#0af" }]
}

Only this company's active groups are listed. Rows whose user_id is still null — invitations nobody has accepted yet — always come back with "groups": []: a user has to exist before they can belong to a group.

groups never reaches past what the caller may already read. For a manager limited to certain groups, a user's row lists only those — not every group that user happens to belong to.

Use this instead of calling GET /v3/group-builder/{company_id}/groups/{group_id}/users/ once per group and matching the members back to your users — one request answers for the whole page.

Error Code Mapping​

error_code is the raw value; error_reason is the matching text below. Any code not listed here yields error_reason: null.

error_codeerror_reason
6student has B2C paid subscription
7invalid email
11server error at processing
13multiple profiles found at activation
15B2B account already exists

POST /{company_id}/bulk - Bulk Add or Remove License Months​

Modify reserved license months for many users matching filters. Async operation (Step Function).

Scope: activations.write
Plan: flexible only

Provide exactly one of:

FieldDescription
licenses_to_addAdd N reserved months to each matched user
licenses_to_removeRemove N reserved months from each matched user
access_untilTrim reserved months so projected access ends on this date (YYYY-MM-DD)

Example — remove 2 reserved months​

POST /v3/users/{company_id}/bulk
{
"licenses_to_remove": 2,
"company_user_ids_include": [101, 102, 103]
}

Example — trim all users to a target access date​

Releases enough reserved months per user so projected access does not extend past the date. Users whose active window already ends after the date, or who are already compliant, are reported in errors with code 422 or 409.

POST /v3/users/{company_id}/bulk
{
"access_until": "2026-09-30",
"state": "active"
}

Per-user outcomes:

error_codeMeaning
409User already ends on or before access_until (skipped)
422access_until is before active coverage; only reserved months can be trimmed
422No coverage to trim
400User in error / processing, or insufficient reserved months for fixed licenses_to_remove

Example — add months with filters​

POST /v3/users/{company_id}/bulk
{
"licenses_to_add": 1,
"state": "active",
"team": "Engineering"
}

Users with fewer reserved months than licenses_to_remove are skipped and returned in errors.


POST /{company_id}/extensions/accept-all - Accept All Pending Extensions​

Grant one reserved month to every user with a pending license extension request in the company (same as clicking "Accept" in the licenses table). Async bulk modify under the hood.

Scope: activations.write
Plan: flexible only

POST /v3/users/{company_id}/extensions/accept-all
{}

Optional filters:

{
"company_user_ids_include": [101, 102],
"company_user_ids_exclude": [103]
}

Response shape matches POST /bulk: data (affected users), metadata.execution, and per-user errors when some requests cannot be queued (not enough company licenses, user in error state, etc.).


GET /{company_id}/metadata-fields - List Company Metadata Fields​

Returns the metadata schema a company accepts for its users. Read-only synchronous endpoint.

Use it to discover which extra columns can accompany a user (e.g. before building an invite_users bulk file). An empty fields list means the company accepts no metadata.

Request​

GET /v3/users/{company_id}/metadata-fields

Path Parameters​

NameTypeDescription
company_idintCompany identifier

Success Response​

{
"data": {
"fields": [
{ "key": "area", "label": "Área", "required": false },
{ "key": "sede", "label": "Sede", "required": true }
]
},
"errors": []
}
FieldTypeDescription
keystringStable identifier for the metadata field.
labelstringHuman-readable name (accepted as a column-header alias).
requiredbooleanWhether the field must be provided when metadata is set.

See API Key Scopes.