Admin Users API
Provisioning and management of administration users — the staff and platform-admin accounts that operate the admin portal.
Every /admin/users endpoint requires an authenticated super_admin (the owner) or admin session; the router is mounted behind a requireRole('super_admin', 'admin') guard, and any other role receives 403 AUTH_FORBIDDEN. An admin then needs the staff-users area: View for the list, Manage to provision. PATCH, DELETE and resend-invite are decided per target, see Access Rules. The acting identity and role are taken from the verified session, never from the request body.
Roles and Areas
The admin portal is partitioned into manageable areas. A user's access is derived from its appRole plus, for the admin role, an explicit set of assigned areas.
Roles
| Role | Areas | Assignable here |
|---|---|---|
super_admin | All areas (owner) | No — owners are not managed here |
admin | The subset assigned via managedAreas (platform-admin) | Yes |
activation_officer | officer, customers (fixed) | Yes |
support | support, customers (fixed) | Yes |
b2b_admin | b2b (fixed) | No — company people are invited through POST /admin/companies/invites |
end_user | None | No |
Roles assignable through this API are exactly the portal staff roles admin, activation_officer, and support. super_admin and end_user can never be assigned here, so an owner cannot mint another owner through this endpoint. b2b_admin cannot be assigned here either: a company person is provisioned only through the company invite, which also links them to their company.
Company people are still managed through PATCH, DELETE and resend-invite on /admin/users/:id, under two extra rules — see Managing company people. They are listed per company by GET /admin/companies/:id/users, and before registration by GET /admin/companies/pending-admins, not by GET /admin/users.
Areas
| Area | Portal section |
|---|---|
activation-records | Activation records dashboard |
packages | Package catalog |
officer | Activation officer tools. Resending a delivery email needs Manage |
support | Customer support |
b2b | B2B company portal |
notifications | Notification administration, gating the admin notification routes. No portal screen yet |
customers | Customer search and record, gating the /admin/customers routes. Fixed for activation_officer and support, assignable to admin |
store | Store order oversight, gating the admin store order routes. Assignable to admin only — no staff role holds it |
identity-detail | Customer identity check detail, gating GET /admin/customers/:id/identity on top of the customers area. Assignable to admin only — no staff role holds it |
audit | Audit history viewer, gating GET /admin/audit-logs. Assignable to admin only — no staff role holds it. Values stay owner only |
Three areas gate people management:
| Area | Portal section |
|---|---|
staff-users | Staff users screen and /admin/users. View lists staff, Manage provisions and edits them. Manage on it is owner-only to grant, and a user holding it can be edited or removed only by the owner |
company-people | Company people: the per-company list and managing their accounts. Needs b2b |
company-onboarding | Invited company admins who have not registered a company yet. Needs b2b |
activation-records, customers, identity-detail and audit are View-only: they are granted at View or not at all, and managedAreasWrite must not include them. A dependent area needs its parent in managedAreas: identity-detail needs customers, and company-people and company-onboarding need b2b.
Support and officer routes are not part of /admin/users. Support ticket triage and refund decisions live under /admin/support and use the support area; the completion tool, fallout queue, officer-scoped tickets and officer refunds live under /admin/officer and use the officer area. Area grants are resolved at request time; the role is carried in the session token, so a role change, deactivation or delete ends the user's live sessions and they sign in again.
Access Rules
The owner is not bound by these checks. For an admin, the service checks every change against the caller's own stored access, and a change outside it returns 403 ADMIN_USER_SCOPE_EXCEEDED.
- Grants stay within your own levels. An
adminmay hand out only a role and areas it holds itself, at the same level or lower. Only the owner grantsstaff-usersManage. - Fixed roles need their module. Creating or moving someone into
activation_officerneedsofficerManage andcustomersView;supportneedssupportManage andcustomersView. - Targets of
PATCH,DELETEandresend-invite. Never yourself. An owner is not a manageable target at all (404 ADMIN_USER_NOT_FOUND). A staff target needsstaff-usersManage, and the caller's levels must cover the target's effective levels. A company person who belongs to a company needscompany-peopleManage; one with no company yet needscompany-onboardingManage. A target outside every area the caller can view answers404 ADMIN_USER_NOT_FOUND, as if it did not exist.
View vs Manage
For the admin role, areas are split into two levels:
managedAreas— the View baseline: areas the admin may open (read).managedAreasWrite— the Manage subset: areas the admin may write to. Must be a subset ofmanagedAreas.
Areas in managedAreas but not in managedAreasWrite are View-only. Fixed staff roles derive their fixed areas from the role and omit both fields. Areas that are View-only always read as View, even if a stored Manage exists. The server resolves these at request time (never frozen in the JWT). A user's effective areas are returned in the user object of GET /auth/session as managedAreas and managedAreasWrite.
Viewer Shaping
Staff-facing reads that render customer data shape it by viewer, independently of which area admitted the caller. The admin user, pending company admin and company people lists follow the same rule for phone and email. The owner (super_admin) sees the stored values. Every other viewer — staff roles and platform admins alike — sees the masked forms, with the same field names and types either way:
- Phone: the stored E.164 value for the owner, a prefix plus the last two digits behind fixed bullets for everyone else (
+9665•••••67). - Email: the stored address for the owner, the first character plus the full domain behind fixed bullets for everyone else (
a•••@example.com). - ICCID: the whole serial for the owner, its last 4 characters for everyone else.
- Operator reference: the stored transaction reference for the owner, its last 4 characters behind fixed bullets for everyone else (
•••c123).
This applies to the customer record, the admin activation records list, and the admin store order views. On a store order the split is sharper: the owner additionally receives the issued profile fields with a masked activation code, while every other viewer receives only the ICCID suffix.
Administration User Object
The shape returned by the list, create, and update endpoints. The list and update responses mask phone and email for every caller but the owner, and return the areas as null for a caller without staff-users Manage; the create response returns the contact as it was submitted.
| Field | Type | Description |
|---|---|---|
id | string (uuid) | User ID |
phone | string | null | Phone in E.164 format; null for email-only users |
email | string | null | Email address; null for phone-only users |
name | string | null | Display name |
role | string | Assigned UserRole |
managedAreas | string[] | null | View areas (admin only; null for staff roles, and in the list and update responses for a caller without staff-users Manage) |
managedAreasWrite | string[] | null | Manage subset (admin only; null for staff roles, and in the list and update responses for a caller without staff-users Manage) |
isActive | boolean | Whether the account is active |
createdAt | string | ISO 8601 creation timestamp |
Success responses are data envelopes (no localized message). Error messages are localized via Accept-Language, and the sign-in invitation sent to a new user is bilingual (English + Arabic).
GET /admin/users
List the portal staff users, newest first. Excludes end_user accounts, super_admin owners, and b2b_admin company people, who are listed per company under B2B Administration.
Requires super_admin, or an admin with staff-users View. Phone and email are masked unless the caller is the owner, and managedAreas and managedAreasWrite are null for a caller without staff-users Manage.
Response
200 OK
{
"data": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"phone": "+966501234567",
"email": null,
"name": "Sara Officer",
"role": "activation_officer",
"managedAreas": null,
"managedAreasWrite": null,
"isActive": true,
"createdAt": "2026-06-29T00:00:00.000Z"
},
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"phone": null,
"email": "product.admin@skyte.sa",
"name": "Platform Admin",
"role": "admin",
"managedAreas": ["packages", "activation-records"],
"managedAreasWrite": ["packages"],
"isActive": true,
"createdAt": "2026-06-28T00:00:00.000Z"
}
]
}Error Responses
| Code | Status | Condition |
|---|---|---|
AUTH_REQUIRED | 401 | Missing token (an invalid or expired token returns AUTH_SESSION_EXPIRED) |
AUTH_FORBIDDEN | 403 | Caller is not super_admin or admin, or lacks the required staff-users level |
POST /admin/users
Provision a new administration user. Requires at least one of phone or email, a name, and a role. For the admin role, managedAreas is required and non-empty; fixed staff roles must omit the area fields. After creation, a bilingual invitation is sent over whichever channel(s) were provided — best-effort, so a delivery failure does not fail creation.
Requires super_admin, or an admin with staff-users Manage and a grant within its own levels (see Access Rules).
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
phone | string | Conditional | Saudi E.164 (+966XXXXXXXXX). At least one of phone or email. |
email | string | Conditional | Email address. At least one of phone or email. |
name | string | Yes | Full name (1–255 characters) |
role | string | Yes | One of admin, activation_officer, support |
managedAreas | string[] | Conditional | Required and non-empty when role is admin; must be omitted for other roles. Values from the area list. A dependent area needs its parent. |
managedAreasWrite | string[] | No | Manage subset of managedAreas (admin only). Must be a subset of managedAreas and must not include a View-only area. |
Examples
Platform-admin with assigned areas:
{
"email": "product.admin@skyte.sa",
"name": "Platform Admin",
"role": "admin",
"managedAreas": ["packages", "activation-records"],
"managedAreasWrite": ["packages"]
}Fixed staff role (phone-only):
{
"phone": "+966501234567",
"name": "Sara Officer",
"role": "activation_officer"
}Response
201 Created — the created Administration User Object wrapped in data.
{
"data": {
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"phone": null,
"email": "product.admin@skyte.sa",
"name": "Platform Admin",
"role": "admin",
"managedAreas": ["packages", "activation-records"],
"managedAreasWrite": ["packages"],
"isActive": true,
"createdAt": "2026-06-30T12:00:00.000Z"
}
}Error Responses
| Code | Status | Condition |
|---|---|---|
AUTH_REQUIRED | 401 | Missing token (an invalid or expired token returns AUTH_SESSION_EXPIRED) |
AUTH_FORBIDDEN | 403 | Caller is not super_admin or admin, or lacks the required staff-users level |
VALIDATION_ERROR | 400 | Schema validation failed (no credential, area rules, managedAreasWrite not a subset of managedAreas or including a View-only area, a dependent area missing its parent, etc.) |
ADMIN_USER_SCOPE_EXCEEDED | 403 | The grant exceeds the caller's own levels, or the role needs areas the caller lacks |
ADMIN_USER_PHONE_EXISTS | 409 | Phone number already registered |
ADMIN_USER_EMAIL_EXISTS | 409 | Email address already registered |
ADMIN_USER_CREATE_FAILED | 502 | Auth account creation failed upstream |
ADMIN_USER_PROFILE_NOT_CREATED | 502 | Profile row was not seeded; the orphaned auth identity is rolled back |
PATCH /admin/users/:id
Update an administration user's access (role + areas) and/or active state. At least one field must be provided. Changing to a fixed staff role clears any admin scope; changing to admin sets the scope from the request. Identity (name/phone/email) is immutable here. Creation and update are audited with managedAreasWrite alongside the role and areas; an update also records the old values, and an edit that changes nothing writes no entry.
Changing role or setting isActive: false signs the user out of every session; an admin area edit that keeps the same role does not.
Requires super_admin, or an admin allowed to act on the target (see Access Rules).
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | Target user ID |
Request Body
All fields optional; at least one is required.
| Field | Type | Required | Description |
|---|---|---|---|
role | string | No | New role (admin, activation_officer, support). Required in order to change areas. Refused for a b2b_admin target. |
managedAreas | string[] | Conditional | Required and non-empty when role is admin |
managedAreasWrite | string[] | No | Manage subset of managedAreas |
isActive | boolean | No | Activate or deactivate the account |
Examples
Grant admin scope:
{
"role": "admin",
"managedAreas": ["packages"],
"managedAreasWrite": ["packages"]
}Deactivate:
{
"isActive": false
}Response
200 OK — the updated Administration User Object wrapped in data.
Error Responses
| Code | Status | Condition |
|---|---|---|
AUTH_REQUIRED | 401 | Missing token (an invalid or expired token returns AUTH_SESSION_EXPIRED) |
AUTH_FORBIDDEN | 403 | Caller is not super_admin or admin (an admin's reach is decided per target, see the two rows below) |
VALIDATION_ERROR | 400 | Schema validation failed (invalid id, no fields, area rules) |
ADMIN_USER_SCOPE_EXCEEDED | 403 | The target is the caller, or is outside the caller's access, or the new grant exceeds it |
ADMIN_USER_NOT_FOUND | 404 | No manageable user with this id, or the target is outside every area the caller can view |
COMPANY_ADMIN_ROLE_LOCKED | 409 | role sent for a b2b_admin target — a company person's role cannot be changed; remove them instead |
COMPANY_LAST_ADMIN | 409 | isActive: false for the last active admin of a company |
DELETE /admin/users/:id
Permanently remove an administration user. Deletes the profile row, then the auth identity, freeing the phone/email for re-registration. The user's live sessions end with the removal. A removal that is refused or rolls back leaves them signed in.
The profile row is deleted in a transaction that also runs the last-admin guard for a company person, and the removal is audited before the auth identity is deleted. A failed auth delete therefore leaves an orphaned auth identity that cannot sign in, since sign-in looks up the profile row, rather than an active account with no identity.
Ticket comments the user wrote are kept, with their author cleared.
Requires super_admin, or an admin allowed to act on the target (see Access Rules).
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | Target user ID |
Response
204 No Content — empty body.
Error Responses
| Code | Status | Condition |
|---|---|---|
AUTH_REQUIRED | 401 | Missing token (an invalid or expired token returns AUTH_SESSION_EXPIRED) |
AUTH_FORBIDDEN | 403 | Caller is not super_admin or admin (an admin's reach is decided per target, see the two rows below) |
VALIDATION_ERROR | 400 | id is not a valid UUID |
ADMIN_USER_SCOPE_EXCEEDED | 403 | The target is the caller, or is outside the caller's access |
ADMIN_USER_NOT_FOUND | 404 | No manageable user with this id, or the target is outside every area the caller can view |
COMPANY_LAST_ADMIN | 409 | The target is the last active admin of a company |
ADMIN_USER_DELETE_FAILED | 502 | Auth identity removal failed upstream, after the profile row was already deleted |
POST /admin/users/:id/resend-invite
Re-send the bilingual sign-in invitation to an existing administration user over its stored channel(s). Best-effort delivery.
Requires super_admin, or an admin allowed to act on the target (see Access Rules).
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | Target user ID |
Response
200 OK
{
"message": "Invitation re-sent"
}Error Responses
| Code | Status | Condition |
|---|---|---|
AUTH_REQUIRED | 401 | Missing token (an invalid or expired token returns AUTH_SESSION_EXPIRED) |
AUTH_FORBIDDEN | 403 | Caller is not super_admin or admin (an admin's reach is decided per target, see the two rows below) |
VALIDATION_ERROR | 400 | id is not a valid UUID |
ADMIN_USER_SCOPE_EXCEEDED | 403 | The target is the caller, or is outside the caller's access |
ADMIN_USER_NOT_FOUND | 404 | No manageable user with this id, or the target is outside every area the caller can view |