Users
Base URL: https://api-sls.platzi.com/production-sls-business-domains
License States
| State | Value | Meaning |
|---|---|---|
| FREE | 0 | Available for assignment |
| RESERVED | 1 | Assigned to a user but not yet activated |
| ACTIVE | 2 | Currently in use |
| USED | 3 | Was active, now consumed |
| EXPIRED | 4 | Plan-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, orphone_number usernameandphone_numberare special cases. Not enabled by default.- Providing multiple identifiers per user is rejected
- For
usernameorphone_numberusers, 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
| Field | Type | Required | Description |
|---|---|---|---|
email | string | conditional | User email (max 320 chars, auto-lowercased). Required unless company_allow_username_phonenumber setting is active. |
username | string | conditional | Username identifier (max 150 chars, auto-lowercased). Only accepted when company setting is active. |
phone_number | string | conditional | Phone number with country code, e.g. +573001234567 (max 20 chars, format: +{digits}). Only accepted when company setting is active. |
name | string | no | User display name (max 255 chars) |
licenses | int | yes | Licenses to assign. 0 = invite without licenses (INACTIVE) |
learning_path_ids | int[] | no | Learning paths to assign after activation |
group_ids | int[] | no | Groups to assign after activation |
is_admin | bool | no | Set user as company admin (default: false) |
gender | string | no | User 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. |
metadata | object | no | Attributes 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_id | string | number | no | Your 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
metadatasent 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_idbehave exactly as before; nothing changes for existing integrations. - String or number. Both are accepted and normalized to the string form:
9101and"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 withEXTERNAL_ID_TAKENbefore 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
usersarray (its old id is released first). - The same person can have a different
external_idin 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.
| Setting | email | username | phone_number | Behavior |
|---|---|---|---|---|
| Inactive | required | rejected | rejected | Standard email-only invite |
| Active | one of | one of | one of | Exactly one identifier per user |
When the setting is active:
- Each user must provide exactly one of
email,username, orphone_number - Providing multiple identifiers per user is rejected
- For
usernameorphone_numberusers, a deterministic fake email is generated:{identifier}+{companyId}0@{companyId}.com skip_mail_sendandskip_mail_validationare automatically forced totruefor the entire batch if any user usesusernameorphone_number
Request Options
| Field | Type | Default | Description |
|---|---|---|---|
skip_mail_send | bool | false | Skip sending activation email. Auto-forced to true when batch contains username/phone users. |
skip_mail_validation | bool | false | Skip 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_idvalues must be unique within the request, and must not be linked to a different user in the company
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page of the affected users to return |
page_size | int | 50 | Results 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
| Code | Description |
|---|---|
NOT_FLEXIBLE_PLAN | Company does not have a flexible plan |
PLAN_EXPIRED | Company plan has expired (only when licenses are requested) |
NOT_ENOUGH_LICENSES | Not enough FREE licenses for the total licenses requested |
INVALID_REQUEST | Company not found or malformed request body |
EXTERNAL_ID_TAKEN | An external_id in the request is already linked to a different user in the company |
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
| Field | Type | Required | Description |
|---|---|---|---|
company_user_id | int | yes | Existing CompanyUser ID |
licenses | int | yes | Target 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:
| Condition | Error |
|---|---|
company_user_id not found in company | Could not find company user {id} (404) |
CompanyUser in error state | Company user {id} is in error state (400) |
CompanyUser in processing state | Company user {id} is in processing state (400) |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page of the affected users to return |
page_size | int | 50 | Results 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
| Code | Description |
|---|---|
NOT_FLEXIBLE_PLAN | Company does not have a flexible plan |
PLAN_EXPIRED | Company plan limit_date < today (only when any licenses > 0) |
NOT_ENOUGH_LICENSES | Not enough FREE licenses for total increase needed |
INVALID_REQUEST | Invalid 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).
Request
DELETE /v3/users/{company_id}
{
"company_user_ids": [603362, 603361],
"delete": true
}
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
company_user_ids | int[] | yes | — | List of CompanyUser IDs to delete/deactivate |
delete | bool | no | true | true = hard delete, false = deactivate |
Validation
company_user_idsmust not be emptycompany_user_idsmust 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:
| Condition | Error |
|---|---|
company_user_id not found in company | Could not find company user {id} (404) |
CompanyUser in processing state | Company user {id} is already processing (400) |
CompanyUser in error state AND delete=false | Cannot 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
| Parameter | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page of the affected users to return |
page_size | int | 50 | Results 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
| Code | Cause |
|---|---|
INVALID_REQUEST | Company 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
| Field | Type | Required | Description |
|---|---|---|---|
company_user_ids | int[] | yes | List of CompanyUser IDs to promote |
Validation
company_user_idsmust not be emptycompany_user_idsmust 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
| Code | Description |
|---|---|
not_found | CompanyUser 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
| Field | Type | Required | Description |
|---|---|---|---|
company_user_ids | int[] | yes | List of CompanyUser IDs to demote |
Validation
company_user_idsmust not be emptycompany_user_idsmust 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
| Code | Description |
|---|---|
not_found | CompanyUser not found or has no linked user_id |
not_admin | User 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.
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
| Field | Type | Required | Description |
|---|---|---|---|
company_user_ids | int[] | yes | CompanyUser IDs receiving the role (max 100) |
company_team_ids | int[] | yes | Groups the manager will cover (1-50) |
Validation
- Both lists must be non-empty and free of duplicates (422 otherwise)
company_team_idsmust 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
| Code | Status | Description |
|---|---|---|
INVALID_TEAM | 400 | A group is not from this company, or is not active |
ALREADY_ADMIN | 404 | The user already administers this company |
not_found | 404 | CompanyUser 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
| Code | Status | Description |
|---|---|---|
not_manager | 404 | The user holds no manager grant here |
not_found | 404 | CompanyUser 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
| Parameter | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number (>= 1) |
page_size | int | 50 | Results per page (1–100, capped at 100) |
state | string | — | Filter by state: matriculate, active, inactive, error, processing |
search | string | — | Case-insensitive search on name and email |
role | string | — | 'admin' or 'student' |
team | string | — | Filter by team name (active teams only) |
learning_path_id | int | — | Filter by active enrollment in a learning path |
include_groups | string | false | 'true' attaches each row's groups (see Response Fields) |
license_extension | string | — | 'true'/'false' — pending license extension |
resendable_invitation | string | — | 'true'/'false' — eligible for resend (10-day cooldown) |
company_user_id | string | — | Comma-separated CompanyUser IDs to include |
not_company_user_id | string | — | Comma-separated CompanyUser IDs to exclude |
user_id | string | — | Comma-separated auth user IDs to include |
ordering | string | -id | Multi-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
| Field | Description |
|---|---|
name | Display name. Shows 'Nuevo Estudiante' if not set |
email | User's email address |
avatar | Avatar URL or default avatar |
start_date | Start of current activity window |
end_date | End of current activity window (extended by reserved months) |
error_code | Raw error code (string) or null when there is no error (see Error Code Mapping below) |
error_reason | Human-readable explanation of error_code, or null when there is no error |
renew_months | Number of RESERVED licenses |
months_used | Number of ACTIVE + USED licenses |
total_months | renew_months + months_active + months_used |
role | 'admin' or 'student' |
profile_url | Platzi profile URL or null |
license_extension | True if user has a pending license extension request and is active or inactive |
resendable_invitation | True if eligible for invitation resend (10-day cooldown) |
start_date_first_month | Start date of first license (active/inactive states only) |
user_metadata | Custom attributes for the user, or null when none were set. Same keys as the request metadata (see User Metadata) |
external_id | Your own identifier for this user, or null when none was set (see External Id) |
groups | Groups 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_code | error_reason |
|---|---|
6 | student has B2C paid subscription |
7 | invalid email |
11 | server error at processing |
13 | multiple profiles found at activation |
15 | B2B 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:
| Field | Description |
|---|---|
licenses_to_add | Add N reserved months to each matched user |
licenses_to_remove | Remove N reserved months from each matched user |
access_until | Trim 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_code | Meaning |
|---|---|
409 | User already ends on or before access_until (skipped) |
422 | access_until is before active coverage; only reserved months can be trimmed |
422 | No coverage to trim |
400 | User 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
| Name | Type | Description |
|---|---|---|
company_id | int | Company identifier |
Success Response
{
"data": {
"fields": [
{ "key": "area", "label": "Área", "required": false },
{ "key": "sede", "label": "Sede", "required": true }
]
},
"errors": []
}
| Field | Type | Description |
|---|---|---|
key | string | Stable identifier for the metadata field. |
label | string | Human-readable name (accepted as a column-header alias). |
required | boolean | Whether the field must be provided when metadata is set. |
See API Key Scopes.