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), onAuthChangeis emitted with the new session,- a
BillingClientbound to thisauthstarts attaching the Bearer token.
Parameters
Signature
auth.signInWithEmail(input: {
email: string;
password: string;
userMeta?: Record<string, string>;
}): Promise<AuthSession>Errors
invalid_credentials
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 userStep 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
passwordto 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 length —
signUpdoes not validate on the client; the platform’s auth backend rejects short passwords withweak_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
- Session management —
onAuthChange,signOut, usingAuthSessionin API calls. - Email OTP — passwordless sign-in with one email.
- Password reset — “forgot password” flow.