Eviworx
Docs

Users, Roles & Agent Groups API

This API covers user management, dynamic role-based access control (RBAC), the manager hierarchy (org chart), agents incl. agent groups with assignment strategies, and specialties (skill routing). The auth/permission model itself is documented centrally under Permissions & RBAC.

Absences/substitution (with manager approval) are a separate domain: Absences API. Permission resolution, cache & critical actions: Permissions & RBAC.

👥
Features
✓ Dynamic roles (own permission set + priority)
✓ Manager hierarchy & org chart
✓ Creation via invitation
✓ Archiving with handover to a successor
✓ Agent groups (4 assignment strategies)
✓ Specialties for skill routing (SKILL_BASED)
✓ Email-only contacts (no portal login)
✓ EntraID/SSO (group → role)

Pre-installed Roles

Besides the system roles, any number of custom roles can be created, each with its own permission set and a priority (order/precedence). System roles (isSystem) can neither be deactivated, deleted nor moved. Pre-installed are the four system roles and the DATA_PROTECTION_OFFICER role, which is created as a custom role:

RolePurpose
END_USERCustomer/requester — own tickets
AGENTSupport agent — manage all tickets/problems
ADMINAdministrator — full access
APPROVERChange approver
DATA_PROTECTION_OFFICERData protection officer (GDPR: breach details, incident view, audit); custom role, isSystem = false

User Endpoints /api/users

MethodEndpointDescription
GET/List (filter/search); anonymized users hidden by default — includeAnonymized=true for administration only
GET/statsCounts per role
GET/assignable?entityType=Assignable candidates per entity kind (TICKET, PROBLEM, CHANGE, INCIDENT, WORKFLOW) — with ?groupId, ?search, ?take/skip
GET/approvers?entityType=Users with approve permission (CHANGE, CHANGE_TEMPLATE, INCIDENT)
GET/checkout-recipientsRecipients for asset checkout
GET/check-emailCheck email availability
GET/:idSingle user
POST/Create user (with invitation); an invitation requires an active account — otherwise 400 INVITATION_ACCOUNT_NOT_ACTIVE
PATCH/:idUpdate user (incl. roleId)
PATCH/:id/profileProfile/status (isActive …)
POST/:id/promotePromote an email contact to a user (portal access, optionally a new role); requires an active account — otherwise 400 PROMOTE_ACCOUNT_NOT_ACTIVE
POST/:id/resend-invitationResend invitation; requires an active account — otherwise 400 INVITATION_ACCOUNT_NOT_ACTIVE
GET / PUT/:id/languageLanguage (de, en, es, fr, it)
GET/:id/archival-checkCheck if archivable
POST/:id/archive-with-transferArchive + transfer items

A user's display preferences: Language, timezone and date format belong to the user — the language via /:id/language, timezone and date format via their own profile (PUT /api/auth/profile). They apply in the interface and equally in the texts the server generates: email, push, Teams and Webex. A message therefore reaches the recipient in their language, with their times and in their date format, regardless of who triggered it. Which level applies when nothing is set.

Note: there is no DELETE endpoint for users. Deactivate via /:id/profile (isActive=false) or archive via /:id/archive-with-transfer (items go to a successor).

Three account states: An account is active, blocked or archived — archived ALWAYS implies blocked. Sending only isArchived: true adds isActive: false silently; asking for isActive: true on an archived account returns 400 ARCHIVED_USER_CANNOT_BE_ACTIVE. Reactivation sends both fields in ONE request (isArchived: false, isActive: true). This way an archived account drops out wherever only active accounts count: notifications, workflows and directory sync. Blocked and archived accounts cannot sign in on any path (403 ACCOUNT_DEACTIVATED); a sign-in never changes the status, unarchiving is only possible via PATCH /:id/profile with users.archive.

On the interface the three states carry the values ACTIVE, INACTIVE and ARCHIVED. The user list narrows to them either through the parameter ?status=ACTIVE|INACTIVE|ARCHIVED or through the filter field f.accountStatus=eq:ACTIVE, which with in: also takes several levels at once (f.accountStatus=in:INACTIVE,ARCHIVED). The spelling is binding: a deviating value is 400. customer.accountStatus in the ticket responses carries the same value.

A blocked or archived account cannot become the customer of a new ticket, nor the successor when archiving (400 SUCCESSOR_NOT_ACTIVE). An incoming email to an archived account does NOT change its status — it only moves the erasure deadline, because a status change is an administrative decision: it requires a permission, is audited and can transfer manager relationships.

A blocked account names the origin of its block: managedProfile.lockSource is ADMIN (administration), ENTRA_SYNC (directory sync) or SYSTEM. On an active account the field is null. It answers the question who can lift the block again — the directory sync only lifts its own blocks.

A status change takes effect immediately: blocking an account ends all of its access — running sessions, refresh tokens, push subscriptions and open real-time connections; the permission cache is cleared. This holds for every path that writes the status: creation, editing, profile, archiving with a successor, directory sync and anonymization. Invitation and promotion therefore require an active account (400 INVITATION_ACCOUNT_NOT_ACTIVE resp. PROMOTE_ACCOUNT_NOT_ACTIVE) — a setup link sent to a blocked account would lead nowhere.

Second factor on an account: Every user response carries secondFactor with exactly three values: LOCAL (a second factor is set up on the account), ENTRA (the sign-in runs through Microsoft — a local second factor is not possible there) and NONE (locally possible but not set up). Filtering uses f.secondFactor=eq:LOCAL, several values with in:. The user list can show the second factor as its own column; it is not preselected and is switched on through the column picker.

The shipped security view "Without 2FA" lists the active login accounts with NONE; email-only contacts stay out. Accounts signing in through Microsoft are not among them: nothing can be missing locally there, the second factor lives in the directory.

Protected accounts: The platform keeps two accounts for itself: the internal system account under which automated operations run, and the local emergency access through which administration gets in even if the Microsoft sign-in fails. Neither can be blocked, archived, anonymized or put under an erasure hold; their role is fixed. Such operations answer 403 PROTECTED_ACCOUNT, and details.operation names the rejected one (status, archive, role or erase). The directory sync never links either account (conflict code PROTECTED_ACCOUNT), and the system account does not sign in on any path. The UI marks these accounts and does not even offer the blocked actions. Profile data, password and two-factor sign-in of the emergency access stay editable.

Role assignment on an account: A roleId is set by POST /, PATCH /:id and /:id/promote — under three limits. The role of a synchronized account belongs to the directory: a role change is rejected with 403 ROLE_MANAGED_BY_ENTRA_ID as long as the account sits in a mapped role group (with entraIDConflict = true it stays manually settable, because the sync does not overwrite it there). A role without usable permissions — deactivated or with an invalid permission matrix — cannot be assigned: 400 ROLE_NOT_ASSIGNABLE with details.roleName, because the account would afterwards be rejected everywhere without being blocked. And nobody assigns a role with higher privileges than their own (403).

Visibility before action: every route targeting a specific user (PATCH /:id, /:id/profile, /:id/language, /:id/promote, /:id/resend-invitation, /:id/archival-check, /:id/archive-with-transfer, the manager routes, the hierarchy reads) first checks whether the caller may see that user at all — their own account or users.viewAll. A role holding users.edit or users.archive without users.viewAll therefore only affects its own profile; anything else is 403.

Archiving requires users.archive; users.manageManagers is only needed when manager relationships are actually transferred. users.archive and agents.manageGroups are critical actions: their permissions are revalidated straight from the database and both successes and denied attempts are audited.

Candidate lists require a permission on the target domain: /assignable requires a permission on the TARGET domain — viewAll, editAll or assign of the respective entity kind (for WORKFLOW: editTemplates, createTemplates or reassignSteps). editOwn deliberately does NOT suffice, otherwise any end user could query the agent list. /approvers requires the approval permission of the entity kind (e.g. changes.approve, incidents.approveClosure) or editAll/editOwn. Roles without those permissions receive 403.

Manager Hierarchy

MethodEndpointDescription
GET/:id/managersManagers of a user
POST/:id/managersAssign manager
PATCH/:id/managers/:managerIdUpdate relationship
DELETE/:id/managers/:managerIdRemove manager
GET/:id/subordinatesDirect subordinates
GET/:id/hierarchyFull org chart (recursive)

The manager hierarchy is the basis for escalations (assignee→lead→manager) and absence approvals.

Create User

POST /api/users
{
  "email": "agent@company.com",
  "name": "John Support",
  "roleId": "clx-role-agent",
  "managedProfile": { "firstName": "John", "lastName": "Support", "department": "IT", "location": "Munich" }
  // without password → invitation flow (password setup via link); see Authentication
}

Email-only contacts (autoCreatedFromEmail/emailOnlyContact) are created automatically on inbound email (sender policy AUTO_CREATE): an account without a password and without sign-in. How many such contacts may be created in one day is a field on the mailbox (default 100). Once the limit is reached the mail stays open and is processed after midnight (UTC) — it is not rejected. See Email System.

Roles (RBAC) /api/roles

MethodEndpointDescription
GET/All roles (with permission matrix; settings.manageRoles). page/limit are validated: limit at most 200, unusable values are 400
GET/assignableLean assignment list for the user editor (id/name/displayName/color); requires users.manageRoles; contains only active roles the caller may assign (privilege ceiling)
GET/:idRole (with permission matrix)
POST/Create custom role
PATCH/:idUpdate permissions
DELETE/:idDelete custom role → 204. Not possible for system roles, for roles with users, and for roles carrying an ACTIVE API key (the key would otherwise be left without a role and rejected with 403 API_KEY_NO_ROLE).
GET/permissions/schemaPermission schema (for UI)
POST/:id/moveMove a role in the priority order — give EXACTLY one side (afterRoleId OR beforeRoleId); both or neither is 400

Error codes of role management

errorCodeHTTPMeaning
ROLE_NOT_FOUND404No role with this id
ROLE_NAME_EXISTS409The name is already taken
ROLE_IN_USE409The role is still held by users or active API keys — details.users and details.activeApiKeys give the counts
SYSTEM_ROLE_PROTECTED400System roles can neither be deactivated nor deleted nor moved
ROLE_PERMISSIONS_INVALID400The matrix contains unknown modules or actions, or non-boolean values
ROLE_LOCKOUT403settings.manageRoles must not be removed from a system role, your own role, or the LAST role that holds it — otherwise role management would be locked out permanently. details.reason names the case.
ROLE_NOT_ASSIGNABLE403Privilege ceiling: nobody assigns a role carrying rights they do not hold themselves. details.reason distinguishes ROLE_NOT_FOUND from PERMISSION_NOT_HELD (with details.permission).

Deactivating a role ends the access of its users: Switching isActive to false makes every account on that role lose its running sessions, refresh tokens, push subscriptions and open real-time connections at once; a new sign-in answers 403 NO_USABLE_ROLE. The accounts themselves stay active — the cause lies with the role, and the message says so. The UI therefore asks for confirmation as soon as the role still holds users; it names their number and points out that those affected receive no notification about it.

A role carries a permission matrix across 29 modules (tickets, problems, changes, incidents, assets, inventory, contracts, licenses, costCenters, knowledgeBase, elibrary, workflows, cronjobs, settings, users, agents, notifications, audit, analytics, absences, customReports, savedViews, …). How permissions are resolved, cached (per role) and freshly revalidated for critical actions is documented centrally under Permissions & RBAC.

POST /api/roles
{
  "name": "level2-agent",
  "displayName": "Level 2 Agent",
  "description": "Agent with change approval",
  "priority": 45000,
  "permissions": {
    "tickets":  { "viewAll": true, "create": true, "editAll": true, "assign": true },
    "changes":  { "viewAll": true, "approve": true, "reject": true }
    // more modules …
  }
}

Agents /api/agents

The agent profile augments a user with support-specific data (workload, isActive, specialties).

MethodEndpointDescription
GET/api/agentsList agents ({data, pagination}); ?forEntityType=TICKET|PROBLEM|CHANGE|INCIDENT|WORKFLOW narrows to the assignable ones
POST/api/agentsCreate agent profile ({userId, isActive})
PATCH/api/agents/:userIdUpdate agent: isActive and/or maxWorkload
DELETE/api/agents/:userIdRemove agent profile

Agents are addressed by userId everywhere: in paths, inputs and responses. Inputs are validated strictly; unknown fields and unknown query values (e.g. an invalid forEntityType) return 400.

Agent Groups

Groups route work to agents. applicableEntityTypes (array) defines which entity kinds the group serves as a queue for, assignmentStrategy decides the pick. Group access can restrict per group who sees it / can be assigned.

AspectValues
applicableEntityTypes (String[])TICKET, PROBLEM, CHANGE, INCIDENT, WORKFLOW (default: all five)
assignmentStrategyFIRST_AVAILABLE, ROUND_ROBIN, LEAST_LOADED, SKILL_BASED
MethodEndpoint
GET/api/agents/groups · /groups/:id
POST / PATCH/api/agents/groups · /groups/:id
POST/api/agents/groups/:id/archive · /restore
GET/api/agents/groups/:id/potential-members (server-side search: ?search=, ?take/skip, ?excludeAbsent, ?excludeInactive)
POST / DELETE / PATCH/api/agents/groups/:groupId/members/:userId (PATCH: isTeamLead and/or isActive → 204)
GET / POST/api/agents/groups/:id/access (access rules)
DELETE/api/agents/groups/:id/access/:accessId
PUT/api/agents/groups/:id/restriction (toggle restriction)
  • By default the group list returns only active, non-archived groups (for selection lists). Administration passes ?includeInactive=true to also get inactive and archived ones; the single lookup /groups/:id resolves inactive groups too, so existing assignments stay visible. Further query params: ?forEntityType=, ?search=.
  • Permissions: read groups = users.viewAgents · manage groups/members/specialties = agents.manageGroups · access rules = agents.manageGroupAccess. Every group response carries canManageMembers for the requesting user (agents.manageGroups or active team lead of that group) — the UI member actions follow it.
  • An access entry belongs to exactly one group: a second grant for the same user or role is 409 ACCESS_ALREADY_GRANTED, and a DELETE through a foreign group URL is 404.

Specialties /api/agents/specialties

Skills for SKILL_BASED routing. CRUD + assignment to agents:

MethodEndpoint
GET/ (?includeInactive=true for administration) · /:id · /agent/:userId
POST / PATCH / DELETE/ · /:id
POST / DELETE/:id/agents/:userId (attach/detach skill, proficiency 1–5)

Everyone may read their own skills (/agent/:userId without users.viewAgents); other people's skills require users.viewAgents. Skills are maintained with agents.manageGroups or as an active team lead of a group the target agent belongs to.

No permission, no answer about existence: When attaching a skill the permission is checked BEFORE the existence check. Anyone who may not maintain skills therefore gets 403 — even for a specialty that does not exist at all. The answer thus never reveals which skills are defined in the system. The same holds when updating an agent: without any maintenance capability the route answers 403 before it looks for the agent.

Error Codes of Agent Administration

Every code sits at the top level of the response as errorCode. For the 403 of the handler gates — updating an agent, the workload limit, granting team lead, pausing a member, maintaining skills — details.required names the required permission (or the required team lead relationship).

errorCodeHTTPMeaning
AGENT_NOT_FOUND404No agent for this userId — when updating and removing the profile, when removing from a group and when detaching a skill
AGENT_GROUP_NOT_FOUND404Group unknown — on the single lookup, the candidates, the access rules, the access restriction and when adding a member
AGENT_GROUP_MEMBER_NOT_FOUND404The agent is not a member of this group (changing member settings)
AGENT_GROUP_ACCESS_NOT_FOUND404Access entry unknown — also when it belongs to a different group
AGENT_SPECIALTY_NOT_FOUND404Specialty unknown — on the single lookup and when attaching a skill
NOT_FOUND404The general code: updating, archiving or restoring a group and updating or deleting a specialty answer with it when the target does not exist
AGENT_ALREADY_EXISTS409This user already has an agent profile
ACCESS_ALREADY_GRANTED409This user or role already has an access entry for the group
DUPLICATE_ENTRY409Group and specialty names are unique
ENTITY_TYPE_REMOVAL_BLOCKED409An entity kind cannot be taken out of the group while open records of that kind are assigned to it — details.conflicts names kind and count
AGENT_HAS_TICKETS400The profile still carries assigned tickets (details.ticketCount)
USER_NEEDS_AGENT_PERMISSIONS400An agent needs an assignment permission (tickets.assign or problems.assign); without it there is neither a profile nor a membership
FORBIDDEN403A handler gate refused; details.required names the required permission

Refused gates of agent administration that hang on the critical permission agents.manageGroups — granting team lead, pausing a member, the workload limit, skill assignment — are additionally audited.

Absence & Availability

Assignability accounts for absences: the candidate lists flag absent agents (isAbsent; the absence reason is not returned because it may contain health data), and if a target agent is absent, the substitute redirect applies at assignment time (override via ignoreSubstitution). Deactivated and archived accounts are never candidates. Absence management (incl. manager approval) on its own page: Absences API.

👥
Core Principles
  • ✓ Freely definable roles with priority
  • ✓ Manager hierarchy & org chart
  • ✓ Agent groups: queue, assignment strategy, access rules
  • ✓ Archiving with transfer to a successor
🔐
Permissions (RBAC)
  • users.viewAll / create / edit / manageRoles
  • settings.manageRoles – roles/permissions
  • agents.* – agents & groups

Auth/role model: Permissions & RBAC

Related Documentation