Skip to Content
SDK v3newAuthenticationEmail & password

Email & password

Classic email and password sign-in — signInWithEmail and signUp. Behaviour depends on the platform’s email-confirmation setting: when it’s on, new users get an email with a code and must enter it; when it’s off, signUp returns a session immediately.

signInWithEmail

const session = await auth.signInWithEmail({ email: 'user@example.com', password: 'secret123' });

On success:

  • the session is written to storage (key pw-<paywallId>-auth-v1),
  • onAuthChange is emitted with the new session,
  • a BillingClient bound to this auth starts attaching the Bearer token.

Parameters

auth.signInWithEmail(input: { email: string; password: string; userMeta?: Record<string, string>; }): Promise<AuthSession>

Errors

Wrong email or password — PaywallError with status: 401 and code: 'invalid_credentials' (when the backend passes it through).

try { await auth.signInWithEmail({ email, password }); } catch (e) { if (e instanceof PaywallError && e.status === 401) { showError('Wrong email or password'); return; } throw e; }

signUp

const result = await auth.signUp({ email: 'new@example.com', password: 'secret123' }); if (result.kind === 'signed_in') { // session issued already, user is signed in console.log(result.session.user.email); } else { // result.kind === 'confirmation_required' — email confirmation needed // show an "enter the code from the email" screen and call verifyOtp }

Discriminated union

type SignUpResult = | { kind: 'signed_in'; session: AuthSession } | { kind: 'confirmation_required'; user: { id: string; email: string } };

Confirmation required. When the platform has email confirmation enabled, signUp does NOT issue tokens — it returns kind: 'confirmation_required'. The user gets an email with a 6-digit code; the next step is verifyOtp({ type: 'signup', token: '<code>' }). Details: Email OTP.

Full signup flow with email confirmation

Step 1: call signUp

const res = await auth.signUp({ email, password }); if (res.kind === 'signed_in') { redirectToApp(); return; } // res.kind === 'confirmation_required' showVerifyCodeScreen({ email });

Step 2: user enters the code

const session = await auth.verifyOtp({ email, token: codeFromInput, type: 'signup' }); // session is signed in — auth.getCachedUser() returns the user

Step 3: business as usual

onAuthChange fires — BillingClient picks up the token and identity automatically.

Handling signin/signup on a single UI

You often want one “email + password” screen without forcing the user to pick “I don’t have an account yet” / “I have an account”. A handy pattern — signUp first, fall back to signInWithEmail on user_already_exists:

async function loginOrSignUp(email: string, password: string) { try { const res = await auth.signUp({ email, password }); return res; // signed_in or confirmation_required } catch (e) { // the platform's auth backend returns user_already_exists (status 400/422) const isAlreadyExists = e instanceof PaywallError && (e.code === 'user_already_exists' || e.code === 'email_exists'); if (!isAlreadyExists) throw e; const session = await auth.signInWithEmail({ email, password }); return { kind: 'signed_in' as const, session }; } }

An alternative — use Email OTP: one path for both signin and signup, no password. More convenient in a Chrome extension where password managers often misbehave.

Security

  • Don’t log password to Sentry/console. The SDK never writes it anywhere — but if you wrap it in your own code, double-check that only email is in traces.
  • Minimum lengthsignUp does not validate on the client; the platform’s auth backend rejects short passwords with weak_password. For stricter rules, validate in your UI before calling the SDK.
  • Rate limit. the platform’s auth backend rate-limits signin/signup by IP — after 3–4 failed attempts it starts returning 429. The SDK does not auto-retry; in the UI show “try later” without permanently disabling the button.

Next steps