Invites
An invite is a pending workspace membership. The flow:
- An admin calls
POST /v1/iam/inviteswith an email and role. - Huudis emails a single-use token link to that address.
- The recipient clicks, lands on
huudis.com/invites/{token}, signs in or creates a Huudis account, and accepts. - The invite row flips to
acceptedAt = nowand a newaccount_memberrow is created.
Use invites when you want the recipient to set their own password and don't already have a Huudis user ID for them. For direct adds (you have the email and want immediate access), use POST /v1/iam/users instead.
All endpoints require a bearer admin JWT — see Authentication.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/iam/invites |
Send an invite (covered on Users) |
GET |
/v1/iam/invites |
List pending invites |
POST |
/v1/iam/invites/:id/cancel |
Cancel an invite before it's accepted |
POST |
/v1/iam/invites/:id/resend |
Re-send the email (rotates the token) |
The accept path itself (POST /v1/iam/invites/accept) is not an admin endpoint — it's hit by the recipient mid-signup. It takes a token and the recipient's session, and on success creates the membership.
List invites
GET /v1/iam/invites
Returns pending invites for the active workspace. Accepted and canceled invites are excluded by default; pass ?include=all to surface them.
Response
{
"data": [
{
"id": "inv_01KPG…",
"email": "recruit@forjio.com",
"role": "member",
"invitedAt": "2026-05-11T14:00:00.000Z",
"expiresAt": "2026-05-18T14:00:00.000Z",
"acceptedAt": null,
"canceledAt": null,
"invitedByUserId": "usr_01KPG…"
}
]
}
expiresAt is always 7 days after the most recent invitedAt. Expired invites stop working server-side but the row stays around for audit; the dashboard renders them with a "Expired" badge so admins can decide whether to resend.
Cancel an invite
POST /v1/iam/invites/:id/cancel
Stamps canceledAt. The token in the recipient's inbox immediately stops working — subsequent accept attempts return INVITE_NOT_FOUND.
204 No Content.
Errors
| Status | error.code |
When |
|---|---|---|
404 |
NOT_FOUND |
No invite with that ID in this workspace. |
409 |
ALREADY_ACCEPTED |
Invite was accepted before you could cancel. |
409 |
ALREADY_CANCELED |
Already canceled. |
Resend an invite
POST /v1/iam/invites/:id/resend
Rotates the token (the old one stops working) and sends a fresh email. expiresAt is extended 7 days from now.
This is the same logic POST /v1/iam/invites runs when called with an email that already has a pending invite — idempotent by design. Calling that endpoint with the original email achieves the same result.
200 OK returns the updated invite row.
Accept an invite (recipient-side)
POST /v1/iam/invites/accept
Called by the recipient, not the admin. They must be signed in to Huudis (matching the invited email) before calling.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
token |
string | yes | The single-use token from the email link. |
On success, returns the workspace they joined; the recipient's session has its activeAccountId switched to the new workspace.
Errors
| Status | error.code |
When |
|---|---|---|
404 |
INVITE_NOT_FOUND |
Token doesn't match an active invite, or has been used/canceled/expired. |
400 |
EMAIL_MISMATCH |
The signed-in user's email doesn't match the invited email. Sign in with the right account. |
The invite object
| Field | Type | Description |
|---|---|---|
id |
string (inv_…) |
Stable invite ID. Used for cancel and resend. |
email |
string | Invited email, lowercased. |
role |
owner | admin | member |
Role the recipient will get after accept. |
invitedAt |
ISO 8601 | Original creation timestamp. |
expiresAt |
ISO 8601 | Always 7 days after the most recent send. |
acceptedAt |
ISO 8601 | null | When the recipient accepted. |
canceledAt |
ISO 8601 | null | When the invite was canceled. |
invitedByUserId |
string (usr_…) |
The admin who sent the invite (most recently for resends). |
The plaintext token is not returned by any API. Only the recipient ever sees it.
Per-email uniqueness
Within one workspace, at most one pending invite per email exists. Creating a second invite for the same email rotates the existing row's token rather than creating a duplicate — the operator doesn't have to clean up before resending.
Accepted and canceled invites can pile up; they don't block a fresh invite to the same email.
Audit and notifications
- The audit log captures
account.member_invitedon send (with the invitee's email and chosen role),account.member_invite_canceledon cancel, andaccount.member_addedwhen the invite is accepted (the latter is also the create event for the membership). - The invitee gets the email at every send. The inviting admin gets a confirmation toast in the dashboard; no email back to them.
Common pitfalls
- Invite expired between send and click. Resend; the original token stops working but a fresh one goes out.
- Recipient signs up with a different email. The accept call fails with
EMAIL_MISMATCH. Resend the invite to the correct email, or have them sign in with the originally invited address. - Recipient already has a Huudis user. Accept works fine — Huudis attaches the membership to the existing user. They keep their existing password.
Events
No webhook events for invites — huudis.account.member_added.v1 fires when the invite is accepted (the membership creation is what consumers care about). The audit log carries the lifecycle.