Users
A user is a person (or service) with credentials in Huudis. This resource manages the workspace members — the humans who can log into the Huudis dashboard for your account and administer it.
For people who signed into one of your OIDC clients (end users of your product, not of your Huudis workspace), see End users — that's a separate surface with different rules.
This page is about adding, editing, and removing workspace members. For the data model see Concepts → User; for the dashboard walkthrough see Portal → Managing users.
All requests on this page require a bearer admin JWT — see Authentication. Requests follow the response envelope, ID, and pagination conventions in API overview.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/iam/users |
List members of the active workspace |
POST |
/v1/iam/users |
Add a new user directly |
PATCH |
/v1/iam/users/:id |
Change role or verify state |
DELETE |
/v1/iam/users/:id |
Remove a member from the workspace |
POST |
/v1/iam/users/:id/reset-password |
Reset a member's password |
POST |
/v1/iam/invites |
Send an invitation email |
GET |
/v1/iam/invites |
List pending invites |
POST |
/v1/iam/invites/:id/cancel |
Cancel an invite before it's accepted |
Only members with role owner or admin can hit any of the mutation endpoints. member tokens get 403 FORBIDDEN.
List users
GET /v1/iam/users
Returns every member of the active workspace, oldest-joined first. Includes a precomputed groups array (from IAM group memberships) so the dashboard's table can render in one round-trip.
Response — 200 OK
{
"data": [
{
"id": "usr_01KPG30SPWNKDQ9G40NET6QKA2",
"email": "adi@forjio.com",
"name": "Adhya Pranata Sakti",
"emailVerified": true,
"role": "owner",
"joinedAt": "2026-04-21T08:12:00.000Z",
"lastLoginAt": "2026-05-12T22:01:11.220Z",
"createdAt": "2026-04-21T08:12:00.000Z",
"isYou": true,
"groups": [
{ "id": "grp_01KPG…", "name": "Engineering" }
]
}
],
"error": null,
"meta": { "requestId": "req_01KPG…", "timestamp": "2026-05-12T22:01:11.221Z" }
}
isYou is the calling identity, useful for "you can't delete yourself" UI hints. lastLoginAt is null if the user has never signed in (e.g. invited but not yet accepted).
Add a user
POST /v1/iam/users
Adds a user directly to the workspace. If the email already has a Huudis user row (because they're a member of another workspace or have used another Forjio product), Huudis attaches the existing user. Otherwise it provisions a fresh usr_… and a temporary password.
Use this when you want immediate access (e.g. seeding an admin during onboarding). For the friendlier "send them an email, let them set their own password" flow, see Invite a user below.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
email |
string (RFC 5321, ≤200) | yes | The user's email. Lowercased before insert. |
name |
string (1–120) | no | Display name on the member list. |
password |
string (10–200) | no | Set an explicit initial password. Omit to have Huudis generate one. |
role |
owner | admin | member |
no | Default member. Only owners can grant owner. |
emailVerified |
boolean | no | Default true. Set false if you want the user to verify their email first. |
sendInviteEmail |
boolean | no | Default true. When false, Huudis won't email the temp password — useful for orchestration where you'll relay it some other way. |
Response — 201 Created
{
"data": {
"id": "usr_01KPG40HMM…",
"email": "newbie@forjio.com",
"name": "Newbie",
"role": "member",
"emailVerified": true,
"joinedAt": "2026-05-12T22:14:00.000Z",
"tempPassword": "K9hM3pNqRtVw7X"
}
}
tempPassword is only present when Huudis generated it (you didn't pass password) and this was a fresh user creation. It's shown exactly once. If the email already had a Huudis user, tempPassword is null because we don't reset their existing password.
Errors specific to this endpoint
| Status | error.code |
When |
|---|---|---|
403 |
FORBIDDEN |
Caller isn't owner/admin, or role: "owner" was requested by a non-owner. |
409 |
ALREADY_MEMBER |
The user is already a member of this workspace. |
400 |
WEAK_PASSWORD |
Supplied password failed strength checks. |
400 |
NO_ACCOUNT |
The calling user has no workspace membership (shouldn't happen in practice). |
Update a user
PATCH /v1/iam/users/:id
Change a member's role or force their email to verified. Only the two fields below are mutable here — the user's name and email are self-managed via /v1/account (the user's own session).
Request body
| Field | Type | Description |
|---|---|---|
role |
owner | admin | member |
New role. Only owners can grant the owner role. Cannot demote the last owner — promote someone else first. |
emailVerified |
boolean | Force-flip the verification flag. Use sparingly; this skips the normal "click the link in your email" check. |
Errors
| Status | error.code |
When |
|---|---|---|
400 |
LAST_OWNER |
Demoting the last owner is forbidden. |
403 |
FORBIDDEN |
Caller not authorized, or non-owner tried to set role: "owner". |
404 |
RESOURCE_NOT_FOUND |
No member with that ID in this workspace. |
Remove a user
DELETE /v1/iam/users/:id
Removes the membership row — the user no longer belongs to this workspace. Their underlying usr_… survives if they belong to any other workspace; otherwise it becomes an orphaned account that can still sign in but sees no workspaces.
204 No Content on success.
Errors
| Status | error.code |
When |
|---|---|---|
400 |
CANT_REMOVE_SELF |
You called this with your own user ID — use Account → Leave workspace from your own session instead. |
400 |
LAST_OWNER |
Cannot remove the last owner; promote a replacement first. |
403 |
FORBIDDEN |
Caller is a plain member. |
Reset a member's password
POST /v1/iam/users/:id/reset-password
Admin-side password reset. Generates a new temporary password, emails it to the target user, and returns the same plaintext to the caller so it can be relayed if the email bounces.
Response — 200 OK
{
"data": { "reset": true, "tempPassword": "Qm7nXt2vK9pW4R" }
}
The user can keep using their current session until the access token rotates — reset only affects future logins. To kick them out immediately, follow up with POST /v1/account/sessions/{id}/revoke for each of their active sessions.
Invite a user
POST /v1/iam/invites
The polite alternative to direct add. Huudis stores an invite row, emails a single-use token, and sets joinedAt only after the recipient accepts at huudis.com/invites/{token}.
Request body
| Field | Type | Description |
|---|---|---|
email |
string | Recipient. Lowercased. |
role |
owner | admin | member |
Role they'll have after accepting. Default member. |
If an active invite for that email already exists in this workspace, calling this again rotates the token (resends) rather than failing — idempotent in spirit.
Response — 201 Created
{
"data": {
"id": "inv_01KPG…",
"email": "recruit@forjio.com",
"role": "member",
"invitedAt": "2026-05-12T22:30:00.000Z",
"expiresAt": "2026-05-19T22:30:00.000Z"
}
}
Invites expire 7 days after the most recent send. The token itself is not returned by the API — only the recipient (via email) ever sees it, which prevents an admin from impersonating the new user by submitting the accept flow themselves.
Errors
| Status | error.code |
When |
|---|---|---|
409 |
ALREADY_MEMBER |
Email already belongs to a member of this workspace. |
403 |
FORBIDDEN |
Caller can't invite (non-admin) or tried to invite as owner without being one. |
List invites
GET /v1/iam/invites
Returns pending invites for the active workspace. Accepted and canceled invites are excluded by default.
Cancel an invite
POST /v1/iam/invites/:id/cancel
Marks the invite as canceled. The token still in the recipient's inbox stops working immediately — subsequent accept attempts return INVITE_NOT_FOUND.
The user object
| Field | Type | Description |
|---|---|---|
id |
string (usr_…) |
Huudis user ID. Stable forever, even across workspace membership changes. |
email |
string | Login email. Unique per Huudis instance. |
name |
string | null | Display name. |
emailVerified |
boolean | Has the user clicked the verify-email link, or did an admin force it? |
role |
owner | admin | member |
Role within the active workspace. The same usr_… can have different roles in different workspaces. |
joinedAt |
ISO 8601 | When this user joined this workspace. |
lastLoginAt |
ISO 8601 | null | Last successful sign-in across any client. |
createdAt |
ISO 8601 | When the underlying usr_… was first created in Huudis. |
groups |
array | IAM groups within this workspace the user belongs to. |
isYou |
boolean | Whether this row is the calling identity (only present on GET /v1/iam/users). |
Roles and what they can do
| Role | Add/remove members | Change roles | Manage OIDC clients | Manage IAM | Read audit log |
|---|---|---|---|---|---|
owner |
yes | yes (including grant owner) | yes | yes | yes |
admin |
yes | yes (except owner) | yes | yes | yes |
member |
no | no | no | no | own entries only |
There must always be at least one owner. Endpoints enforce this server-side — you cannot demote or delete the last one without first promoting someone else.
Events
| Event type | Fires on |
|---|---|
huudis.user.created.v1 |
POST /v1/iam/users succeeds with a fresh user row, or someone signs up directly. |
huudis.account.member_added.v1 |
A user joins this workspace (direct add or invite accept). |
huudis.user.disabled.v1 |
A user is globally disabled (Forjio-internal endpoint). |
huudis.user.enabled.v1 |
A previously disabled user is re-enabled. |
role.changed and user.removed events are reserved but not currently emitted — subscribe defensively if you need them; the audit log carries the same data for now.
See Webhooks for the envelope and retry policy.
Next
- End users — users of your OIDC clients (different surface).
- Access keys — long-lived service-account credentials.
- Policies — attach IAM policies to users.