Team Invitations
RESTHeart CloudTeam invitations allow users with the owner role to add people to their tenant.
|
Important
|
The invite endpoint requires the JWT to contain the active tenant claim. Ensure the following configuration:
Without these, the service cannot determine the caller’s team role and returns |
The behavior differs depending on whether the invited user already has an account.
flowchart TD
A[POST /auth/invite\nrequires owner role] --> B{User exists?}
B -- No --> C[Check pending invitation\nin auth_invitations]
C -- Pending exists --> L2[409 Conflict]
C -- None --> D[Create user roles: $unauthenticated\nCreate auth_invitations doc isNewUser=true]
D --> E[Email: /auth/activate\n?email=…&token=…]
E --> F1[PATCH /auth/activate\nemail + token + password]
E --> F2[OAuth authorize\n?pendingInviteToken=…]
F1 --> G[Set password\nMembershipProvider.addMember\nroles → user\nDelete auth_invitations\nAuto-login JWT cookie]
F2 --> G2[addMember from invite\nactivateViaOAuth\nDelete auth_invitations\nJWT cookie]
B -- Yes, not in tenant --> H[Create auth_invitations doc\nisNewUser=false TTL 7 days]
H --> I[Email: /invitations/accept\n?email=…&token=…]
I --> J1[POST /auth/accept-invite\ntoken — auth required]
I --> J2[OAuth authorize\n?pendingInviteToken=…]
J1 --> K[MembershipProvider.addMember\nsetActiveMembership\nDelete auth_invitations\n200 OK]
J2 --> K
B -- Yes, already member --> L[409 Conflict]
Invite a user
POST /auth/invite — requires role owner.
Request
POST /auth/invite
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "bob@example.com",
"role": "member"
}
role must be one of: the configured member-role-name (default "member") or the configured ownership-role (default "owner").
|
Important
|
The admin role is a system-level ACL role in RESTHeart and should NEVER be used as a team/membership role. Using admin as a membership role grants the user unrestricted access to the entire system.
|
Server-side steps
-
Authorize the caller (must be
ownerof their tenant). -
Validate
emailformat androlevalue. -
If the user already belongs to the caller’s tenant →
409 Conflict. -
If a pending invitation already exists for the same email+org in
auth_invitations→409 Conflict. -
If the user does not exist → create user with
roles: ["$unauthenticated"]. Create anauth_invitationsdocument withisNewUser: true. Send activation email with link to{frontend-url}/auth/activate?email=…&token=…. Membership is NOT added yet — it is deferred until the user activates. -
If the user exists but is NOT in this tenant → create an
auth_invitationsdocument withisNewUser: false. Do NOT add membership yet. Send invite email with link to{frontend-url}/invitations/accept?email=…&token=…. -
Return
201 Created.
|
Note
|
All invitation tokens — for both new and existing users — are stored exclusively in the The
|
Email links
| User type | Email link |
|---|---|
New user (no account) |
|
Existing user |
|
Read invitation metadata
GET /auth/invitation?email=…&token=… — public.
The frontend calls this on page load to determine which UI to show and to display the org name and role.
Both parameters are required. The (email, token) pair is known only to the invitee (delivered via private email link), so this endpoint cannot be used for enumeration.
Response 200 OK
{
"email": "bob@example.com",
"orgName": "Acme Corp",
"role": "member",
"isNewUser": true,
"expiresAt": "2026-06-24T10:00:00Z"
}
Frontend behavior
-
isNewUser: true→ show "Set your password" form + OAuth buttons →PATCH /auth/activate -
isNewUser: false→ show "Log in to accept" form + OAuth buttons → login thenPOST /auth/accept-invite
Error responses
| Status | Reason |
|---|---|
|
Missing |
|
No valid (non-expired) invitation found for that |
Activate an invitation (new user)
PATCH /auth/activate — public (the user has only the token link).
Request
PATCH /auth/activate
Content-Type: application/json
{
"email": "bob@example.com",
"token": "<token from email>",
"password": "correct-horse-battery"
}
Server-side steps
-
Find invitation by
(email, token)inauth_invitations(not on user document). -
Verify
isNewUser: true—400otherwise (wrong endpoint). -
Check expiry (TTL 7 days).
-
Enforce password strength.
-
Set password, assign
roles: ["user"]. -
MembershipProvider.addMember(userId, orgId, role)— adds user to the org and sets active tenant (readsorgIdandrolefrom the invitation document). -
Delete invitation from
auth_invitations(one-shot). -
Issue JWT + cookie → auto-login.
-
Return
200 OK.
|
Note
|
Consents are not managed by this endpoint.
Use a response interceptor on the deployment layer to persist consents from the request body.
See the |
Error responses
| Status | Reason |
|---|---|
|
Missing fields; password too weak; |
|
Invalid or expired invite token |
Accept an invitation (existing user)
POST /auth/accept-invite — requires authentication.
Request
POST /auth/accept-invite
Authorization: Bearer <token>
Content-Type: application/json
{
"token": "<token from email>"
}
Server-side steps
-
Find invitation by
tokeninauth_invitations. -
Verify
isNewUser: false—400otherwise (wrong endpoint). -
Verify invitation email matches the authenticated user.
-
MembershipProvider.addMember(userId, orgId, role)— adds user to team. -
MembershipProvider.setActiveMembership(userId, orgId)— switches the user’s active org to the newly joined one. -
Delete invitation from
auth_invitations. -
Return
200 OK.
Frontend flow
-
User clicks link in email →
/invitations/accept?email=…&token=… -
Frontend calls
GET /auth/invitation?email=…&token=…to get org name and role. -
If not logged in → shows "Log in to accept" with OAuth buttons + login form.
-
After login → calls
POST /auth/accept-invite { token }. -
On success → redirect to dashboard.
Accept via OAuth
Both new and existing invited users can accept via OAuth. The pendingInviteToken is passed as a query parameter to the OAuth authorize URL.
OAuth URL
{api-base-url}/auth/oauth/authorize/{provider}?pendingInviteToken=...
New user (roles: $unauthenticated)
-
OAuth callback detects
$unauthenticateduser withpendingInviteToken. -
Find invitation by
(email, pendingInviteToken)inauth_invitations; callMembershipProvider.addMember(userId, orgId, role)to set tenant. -
MembershipProvider.activateViaOAuth(userId, consents)— assignsroles: ["user"], stores consents. -
Delete invitation from
auth_invitations. JWT cookie set, redirect tofrontend-app-url.
Existing user
-
OAuth callback detects user is NOT
$unauthenticatedandpendingInviteTokenis present. -
Find invitation by
(email, pendingInviteToken)inauth_invitations. -
MembershipProvider.addMember(userId, orgId, role). -
Delete invitation. Issue JWT. Redirect to
frontend-app-url.
Re-send an invitation
POST /auth/resend-invite — requires role owner.
Request body:
{ "email": "bob@example.com" }
Generates a new token (invalidating the previous one) by updating the auth_invitations document, and re-sends the invitation email. Works for both new and existing user invitations.
Returns 200 OK on success. Returns 404 if no pending invitation is found for that user in the caller’s team.
Remove a member
DELETE /auth/remove-member — requires role owner.
Request
DELETE /auth/remove-member
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "bob@example.com"
}
Server-side steps
-
Authorize the caller (must be
ownerof their active tenant). -
Verify the target user is a member of the caller’s tenant —
404 Not Foundotherwise. -
Prevent the owner from removing themselves (
400 Bad Request). -
Call
MembershipProvider.removeMember(email, tenantId). -
Return
200 OK.
Error responses
| Status | Reason |
|---|---|
|
Missing |
|
Caller does not have |
|
Target user is not a member of the caller’s tenant |
Update a member’s role
PATCH /auth/member-role — requires role owner.
Request
PATCH /auth/member-role
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "bob@example.com",
"role": "member"
}
role must be one of: the configured member-role-name (default "member") or the configured ownership-role (default "owner").
|
Important
|
The admin role is a system-level ACL role and cannot be assigned as a team role via this endpoint.
|
Server-side steps
-
Authorize the caller (must be
ownerof their active tenant). -
Validate
role—400 Bad Requestif notmemberRoleNameorownershipRole. -
Verify the target user is a member of the caller’s tenant —
404 Not Foundotherwise. -
Call
MembershipProvider.updateMemberRole(email, tenantId, newRole). -
Return
200 OK.
Error responses
| Status | Reason |
|---|---|
|
Missing fields; or |
|
Caller does not have |
|
Target user is not a member of the caller’s tenant |