Invitations

Invitations bring new users into a team. A team admin invites a user by email address; the recipient accepts to join the workspace. The invitee does not need an existing Xagent account first — if the email doesn't match one yet, the invite still sends, and the recipient creates their account and joins in one step from the emailed link. All management endpoints (create, resend, cancel, list) authenticate with a JWT access token and require team admin (or system admin) privileges.

Sending an Invitation

A team admin invites a user and assigns their role (team_role defaults to member):

POST /api/teams/{team_id}/invitations
{ "email": "grace@example.com", "team_role": "member" }
{
  "id": 5,
  "team_id": 1,
  "user_id": 9,
  "invitee_email": "grace@example.com",
  "invited_by": 7,
  "status": "pending",
  "team_role": "member",
  "created_at": "2026-07-10T09:15:00Z",
  "responded_at": null,
  "email_status": "sent",
  "email_last_error": null
}

user_id is null when the invitee has no account yet — the invitation is still created (unlinked) and links itself to an account automatically once one exists with that email. email_status reports whether the invite email itself was delivered (sent or failed); a failed send does not fail the request; the invitation is still created, and an admin can retry it with resend.

Errors: 422 invalid email address, 409 "User is already a member of this team", 409 "An invitation is already pending for this user", 409 "User has already accepted an invitation to this team". Re-inviting an email whose prior invite expired or was rejected transparently reissues it with a fresh token rather than erroring.

List a team's invitations with GET /api/teams/{team_id}/invitations to track who has yet to accept — any team member can view the list, but only team admins see each row's email_last_error (it can contain raw delivery diagnostics). Force a fresh token and re-send the email with POST /api/teams/{team_id}/invitations/{invitation_id}/resend, or withdraw a pending invite with DELETE /api/teams/{team_id}/invitations/{invitation_id}.

Accepting an Invitation

The emailed link opens an accept page that behaves differently depending on whether the invitee already has an account:

Invitee has an accountFlow
YesSign in, then POST /api/invitations/by-token/{token}/accept — the signed-in account must match the invited email (or an already-linked user_id), otherwise this returns 403.
NoPOST /api/invitations/register with { "token", "password" } — creates the account and accepts the invitation in one unauthenticated call. The token itself is the proof of authorization.

A signed-in user can also list and act on their own pending invitations directly, matched by account or by invited email:

GET  /api/invitations                        # my pending invitations
POST /api/invitations/{invitation_id}/accept
POST /api/invitations/{invitation_id}/reject
{ "message": "Invitation accepted", "team_id": 1, "team_role": "member" }

Accepting moves the user into the team with the assigned role. Errors: 404 "Invitation not found or already processed", and for accept, 402 if the team is already at its seat limit — see Plans.

Registering directly with an invited email joins that team automatically

If you create your account through ordinary registration or Google sign-up (instead of the emailed invite link) using the same email address a pending invitation was sent to, you are moved into the inviting team automatically right after your account is created, and the empty personal team that registration would otherwise have left you in is removed. This only happens while you are still the sole member of your new personal team, so it cannot move an established account. If you registered with a registration code, this is skipped so your code-redeemed team isn't replaced; the invitation stays pending and you can accept it deliberately later. When several teams have invited the same address, only the most recently invited one is joined this way — the rest remain pending and are still visible and acceptable from the team page.

Accepting can be blocked by your current team's knowledge bases

If accepting this invitation would leave your current team (because you are its only member), acceptance is refused with 409 and a machine-readable body — { "code": "invitation_team_knowledge_bases", "team_id", "team_name", "knowledge_bases": [...] } — listing that team's knowledge base names. The app shows this as an inline recovery panel linking to Knowledge Bases so you can promote, demote, or delete each one first, then retry accepting. This mirrors the existing rule that a team cannot be deleted while it still owns knowledge bases; it now also applies when your departure would empty the team out from under an invitation accept.

Regional deployments: the link switches region first

On a deployment split across regions (see the country choice in Getting Started), the emailed accept link now first routes through a brief "switching region" step before landing on the accept page shown above, so the invitee ends up on the team's own region rather than whichever region served the click. This is transparent to the invitee — no extra action is needed — but if you script against the accept-link URL, expect an intermediate redirect rather than a direct link straight to /accept-invite.

Tip

Invite users at the role they need from the start. You can change a member's effective access later by removing and re-inviting, or through team administration.

Next Steps