Administering members and roles
The admin-only surface for managing the caller's tenant. The tenant is always the admin's own (principal.tenantId).
- GET /v1/roles — the assignable role allow-list, so the UI offers only valid grants
- GET /v1/users — list members (cursor-paginated via meta.page.nextCursor)
- GET /v1/users/{userId} — inspect one member and their roles
- PUT / DELETE /v1/users/{userId}/roles/{role} — grant / revoke a role (idempotent)
- suspend / reactivate / remove a member
- PUT /v1/tenant/mfa-policy — tenant-wide MFA enforcement
/v1/rolesadminList assignable rolesThe 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.
GET https://api.kerja.team/v1/rolesResponse 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.
dataarrayalwaysdata[].nameenumalwaysadmin— Tenant administrator role — full admin access to tenant-scoped management endpoints.member— Standard member role — baseline tenant access.editor— Editor role — elevated content/edit permissions above member.
/v1/usersadminList membersField 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 choosecursorquerystringoptionalfrom a responseGET https://api.kerja.team/v1/users?limit=20Response 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[]arrayalwaysdata[].userIdUUIDalwaysdata[].statusenumalwaysactive— Member is active and can authenticate against the tenant.invited— Member has been invited but has not yet accepted/activated.
data[].isDefaultbooleanalwaysmeta.pageobjectalwaysmeta.page.limitintegeralwaysmeta.page.nextCursorstringalways/v1/users/{userId}adminGet one memberA user outside your tenant is reported as 404 (never 403), so membership isn't leaked.
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 responseGET 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.userIdUUIDalwaysdata.emailstringalwaysdata.displayNamestringalwaysdata.statusenumalwaysactive— Member is active and may authenticate / hold sessions in this tenant.suspended— Membership has been suspended (sessions revoked); the member cannot authenticate in this tenant until reactivated.
data.isDefaultbooleanalwaystrue— This tenant is the user's default membership.false— This tenant is not the user's default membership.
data.roles[]arrayalwaysadmin— Administrative role with management access (e.g. managing members, roles, tenant settings).member— Standard non-admin member role with regular tenant access.
data.isLastAdminbooleanalwaystrue— This member is the tenant's last active admin — pre-disable suspend / remove / revoke-admin in the UI (the server would return 409 last_admin).false— Not the last active admin — destructive actions are not blocked by the last-admin guard.
/v1/users/{userId}/roles/{role}adminGrant roleIdempotent; no body. A role outside the allow-list → 422.
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 responserolepathenumrequiredyou chooseadmin— Grants tenant administrator privileges (full admin access to tenant management endpoints).member— Grants the standard member role for the tenant.editor— Grants the editor role for the tenant.
PUT https://api.kerja.team/v1/users/{userId}/roles/editorResponse 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/v1/users/{userId}/roles/{role}adminRevoke roleIdempotent. 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.
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 responserolepathenumrequiredyou chooseadmin— Removes 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.member— Removes the basic member role from the user.editor— Removes the editor role from the user.
DELETE https://api.kerja.team/v1/users/{userId}/roles/editorResponse 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/v1/users/{userId}/suspendadminSuspend memberNo 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.
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 responsePOST https://api.kerja.team/v1/users/{userId}/suspendResponse 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).
/v1/users/{userId}/reactivateadminReactivate memberNo body.
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 responsePOST https://api.kerja.team/v1/users/{userId}/reactivateResponse 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.
/v1/users/{userId}adminRemove memberCannot 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.
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 responseDELETE 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).
/v1/tenant/mfa-policyadminSet tenant MFA enforcementTenant-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.
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 choosetrue— Enforce MFA: a member's next login / switch-tenant returns 403 mfa_enrollment_required until they enrol any one MFA method (scenario 15 / 18).false— Relax enforcement: MFA is optional for the tenant; no enrolment gate.
PUT https://api.kerja.team/v1/tenant/mfa-policyResponse 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.isMfaRequiredbooleanalwaystrue— MFA is now enforced tenant-wide.false— MFA enforcement is now off for the tenant.