switching clears the token & responses
No token
48

Administering members and roles

The admin-only surface for managing the caller's tenant. The tenant is always the admin's own (principal.tenantId).

  1. GET /v1/roles — the assignable role allow-list, so the UI offers only valid grants
  2. GET /v1/users — list members (cursor-paginated via meta.page.nextCursor)
  3. GET /v1/users/{userId} — inspect one member and their roles
  4. PUT / DELETE /v1/users/{userId}/roles/{role} — grant / revoke a role (idempotent)
  5. suspend / reactivate / remove a member
  6. PUT /v1/tenant/mfa-policy — tenant-wide MFA enforcement
GET/v1/rolesadminList assignable roles

The allow-list (admin · member · editor) the role routes accept.

ℹ︎Field guide — what each value means & where it comes fromno inputs

No request body, path params, or query params. Lists the tenant's assignable role names (e.g. "admin", "member", "editor") that the PUT/DELETE role allow-list routes accept. Requires Admin access: the tester supplies the Authorization bearer token and the gateway injects x-gateway-context automatically; a 403 forbidden is returned if the gateway context is missing or the caller is not an admin.

Bearer token
GET https://api.kerja.team/v1/roles
↩︎Response guide — what comes back & what each value means2 fields

200 OK returning the standard { data } envelope; data is a JSON array of role objects (no special response headers). Each item is { name } — the assignable role names the tenant's role-assignment routes accept.

dataarrayalways
The { data } envelope wraps an array of assignable role objects for the caller's tenant. Iterate to build the allow-list of role names that the PUT/DELETE role-assignment routes will accept.
data[].nameenumalways
The assignable role name. Feed these values into role-assignment calls (PUT/DELETE /v1/users/{id}/roles); only names returned here are accepted.
  • adminTenant administrator role — full admin access to tenant-scoped management endpoints.
  • memberStandard member role — baseline tenant access.
  • editorEditor role — elevated content/edit permissions above member.
GET/v1/usersadminList members
Query parameters
ℹ︎Field guide — what each value means & where it comes from2 fields

Lists members of the caller's tenant (admin-only); the tenant is always the principal's own tenantId, so it is never supplied by the caller. No request body, path params, or caller-set headers — the tester and gateway handle Authorization and x-gateway-context. Only two optional cursor-pagination query params.

limitqueryintegeroptionalyou choose
Optional page size you choose. Integer between 1 and 100; defaults to 20 when omitted. Controls how many members are returned per page.
cursorquerystringoptionalfrom a response
Optional opaque pagination cursor (max 512 chars). You do not invent it: copy it from a prior GET /v1/users response's meta.page.nextCursor field to fetch the next page. nextCursor is null on the last page, so when it is null there is nothing to pass.
Bearer token
GET https://api.kerja.team/v1/users?limit=20
↩︎Response guide — what comes back & what each value means7 fields

200 OK; { data } envelope where data is an ARRAY of tenant member objects plus meta.page cursor pagination; no notable response headers (no Set-Cookie / Cache-Control). meta.page.nextCursor is null on the last page.

data[]arrayalways
Array of member objects for the caller's tenant (cursor-paginated). Empty array if the tenant has no matching members on this page.
data[].userIdUUIDalways
The member's user id within the tenant. Feed into GET /v1/users/{userId} or suspend/role calls.
data[].statusenumalways
Membership status of this user in the tenant.
  • activeMember is active and can authenticate against the tenant.
  • invitedMember has been invited but has not yet accepted/activated.
data[].isDefaultbooleanalways
True if this is the user's default tenant membership; the tester can show which membership is the primary one.
meta.pageobjectalways
Pagination envelope for the cursor-paginated list.
meta.page.limitintegeralways
The page size that was applied (the effective limit, 1-100, default 20).
meta.page.nextCursorstringalways
Opaque keyset cursor for the next page; pass back as ?cursor to fetch the next page. Null on the last page (no more results).
GET/v1/users/{userId}adminGet one member

A user outside your tenant is reported as 404 (never 403), so membership isn't leaked.

Path parameters
ℹ︎Field guide — what each value means & where it comes from1 field

Read-only admin lookup of one member of the caller's tenant. No request body, no query params, and no caller-set header beyond auth — the tester/gateway supply Authorization and x-gateway-context. The only input is the userId path param.

userIdpathUUIDrequiredfrom a response
The membership/user id of the member to look up. Do not type it freely; copy it from a prior tester response — e.g. the userId field of an entry from the list-users response or another user/get. Must be a UUID; a malformed value returns 422, and a user outside your tenant returns 404 (indistinguishable from not existing).
Bearer token
GET https://api.kerja.team/v1/users/{userId}
↩︎Response guide — what comes back & what each value means7 fields

200 OK with the standard { data } envelope; data is a single membership object for the requested member of the caller's tenant (a user outside the tenant returns 404, not 403). No notable response headers (no Set-Cookie; no Cache-Control: no-store).

data.userIdUUIDalways
The member's user identifier within the caller's tenant. Feed into the suspend/reactivate/role-assignment endpoints as the {userId} path param.
data.emailstringalways
The member's email address. Display-only identifier for the user.
data.displayNamestringalways
The member's human-readable display name shown in the tester's user view.
data.statusenumalways
The member's lifecycle status within this tenant. Use it to decide whether suspend or reactivate actions apply.
  • activeMember is active and may authenticate / hold sessions in this tenant.
  • suspendedMembership has been suspended (sessions revoked); the member cannot authenticate in this tenant until reactivated.
data.isDefaultbooleanalways
Whether this membership is the user's default tenant. true marks the tenant selected by default at login; false otherwise.
  • trueThis tenant is the user's default membership.
  • falseThis tenant is not the user's default membership.
data.roles[]arrayalways
The role names granted to this member in the tenant. Each item is a role string; the tester renders these as the member's permissions/badges.
  • adminAdministrative role with management access (e.g. managing members, roles, tenant settings).
  • memberStandard non-admin member role with regular tenant access.
data.isLastAdminbooleanalways
Read-only hint: true only when this member is an active admin AND the tenant's sole active admin — meaning suspending/removing them or revoking their admin role would be refused with 409 last_admin. A UI uses it to pre-disable those destructive actions; the write paths still enforce the rule regardless.
  • trueThis member is the tenant's last active admin — pre-disable suspend / remove / revoke-admin in the UI (the server would return 409 last_admin).
  • falseNot the last active admin — destructive actions are not blocked by the last-admin guard.
PUT/v1/users/{userId}/roles/{role}adminGrant role

Idempotent; no body. A role outside the allow-list → 422.

Path parameters
ℹ︎Field guide — what each value means & where it comes from2 fields

Grants a role to a member; idempotent (granting an existing role is a no-op). No request body, no query params, and no caller-set headers — only two path params. Requires admin auth (Authorization + x-gateway-context are handled by the tester/gateway).

userIdpathUUIDrequiredfrom a response
The member to grant the role to. You do not type this freely; obtain it from a prior tenant member list or get response (the user's id field). Must be a valid UUID; a malformed value returns 422, and a user that is not a member of this tenant returns 404.
rolepathenumrequiredyou choose
The role name to grant; you pick one of the tenant's allow-listed roles (the same set returned by GET /v1/roles). An unknown role returns 422.
  • adminGrants tenant administrator privileges (full admin access to tenant management endpoints).
  • memberGrants the standard member role for the tenant.
  • editorGrants the editor role for the tenant.
Bearer token
PUT https://api.kerja.team/v1/users/{userId}/roles/editor
↩︎Response guide — what comes back & what each value means1 field

Returns 200 OK with the standard { data } envelope, where data is an empty object ({ "data": {} }) — granting a role is idempotent and confirms success without returning a body payload. No Set-Cookie or Cache-Control headers of note. Side effect: audits role_granted with the role name as the reason.

dataobjectalways
Empty object ({}) confirming the role grant succeeded. The role is now active for the member; no fields are returned — the client treats a 200 as confirmation and may re-fetch the member or roles list.
DELETE/v1/users/{userId}/roles/{role}adminRevoke role

Idempotent. Self-lockout protection: you cannot revoke your own admin role (403). Last-admin protection: revoking admin from the tenant's last active admin → 409 last_admin.

Path parameters
ℹ︎Field guide — what each value means & where it comes from2 fields

No request body. Admin-only; the tester supplies Authorization and x-gateway-context automatically. You only fill the two path params. Idempotent — cannot revoke your own admin role nor admin from the tenant's last active admin.

userIdpathUUIDrequiredfrom a response
The member whose role you are revoking. You do not type this freely; copy the opaque user id from a prior response in the tester, such as the userId from a member list/get (e.g. GET /v1/users) or from an invitation response. Must be a valid UUID or the call returns 422.
rolepathenumrequiredyou choose
The role name to revoke from the member. You pick one of the allowed values; the assignable set is the tenant's role allow-list (see GET /v1/roles). An unknown value returns 422.
  • adminRemoves tenant administrator privileges. Blocked (403) if it would revoke your own admin role; blocked (409 last_admin) if it would remove admin from the tenant's last active admin.
  • memberRemoves the basic member role from the user.
  • editorRemoves the editor role from the user.
Bearer token
DELETE https://api.kerja.team/v1/users/{userId}/roles/editor
↩︎Response guide — what comes back & what each value means1 field

200 OK. Body is the standard { data } envelope where data is an empty object ({ "data": {} }) — the revoke is idempotent and returns no fields. No notable response headers (no Set-Cookie, no Cache-Control: no-store).

dataobjectalways
An empty object ({}) signalling the role was revoked (idempotent — succeeds even if the member never held the role). No properties are returned; the client needs only the 200 status to confirm success.
POST/v1/users/{userId}/suspendadminSuspend member

No body. Cannot suspend yourself (403) or the last active admin (409 last_admin). CASCADES: the member's sessions in this tenant are revoked — but their DEVICE TRUST is not touched, because that is an identity-plane fact spanning every tenant they belong to, so removing them from one tenant must not silently re-challenge them in another.

Path parameters
ℹ︎Field guide — what each value means & where it comes from1 field

Admin-only, no request body. Suspends a member by userId. Auth still required: the tester supplies Authorization and the gateway injects x-gateway-context (tenant/principal); the only caller input is the userId path param.

userIdpathUUIDrequiredfrom a response
The membership/user id to suspend. You do not type it freely; copy it from a prior tester response such as the member list (GET /v1/users) or a get-by-id. Must be a UUID or the call returns 422. Cannot be your own membership (403) or the tenant's last active admin (409).
Bearer token
POST https://api.kerja.team/v1/users/{userId}/suspend
↩︎Response guide — what comes back & what each value meansno body

Returns 200 OK with the standard { data } envelope, where data is an empty object ({ "data": {} }) — there are no body properties to consume. It signals the member's membership status was set to "suspended" and their sessions in this tenant were revoked (cascade); the action is audited as membership_suspended. No notable response headers (no Set-Cookie or Cache-Control: no-store).

POST/v1/users/{userId}/reactivateadminReactivate member

No body.

Path parameters
ℹ︎Field guide — what each value means & where it comes from1 field

Reactivates a suspended member (status to active). Admin-only; the tester + gateway supply Authorization and x-gateway-context. No request body, no query params, and no caller-set headers — the only input is the userId path param.

userIdpathUUIDrequiredfrom a response
The id of the suspended member to reactivate. You do not type this freely — copy it from a prior response such as a user list or get. Must be a valid UUID or the call returns 422; an unknown member in this tenant returns 404.
Bearer token
POST https://api.kerja.team/v1/users/{userId}/reactivate
↩︎Response guide — what comes back & what each value meansno body

200 OK with the standard { data } envelope, where data is an empty object ({ "data": {} }). No response headers of note (no Set-Cookie, no Cache-Control: no-store). A 200 with empty data signifies the suspended member's status was set back to active and a membership_reactivated event was audited; there are no body properties to consume.

DELETE/v1/users/{userId}adminRemove member

Cannot remove yourself or the last active admin. Cascades: revokes the member's sessions in this tenant (device trust untouched — it is an identity-plane fact spanning every tenant they belong to). ALSO REVOKES any invitation still outstanding for the membership: left open it would outlive the membership it was issued for, so the invitee could present the link, consume the token and then be told 403 no active membership — an invitation that works and then fails — and it would keep the open-invite index slot occupied against the person just removed.

Path parameters
ℹ︎Field guide — what each value means & where it comes from1 field

Removes a member from the current tenant. No request body, query params, or caller-set headers — admin auth (handled by the tester/gateway via Authorization + x-gateway-context) plus the path userId is all that is supplied. You cannot remove your own membership or the tenant's last active admin.

userIdpathUUIDrequiredfrom a response
The membership/user to remove from this tenant. You do not type this freely — copy it from a prior response in the tester (e.g. the user list GET /v1/users or a get-by-id). Must be a valid UUID or the call returns 422; removing your own membership returns 403 and removing the last active admin returns 409.
Bearer token
DELETE https://api.kerja.team/v1/users/{userId}
↩︎Response guide — what comes back & what each value meansno body

200 OK with the standard { data } envelope, but data is an empty object ({ "data": {} }) — there are no body properties to consume; success is signalled by the status code alone. Side effects (server-side only): the membership and its role grants are deleted, the member's sessions in this tenant are revoked, and a membership_removed event is audited. No notable response headers (no Set-Cookie / no Cache-Control: no-store).

PUT/v1/tenant/mfa-policyadminSet tenant MFA enforcement

Tenant-wide (always the caller's own tenant — taken from the verified principal, never the body). When required=true, every member's next login / switch-tenant is rejected (403 mfa_enrollment_required) until they hold a confirmed MFA method (scenario 25 / 26), and members can no longer disable MFA at all while it is in force — the member-side effect is scenario 27. Any ONE method satisfies it. SELF-LOCKOUT GUARD: enabling is refused with 422 when YOU hold no confirmed method of your own. Enforcement is checked at session issue and switch-tenant, so an admin who turned it on without a second factor would be refused entry to their own tenant from their very next login — and the only surface that can turn it back off lives inside that tenant. There would be no undo and, for a sole admin, nobody left to ask. Disabling enforcement is never guarded: it cannot lock anyone out.

Request body (JSON)
ℹ︎Field guide — what each value means & where it comes from1 field

Admin-only; tenant-wide MFA enforcement for the caller's OWN tenant. One boolean body field — the tenant is taken from the verified principal (x-gateway-context), never the body. Needs an admin bearer token. SELF-LOCKOUT GUARD on the enabling direction only: required=true is refused 422 unless YOU already hold a confirmed MFA method, because enforcement is checked at session issue and switch-tenant and the only surface that could turn it back off lives inside the tenant you would be locked out of. Turning it off is never guarded — it cannot lock anyone out.

requiredbodybooleanrequiredyou choose
Whether MFA is mandatory for every member of the caller's tenant. The admin chooses this. When true, members without a confirmed MFA method are blocked at their next login / switch-tenant until they enrol.
  • trueEnforce MFA: a member's next login / switch-tenant returns 403 mfa_enrollment_required until they enrol any one MFA method (scenario 15 / 18).
  • falseRelax enforcement: MFA is optional for the tenant; no enrolment gate.
Bearer token
PUT https://api.kerja.team/v1/tenant/mfa-policy
↩︎Response guide — what comes back & what each value means1 field

200 OK with the { data } envelope; returns the tenant's MFA policy after the update.

data.isMfaRequiredbooleanalways
The tenant MFA enforcement flag after the change — true if MFA is now mandatory for all members of the tenant.
  • trueMFA is now enforced tenant-wide.
  • falseMFA enforcement is now off for the tenant.