Invites

An invite is a pending workspace membership. The flow:

  1. An admin calls POST /v1/iam/invites with an email and role.
  2. Huudis emails a single-use token link to that address.
  3. The recipient clicks, lands on huudis.com/invites/{token}, signs in or creates a Huudis account, and accepts.
  4. The invite row flips to acceptedAt = now and a new account_member row 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_invited on send (with the invitee's email and chosen role), account.member_invite_canceled on cancel, and account.member_added when 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.

Next

  • Users — the create-invite endpoint and the alternative "direct add" flow.
  • Account — the recipient's own session view post-accept.