Credential & flow hooks
The drop-in components cover most apps. When
you want to build the auth UI yourself (your own markup, copy, and steps),
these headless hooks drive the same flows. They run the network and parsing on
the Torii runtime (so security-critical logic stays current), while you own the
rendering. You never import flow logic from @torii-js/core or hardcode an API
URL.
All of these require a <ToriiProvider>
ancestor.
The two shapes
Section titled “The two shapes”Every hook is one of two shapes (there is no third), and the shape tells you exactly where to read the error, so you never have to guess:
- Flow hooks (multi-step ceremonies: sign-in, sign-up, reset): a
discriminated
stateunion you branch on (status: 'idle' | 'error' | …), named actions that resolve to the nextstate, andreset(). The error lives instate:state.status === 'error'carriesstate.error. - Data hooks (lists you load and mutate: sessions, emails, identities): a
named payload,
isLoading, a top-levelerror,refresh(), and mutating actions that resolve to aMutationResult<T>. The error lives in the result:r.ok === falsecarriesr.error.
The one deliberate crossover is useSignUp().resend: it is a
single mutation living inside a flow hook, so it returns a MutationResult
rather than moving the machine’s state.
Gating on the runtime
Section titled “Gating on the runtime”Flow actions run on the Torii runtime, which loads asynchronously. Calling an
action before it is ready resolves to { status: 'error' }
(RUNTIME_NOT_LOADED_ERROR) instead of running. To avoid that, the simple flow
hooks (useSignIn, useSignUp, usePasswordReset, useEmailVerification)
expose isLoaded (true once the runtime has loaded); disable your submit until
it is true, or wrap the screen in
<ToriiLoading> / <ToriiLoaded>. If the
runtime fails to load entirely, isLoaded stays false, so pair it with
<ToriiFailed> to show a fallback
rather than a permanently disabled button.
const { isLoaded, signIn } = useSignIn();<button disabled={!isLoaded} onClick={() => signIn({ email, password })}>Sign in</button>The two hooks that fetch their own data first, useOAuthSignup
(loading-intent) and useChangePassword (loading-status), report that
loading in state, so branch on the status rather than reading isLoaded.
Flow hooks
Section titled “Flow hooks”| Hook | Drives |
|---|---|
useSignIn() | Password sign-in |
useSignUp() | Email + password sign-up (start → verify code) |
useInvitationSignUp() | Redeem an organization invitation by setting a password |
usePasswordReset() | Forgot-password (request code → confirm) |
useOAuthSignup() | The OAuth sign-up continuation (consent) leg |
useEmailVerification() | Resend a verification email |
useChangePassword() | Set/change password for the signed-in user |
useOAuthProviders() | The enabled social providers + a redirect starter |
Flows that mint a session (useSignIn, useSignUp, useOAuthSignup) sign the
user in automatically. Pass { autoSignIn: false } to handle it yourself; the
success state still carries tokens for useAuth().signIn(tokens).
useSignIn
Section titled “useSignIn”const { state, isLoaded, signIn, verifyMfa, confirmMfaEnrollment, reset } = useSignIn();await signIn({ email, password });// state.status: 'idle' | 'submitting' | 'error' | 'success'// | 'mfa-required' (account has a factor, call verifyMfa(code))// | 'mfa-enrollment-required' (env enforces MFA, no factor, see below)When two-factor authentication is in
play, signIn resolves to one of two extra states instead of success:
mfa-required({ challengeToken, error? }): the account has a confirmed factor. CallverifyMfa(code)with a TOTP or recovery code; a wrong code keeps the state witherrorset for a retry.mfa-enrollment-required({ challengeToken, otpauthUri, secret, error? }): the environment enforces MFA and the user has no factor yet. RenderotpauthUrias a QR (and showsecretfor manual entry), then callconfirmMfaEnrollment(code). On success the state issuccesswithrecoveryCodesset (show them once).
useSignUp
Section titled “useSignUp”Sign-up emails a 6-digit code and does not create a session until the code is
verified. Read legalSettings to satisfy environments that require legal
consent.
const { state, isLoaded, start, verifyCode, confirmMfaEnrollment, resend, legalSettings } = useSignUp();
await start({ email, password, legalConsentAccepted: true });// state.status -> 'needs-code'await verifyCode(code);// state.status -> 'success' (signed in)// branches: 'code-invalid' (retry), 'code-expired' (start over)// | 'mfa-enrollment-required' (env enforces MFA, see below)In an MFA-enforced environment, verifyCode resolves to mfa-enrollment-required
({ challengeToken, otpauthUri, secret, error? }) instead of success: the user
must enroll a factor to finish signing up. Render the QR + secret, then call
confirmMfaEnrollment(code); on success the state is success with recoveryCodes
(shown once). No session is minted until enrollment completes.
start() also accepts an optional unsafeMetadata field to seed an
unsafe-metadata bag on the new user at
sign-up (max 512 bytes serialized).
useInvitationSignUp
Section titled “useInvitationSignUp”Redeems an organization invitation by setting a password. The email,
organization, and role come from the invitation, so the only inputs are the
token, the chosen password, and the legal-consent flag. On success it creates a
verified user, accepts the invitation, and (unless autoSignIn: false) signs
the user in. Pass { autoSignIn: false } to handle the session yourself; the
success state still carries tokens for useAuth().signIn(tokens). Gate your
form on isLoaded (true once the runtime is ready).
const { state, isLoaded, redeem, reset } = useInvitationSignUp();// InvitationRedeemParams: { token, password, legalConsentAccepted }await redeem({ token, password, legalConsentAccepted: true });// state.status: 'idle' | 'submitting' | 'error' | 'success' (signed in)On 'error', branch on state.error.code: invitation_invalid,
invitation_expired, invitation_account_exists, or weak_password. For
OAuth-based invitation redemption, use the <InvitationSignUp>
component instead: the OAuth leg is a full-page redirect, not a hook action.
usePasswordReset
Section titled “usePasswordReset”const { state, isLoaded, request, confirm, reset } = usePasswordReset();await request(email); // -> 'code-sent' (never reveals if the email exists)await confirm({ code, newPassword }); // -> 'done' (or 'code-invalid' to retry)useOAuthSignup
Section titled “useOAuthSignup”After a provider redirects a new user back with a #__torii_signup_continue
fragment, this loads the pending intent so you can render your own consent
screen.
const { state, complete, reset } = useOAuthSignup();// state.status: 'loading-intent' -> 'intent-ready' (state.intent has email/provider/terms)await complete({ legalConsentAccepted: true }); // -> 'success' (signed in)// | 'mfa-required' ({ challengeToken, kind }) (enforced env, hand off to the MFA step)In an MFA-enforced environment complete may resolve to mfa-required instead of
success: consent was recorded but a second factor is owed. Drive it with
challengeToken: kind: 'enroll' starts enrollment, kind: 'challenge' verifies
an existing factor. The prebuilt <SignUp> handles this for you automatically.
useEmailVerification
Section titled “useEmailVerification”const { state, isLoaded, resend } = useEmailVerification();await resend(email); // state.status: 'idle' | 'sending' | 'sent' | 'error'useChangePassword
Section titled “useChangePassword”const { state, submit, reset } = useChangePassword();// state.status: 'loading-status' -> { status: 'idle', hasPassword }// hasPassword === false for OAuth-only users (omit currentPassword to set one)await submit({ currentPassword, newPassword }); // -> 'done'// reset() returns the machine to 'idle' (keeping the known hasPassword) so the user can retry after an erroruseOAuthProviders
Section titled “useOAuthProviders”const { providers, isLoading, error, start } = useOAuthProviders();providers.map((p) => <button onClick={() => start(p.name)}>{p.displayName}</button>);// start() is a full-page redirect to the providerThis is a data-shaped hook (a named providers payload, a top-level error,
and a redirect-only start, with no state and no reset), so it does not follow
the flow-hook “state + reset” shape described at the top of the page.
Data hooks
Section titled “Data hooks”These follow the same shape as useSessions(),
useEmailAddresses(),
useIdentities(), and
useUserProfile(): a named payload plus
isLoading, error, refresh, and mutating actions that resolve to a
MutationResult.
MutationResult
Section titled “MutationResult”Every mutating action resolves to a discriminated result, so you handle the
outcome from a single await without reading the hook’s error afterwards:
type MutationResult<T> = { ok: true; value: T } | { ok: false; error: ToriiError };
const r = await revokeSession(id);if (r.ok) toast('Signed out that device');else toast(r.error.message);Errors
Section titled “Errors”Hooks never return localized strings. Errors are a ToriiError carrying a
machine-readable code (plus a default message and details). Branch on
code and supply your own copy, or reuse the SDK labels from useAuth().labels.
Next steps
Section titled “Next steps”- Custom flows guide: build a sign-up and reset screen end to end.
- Key Hooks: the everyday
useAuth()/useAuthFetch().