The invite page
One page and one redeem call handle every invite kind.
Every invitation link points at the same page, carrying only ?token=. The invitee never needs to know what kind of invite they hold.
const { data: invite } = await authClient.invite.get({ token });Rendering by nextAction
invite.nextAction | Meaning | What to render |
|---|---|---|
SIGN_UP | Invite-only, no session | The fields listed in requiredFields |
SIGN_IN | Open mode, no session | Your sign-in form; keep the token in the URL |
CONFIRM | Open mode, session present | A confirm button, plus any fields listed |
null | Expired, revoked, or consumed | A clear terminal message |
Two of the four are basic and never collect fields: SIGN_IN hands off to your sign-in form and null is a message. The other two are forms, and both can carry additional fields: SIGN_UP collects them while creating the account, CONFIRM while activating on an existing one.
requiredFields lists exactly what to collect: always password and name for sign-up, plus email for public invites, plus organizationName and organizationSlug for org-create (CONFIRM has no built-in fields). Additional fields configured for the current step appear too: required ones in requiredFields, optional ones in optionalFields, and invite.additionalFields carries { type, required } for each so your page knows which input to render:
{
"nextAction": "SIGN_UP",
"requiredFields": ["password", "name", "department"],
"optionalFields": ["referral"],
"additionalFields": {
"department": { "type": "string", "required": true },
"referral": { "type": "string", "required": false }
}
}What your page will see
Most invitees have no account yet, so live invites return nextAction: "SIGN_UP": your page is a sign-up form driven by requiredFields. An invitee who already holds an established account (an org inviting an existing user, or someone redeeming a second invite after signing up through the first) gets "SIGN_IN" (no session) or "CONFIRM" (signed in), exactly like open mode, so render all three plus null. The server decides from the account's live state; the page never has to work out which case it is in. On a passwordless app the response carries passwordless: true and requiredFields omits password, so a field-driven form needs no special casing.
Redeeming
Submit everything to one endpoint:
await authClient.invite.redeem({
token,
password,
name,
email,
organizationName,
organizationSlug
// ...plus any configured additional fields, e.g. department
});
// -> { action: "ACCEPTED", organization? }redeem routes to the right semantics for the mode and kind, so your page never branches. The response says what happened, so you can render "You joined Acme" or "Your organization is ready".
name is required whenever the redemption signs the invitee up (nextAction: "SIGN_UP"); a sign-up without a name defeats the profile-populating point of the flow, so it fails with NAME_REQUIRED. Sign-in and confirm flows act on an existing account and take no name; a CONFIRM redemption still submits any additional fields configured for that step, in the same body.
Redemption never creates a session. After ACCEPTED in the sign-up flow, your page already holds the email and password it just collected, so sign the user in with them:
await authClient.signIn.email({ email, password });In the CONFIRM flow the user was already signed in, and nothing about their session (including the active organization) is changed; switching to a newly joined org is your call.
A deliberately thin payload
invite.get returns only kind, role, derived status, expiry, uses remaining, and a masked email
such as a***@example.com. Never the inviter's identity or internal ids.
Where to go next
Last updated on