Skip to main content
Para provides simplified React hooks that handle the entire authentication flow in a single call. Instead of managing multiple steps (signup/login, verification, session polling, wallet creation) separately, these hooks orchestrate everything automatically and return a unified response. Available in v2.13.0+
These hooks are long-running — they internally poll for session completion and wait for the user to finish interacting with the portal. Place the hook call in a provider or higher-order component that will not unmount during the authentication flow. If the component unmounts while the hook is running, the authentication will be interrupted.
While the hooks manage the flow end-to-end, you are responsible for opening the portal URLs that Para generates during authentication (for verification, passkey creation, password entry, etc.). Use para.onStatePhaseChange() to listen for these URLs and open them. Passkey URLs must be opened in a popup — WebAuthn does not work in iframes. See Handling State Changes below.

Prerequisites

You must have a Para account set up with authentication methods enabled in your Developer Portal. Install the React SDK:
npm install @getpara/react-sdk --save-exact
Ensure your app is wrapped with the ParaProvider as described in the React quickstart guide.

Hooks Reference

This hook must be paired with para.onStatePhaseChange() to handle portal URLs that appear during authentication (verification, passkey, password, PIN). See Handling State Changes for the full state listener pattern.
This hook must be paired with para.onStatePhaseChange() to handle portal URLs that appear after OAuth completes (e.g. passkey or password setup for returning users). See Handling State Changes for the full state listener pattern.

Handling State Changes

During authentication, Para’s state machine progresses through phases that require user interaction — either opening portal URLs (for basic login users and biometric flows) or showing a code input (for non-basic-login new signups). Since you’re building a custom UI without the ParaModal, you need to subscribe to state changes and handle them yourself.
Passkey URLs must be opened in a popup window, not an iframe. WebAuthn/passkey operations require a top-level browsing context and will fail silently in iframes due to browser security restrictions. Password and PIN URLs can be opened in either a popup or an iframe. Verification URLs can also use either approach.
Use para.onStatePhaseChange() to receive a StateSnapshot. The snapshot contains authPhase (what stage the flow is in) and authStateInfo (URLs and flags for the current stage):
import { useEffect, useRef, useState } from "react";
import { useClient } from "@getpara/react-sdk";
import type { StateSnapshot, AuthPhase } from "@getpara/web-sdk";

function useParaAuthStateListener() {
  const para = useClient();
  const popupRef = useRef<Window | null>(null);
  const lastUrlRef = useRef<string | null>(null);
  const [authPhase, setAuthPhase] = useState<AuthPhase>("unauthenticated");
  const [authStateInfo, setAuthStateInfo] = useState<StateSnapshot["authStateInfo"] | null>(null);

  useEffect(() => {
    const unsubscribe = para.onStatePhaseChange((snapshot) => {
      const { authStateInfo, authPhase } = snapshot;
      setAuthPhase(authPhase);
      setAuthStateInfo(authStateInfo);

      // Basic login verification URL — open in popup or iframe
      if (authStateInfo.verificationUrl && authStateInfo.verificationUrl !== lastUrlRef.current) {
        lastUrlRef.current = authStateInfo.verificationUrl;
        popupRef.current = window.open(
          authStateInfo.verificationUrl,
          "ParaVerification",
          "popup,width=400,height=500"
        );
        return;
      }

      // Biometric / security URLs
      const { passkeyUrl, passwordUrl, pinUrl } = authStateInfo;

      // Passkey URLs MUST use a popup — WebAuthn does not work inside iframes
      if (passkeyUrl && passkeyUrl !== lastUrlRef.current) {
        lastUrlRef.current = passkeyUrl;
        popupRef.current = window.open(passkeyUrl, "ParaPasskey", "popup,width=400,height=500");
      } else if (passwordUrl && passwordUrl !== lastUrlRef.current) {
        lastUrlRef.current = passwordUrl;
        popupRef.current = window.open(passwordUrl, "ParaPassword", "popup,width=400,height=500");
      } else if (pinUrl && pinUrl !== lastUrlRef.current) {
        lastUrlRef.current = pinUrl;
        popupRef.current = window.open(pinUrl, "ParaPIN", "popup,width=400,height=500");
      }
    });

    return () => {
      unsubscribe();
      lastUrlRef.current = null;
    };
  }, [para]);

  return { authPhase, authStateInfo, popupRef };
}
When authPhase is 'awaiting_account_verification', the user is a non-basic-login new signup who has been sent an OTP code via email or SMS. There is no URL to open — you must show a code input field and call useVerifyNewAccount() to submit the code. If the user needs a new code, use useResendVerificationCode(). The simplified hook is waiting for this step to complete before it proceeds. See the full example below.

authStateInfo Fields

FieldTypeDescription
verificationUrlstring | nullPortal URL for basic login users to complete auth (OTP, passkey, etc.) in the hosted portal. Only set during awaiting_session_start for basic login flows. Can be opened in a popup or iframe.
passkeyUrlstring | nullPortal URL for passkey login or creation. Must be opened in a popup — WebAuthn does not work in iframes.
passwordUrlstring | nullPortal URL for password login or creation. Can be opened in a popup or iframe.
pinUrlstring | nullPortal URL for PIN login or creation. Can be opened in a popup or iframe.
isPasskeySupportedbooleanWhether the user’s device supports passkeys/WebAuthn.
isNewUserbooleanWhether this is a new signup flow.
passkeyHintsBiometricLocationHint[] | nullHints for known device detection.

State Phase Reference

The StateSnapshot returned by para.onStatePhaseChange() contains three phase fields that tell you exactly where in the flow the user is. Use these to drive your UI.

corePhase — Top-Level Lifecycle

PhaseDescription
setting_upPara is initializing. Show a loading indicator.
auth_flowThe user is in the authentication flow. Refer to authPhase for details.
wallet_flowAuthentication succeeded; wallets are being set up. Refer to walletPhase for details.
authenticatedThe user is fully logged in with wallets ready.
guest_modeThe user is in guest mode (no full authentication).
logging_outA logout is in progress.
errorAn unrecoverable error occurred. Check snapshot.error.

authPhase — Authentication Flow Detail

PhaseDescriptionWhat to show
unauthenticatedNo auth in progress.Login / signup form.
authenticating_email_phoneEmail/phone auth initiated, routing in progress.Loading indicator.
authenticating_oauthStandard OAuth flow in progress.Loading indicator or “Waiting for provider…”.
authenticating_telegramTelegram auth in progress.Telegram popup/iframe.
authenticating_farcasterFarcaster auth in progress.Farcaster QR code.
processing_authenticationServer is processing the auth request.Loading indicator.
verifying_new_accountVerification code is being validated.Loading indicator.
awaiting_account_verificationNew user (non-basic-login) needs to enter an OTP verification code sent to their email or phone. No URL to open — authStateInfo will not have a verificationUrl.Show a code input field. Call para.verifyNewAccount({ verificationCode }) when submitted.
awaiting_session_startPortal URLs are ready. For basic login users, authStateInfo.verificationUrl is set (user completes auth in the portal). For passkey/password/PIN users, passkeyUrl, passwordUrl, and/or pinUrl are set.Open the appropriate URL. Passkey URLs must use a popup on web.
waiting_for_sessionSession polling is active — waiting for the user to complete in the portal.”Completing setup…” loading state.
authenticatedAuth flow is complete. (Core may still be in wallet_flow.)Transition to authenticated UI, or show wallet loading indicator.
guest_modeThe user is in guest mode (no full authentication).Guest UI.
errorAuth flow failed. Check snapshot.error.Error message with retry option.
Basic login vs passkey/password/PIN: Basic login users complete their entire authentication through a portal URL — the verificationUrl handles OTP entry, passkey creation, etc. in a single hosted flow. Passkey, password, and PIN users go through a two-step process where OTP verification happens in your app (via awaiting_account_verification) and biometric setup happens in the portal (via awaiting_session_start).

walletPhase — Wallet Setup Detail

PhaseDescription
checking_wallet_stateChecking if wallets need to be created.
needs_walletsWallets need to be created for this user.
claiming_walletsClaiming pregenerated wallets that were created for this user before signup.
creating_walletsWallets are being created.
waiting_for_walletsWaiting for wallet creation or claiming to complete.
setting_up_after_loginPerforming post-login wallet setup (e.g. refreshing session, syncing wallet state).
wallets_readyWallets are created and ready to use.
no_wallets_neededUser already has wallets; nothing to create.
errorWallet creation failed. Check snapshot.error.

Email / Phone Authentication

Use useAuthenticateWithEmailOrPhone to authenticate a user by email or phone number. The hook handles the complete flow: it determines whether the user is new or returning, manages session polling, waits for session establishment, and creates wallets for new signups. You need to handle two things alongside the hook:
  1. State listener — subscribe to onStatePhaseChange to open portal URLs (verification, passkey, password, PIN) when they become available.
  2. OTP code input — when authPhase is 'awaiting_account_verification', show a code input and call useVerifyNewAccount() to submit the code. The hook is waiting for this before it proceeds.
import { useState } from "react";
import {
  useAuthenticateWithEmailOrPhone,
  useVerifyNewAccount,
  useResendVerificationCode,
} from "@getpara/react-sdk";

function EmailAuth() {
  const {
    authenticateWithEmailOrPhoneAsync,
    isPending,
    error,
  } = useAuthenticateWithEmailOrPhone();

  const { verifyNewAccountAsync } = useVerifyNewAccount();
  const { resendVerificationCodeAsync } = useResendVerificationCode();

  // Use the state listener hook from the section above
  const { authPhase, popupRef } = useParaAuthStateListener();

  const [email, setEmail] = useState("");
  const [verificationCode, setVerificationCode] = useState("");

  const handleAuth = async () => {
    try {
      const result = await authenticateWithEmailOrPhoneAsync({
        auth: { email },
        sessionPollingCallbacks: {
          onPoll: () => {
            if (popupRef.current?.closed) {
              popupRef.current = null;
            }
          },
        },
      });

      if (result.hasCreatedWallets && result.recoverySecret) {
        // Non-basic-login new user — display or store the recovery secret
        console.log("Recovery secret:", result.recoverySecret);
      }

      // User is now fully authenticated
      console.log("Auth info:", result.authInfo);
    } catch (err) {
      console.error("Authentication failed:", err);
    }
  };

  // Show OTP input when awaiting verification for non-basic-login new signups
  if (authPhase === "awaiting_account_verification") {
    return (
      <div>
        <p>Enter the verification code sent to {email}</p>
        <input
          value={verificationCode}
          onChange={(e) => setVerificationCode(e.target.value)}
          placeholder="6-digit code"
        />
        <button onClick={() => verifyNewAccountAsync({ verificationCode })}>
          Verify
        </button>
        <button onClick={() => resendVerificationCodeAsync({ type: "SIGNUP" })}>
          Resend Code
        </button>
      </div>
    );
  }

  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Enter your email"
      />
      <button onClick={handleAuth} disabled={isPending}>
        {isPending ? "Authenticating..." : "Continue"}
      </button>
      {error && <p>{error.message}</p>}
    </div>
  );
}
For phone number authentication, pass { phone: '+1234567890' } instead of { email }:
authenticateWithEmailOrPhoneAsync({
  auth: { phone: `+${countryCode}${phoneNumber}` as `+${number}` },
});

OAuth Authentication

Use useAuthenticateWithOAuth to authenticate a user via a third-party OAuth provider. The hook manages the OAuth redirect/popup, polls for completion, waits for session establishment, and creates wallets for new signups.

Standard OAuth (Google, Apple, Discord, X, Facebook)

For standard OAuth providers, the onOAuthPopup callback gives you the initial popup window. The state listener handles biometric URLs that appear after the OAuth step completes (e.g. when a returning user needs to authenticate with their passkey).
import { useAuthenticateWithOAuth } from "@getpara/react-sdk";

function OAuthLogin() {
  const {
    authenticateWithOAuthAsync,
    isPending,
    error,
  } = useAuthenticateWithOAuth();

  // Use the state listener hook from the section above
  const { popupRef } = useParaAuthStateListener();

  const handleOAuth = async (method: "GOOGLE" | "APPLE" | "DISCORD" | "X" | "FACEBOOK") => {
    try {
      const result = await authenticateWithOAuthAsync({
        method,
        redirectCallbacks: {
          onOAuthPopup: (popup) => {
            popupRef.current = popup;
          },
        },
        oAuthPollingCallbacks: {
          onPoll: () => {
            if (popupRef.current?.closed) {
              popupRef.current = null;
            }
          },
        },
      });

      if (result.hasCreatedWallets && result.recoverySecret) {
        console.log("Recovery secret:", result.recoverySecret);
      }

      console.log("Auth info:", result.authInfo);
    } catch (err) {
      console.error("OAuth failed:", err);
    }
  };

  return (
    <div>
      <button onClick={() => handleOAuth("GOOGLE")} disabled={isPending}>
        Continue with Google
      </button>
      <button onClick={() => handleOAuth("APPLE")} disabled={isPending}>
        Continue with Apple
      </button>
      <button onClick={() => handleOAuth("DISCORD")} disabled={isPending}>
        Continue with Discord
      </button>
      {error && <p>{error.message}</p>}
    </div>
  );
}

Telegram

Telegram authentication works the same way — the hook manages the Telegram bot interaction automatically:
const result = await authenticateWithOAuthAsync({
  method: "TELEGRAM",
  redirectCallbacks: {
    onOAuthPopup: (popup) => {
      popupRef.current = popup;
    },
  },
});

Farcaster

Farcaster uses a QR code flow. Use the redirectCallbacks.onOAuthUrl callback to receive the Farcaster Connect URI and display it as a QR code:
const [farcasterUri, setFarcasterUri] = useState<string | null>(null);

const handleFarcaster = async () => {
  try {
    const result = await authenticateWithOAuthAsync({
      method: "FARCASTER",
      redirectCallbacks: {
        onOAuthUrl: (url) => {
          setFarcasterUri(url);
        },
      },
      oAuthPollingCallbacks: {
        isCanceled: () => !farcasterUri,
      },
    });

    setFarcasterUri(null);
    console.log("Auth info:", result.authInfo);
  } catch (err) {
    console.error("Farcaster auth failed:", err);
  }
};

Cancelling Authentication

Both hooks accept polling callbacks with an isCanceled function. Return true from isCanceled to stop the polling loop — for example, when the user closes a popup or navigates away. The cancellation is clean: no error is thrown, and the optional onCancel callback is fired.
const result = await authenticateWithEmailOrPhoneAsync({
  auth: { email },
  sessionPollingCallbacks: {
    isCanceled: () => {
      // Cancel if the user closed the popup
      return popupRef.current === null || popupRef.current.closed;
    },
    onCancel: () => {
      console.log("User canceled authentication");
    },
  },
});
For OAuth, you can cancel both the OAuth polling and session polling independently:
const result = await authenticateWithOAuthAsync({
  method: "GOOGLE",
  oAuthPollingCallbacks: {
    isCanceled: () => userClickedCancel,
    onCancel: () => console.log("OAuth polling canceled"),
  },
  sessionPollingCallbacks: {
    isCanceled: () => userClickedCancel,
    onCancel: () => console.log("Session polling canceled"),
  },
});
Calling logout also cancels all active polling and resets the state phases back to unauthenticated. This is useful for implementing a “Cancel” button that fully resets the auth flow:
import { useLogout } from "@getpara/react-sdk";

const { logoutAsync } = useLogout();

const handleCancel = async () => {
  await logoutAsync();
  // All polling stops, state phases reset to unauthenticated
};

Handling Results

Both hooks return an AuthenticateResponse object with the same shape:
type AuthenticateResponse = {
  authInfo: CoreAuthInfo;      // The user's authentication info (email, userId, etc.)
  hasCreatedWallets: boolean;  // Whether new wallets were created during this flow
  recoverySecret?: string;     // Recovery secret for non-basic-login newly created wallets
};
FieldDescription
authInfoContains the user’s primary authentication information such as email, phone, userId, and any auth extras.
hasCreatedWalletstrue if the user is new and wallets were auto-created during signup. false for returning users.
recoverySecretPresent only for non-basic-login when new wallets are created. Basic login users do not receive a recovery secret. This should be displayed to the user or stored securely — it cannot be retrieved again.

Next Steps