On this page

MIOSA has two parallel invite flows:

  • Workspace invites — invite a user to a specific workspace. If the email already belongs to an org member, the user is added directly (no email sent). If not, an invite email is dispatched and accepting it creates both the tenant_members and workspace_members rows atomically.
  • Org invites — invite a user to join the org (tenant) without pre-selecting a workspace. The accept step only creates the tenant_members row.

Workspace Invite Endpoints

MethodPathAuthDescription
POST/workspaces/{id}/invitesRequiredSend a workspace invite
GET/workspaces/{id}/invitesRequiredList pending invites
DELETE/workspaces/{id}/invites/{invite_id}RequiredRevoke an invite
GET/workspace-invites/{token}None (public)Preview invite details
POST/workspace-invites/{token}/acceptRequiredAccept the invite

Org Invite Endpoints

MethodPathAuthDescription
POST/tenants/{id}/invitesRequired (admin/owner)Send an org invite
GET/tenants/{id}/invitesRequired (admin/owner)List pending org invites
DELETE/tenants/{id}/invites/{invite_id}Required (admin/owner)Revoke an org invite
GET/invites/{token}None (public)Preview org invite details
POST/invites/{token}/acceptRequiredAccept the org invite

Workspace Invite Flow

Send Workspace Invite

POST /api/v1/workspaces/{id}/invites

Sends an invite email and returns the invite record. If the email already belongs to a tenant member the user is added directly (response type = "added", no email sent).

Request Body

FieldTypeRequiredDescription
emailstringYesEmail address to invite
rolestringYesowner, admin, member, or viewer

Response — 201 Created (new invite)

{
  "type": "invited",
  "data": {
    "id": "inv_abc123",
    "workspace_id": "ws-uuid",
    "tenant_id": "ten-uuid",
    "email": "alice@example.com",
    "role": "member",
    "invited_by": "usr_owner",
    "expires_at": "2026-05-29T09:00:00Z",
    "accepted_at": null,
    "inserted_at": "2026-05-22T09:00:00Z"
  }
}

Response — 200 OK (existing member added directly)

{
  "type": "added",
  "data": {
    "user_id": "usr_def456",
    "workspace_id": "ws-uuid",
    "role": "member",
    "joined_at": "2026-05-22T09:00:00Z",
    "added_by": "usr_owner"
  }
}

List Workspace Invites

GET /api/v1/workspaces/{id}/invites

Returns all pending workspace invites (excludes accepted and revoked).

Response — 200 OK

{
  "data": [
    {
      "id": "inv_abc123",
      "workspace_id": "ws-uuid",
      "tenant_id": "ten-uuid",
      "email": "alice@example.com",
      "role": "member",
      "invited_by": "usr_owner",
      "expires_at": "2026-05-29T09:00:00Z",
      "accepted_at": null,
      "inserted_at": "2026-05-22T09:00:00Z"
    }
  ]
}

Revoke Workspace Invite

DELETE /api/v1/workspaces/{id}/invites/{invite_id}

Revokes a pending invite. Returns 409 if the invite was already accepted.

Response — 200 OK

{ "invite_id": "inv_abc123", "revoked": true }

Preview Workspace Invite (public)

GET /api/v1/workspace-invites/{token}

Returns invite metadata without requiring authentication. Use this to render the pre-auth landing page.

Response — 200 OK

{
  "data": {
    "workspace_name": "Dr. Smith Clinic",
    "role": "member",
    "expires_at": "2026-05-29T09:00:00Z",
    "expired": false,
    "accepted": false
  }
}

Accept Workspace Invite

POST /api/v1/workspace-invites/{token}/accept

Accepts the invite on behalf of the authenticated user. If the user is new to the org, a tenant_members row is created atomically alongside the workspace_members row.

The caller’s API key email must match the invite email (case-insensitive).

Response — 200 OK

{
  "accepted": true,
  "workspace_id": "ws-uuid",
  "tenant_id": "ten-uuid",
  "role": "member"
}

Errors

StatusCodeCause
400INVALID_TOKENToken not found or expired
400REVOKEDInvite was revoked
422EMAIL_MISMATCHAuthenticated user’s email differs from invite email

Org Invite Flow

Org invites grant membership in the tenant (org) without assigning a specific workspace. Write operations require admin or owner role.

Send Org Invite

POST /api/v1/tenants/{id}/invites

Dispatches an invite email. The response includes an invite_url that is host-aware — on white-label tenants it uses the tenant’s custom domain.

Request Body

FieldTypeRequiredDescription
emailstringYesEmail address to invite
rolestringYesowner, admin, or member

Response — 201 Created

{
  "data": {
    "invite_id": "inv_xyz789",
    "email": "bob@example.com",
    "role": "member",
    "expires_at": "2026-05-29T09:00:00Z",
    "invite_url": "https://app.miosa.ai/invites/eyJ..."
  }
}

List Org Invites

GET /api/v1/tenants/{id}/invites

Returns all pending org invites.


Revoke Org Invite

DELETE /api/v1/tenants/{id}/invites/{invite_id}

Revokes a pending org invite. Returns 409 if the invite was already accepted.


Preview Org Invite (public)

GET /api/v1/invites/{token}

Returns invite metadata without authentication.

Response — 200 OK

{
  "data": {
    "email": "bob@example.com",
    "tenant_name": "Acme Corp",
    "role": "member",
    "expires_at": "2026-05-29T09:00:00Z",
    "expired": false,
    "accepted": false
  }
}

Accept Org Invite

POST /api/v1/invites/{token}/accept

Accepts an org invite on behalf of the authenticated user. Creates a tenant_members row.

The caller’s API key email must match the invite email (case-insensitive).

Response — 200 OK

{
  "accepted": true,
  "tenant_id": "ten-uuid",
  "tenant": {
    "id": "ten-uuid",
    "name": "Acme Corp"
  }
}

Errors

StatusCodeCause
400invalid_tokenToken not found or expired
422EMAIL_MISMATCHSession email differs from invite email

Common Errors

StatusCodeCause
400INVALID_TOKEN / invalid_tokenToken not found, expired, or revoked
400REVOKEDInvite was explicitly revoked
409ALREADY_ACCEPTEDCannot revoke an invite that was already accepted
422EMAIL_MISMATCHAuthenticated user’s email does not match the invite email
422MISSING_EMAILemail field missing from request body

See also

Was this helpful?