Authenticate with Email OTP
What you'll build
A minimal React login form that signs a user in with a one-time code sent to their email — and gives them an embedded non-custodial wallet on the way. The same pattern adapts to Phone OTP, Google or Auth0 once you've internalised the two-step flow.
- Time~10 min
- DifficultyBeginner — basic React
- You'll needA Keyban organization with Email OTP enabled, and
@keyban/sdk-reactinstalled
How the flow works
The exchange is a two-leg conversation between the user, the SDK, and the Keyban backend. Step 1 mails an OTP; step 2 trades the code for a session.
- Userend user
- SDK@keyban/sdk-react
- BackendKeyban API
- 1enters email
- 2POST /auth/send-otp
- 3email with one-time code
- 4enters OTP
- 5POST /auth/verify-otp
- 6session JWT + user record
Step 1 — Enable Email OTP in the Admin Panel
Authentication methods are configured per organization, not per application.
- Open the Admin Panel and navigate to Organization → Settings → Authentication.
- Toggle Email OTP on.
- Click Save.
The setting is persisted on Organization.settings.authConfig and picked up by the SDK on the next config fetch.
Step 2 — Wrap your app with the Keyban providers
KeybanProvider configures the wallet stack; KeybanAuthProvider exposes the auth state to your component tree.
import { KeybanProvider, KeybanAuthProvider } from "@keyban/sdk-react";
function App() {
return (
<KeybanProvider appId="your-app-id" network="StarknetSepolia">
<KeybanAuthProvider>
<YourApp />
</KeybanAuthProvider>
</KeybanProvider>
);
}
Step 3 — Build the login component
The component below is the smallest useful form: it asks for an email, sends an OTP, then asks for the code and signs the user in. Both inputs are KeybanInput — never plain <input> — because OTP codes and email addresses must stay inside the Keyban iframe boundary.
import { KeybanInput, useKeybanAuth } from "@keyban/sdk-react";
import { useState } from "react";
export default function EmailOTPLogin() {
const [showOtp, setShowOtp] = useState(false);
const [error, setError] = useState<string | null>(null);
const { sendOtp, signIn, isLoading } = useKeybanAuth();
const handleSendCode = async () => {
setError(null);
try {
await sendOtp({
type: "email-otp",
emailInputName: "email",
});
setShowOtp(true);
} catch (err) {
setError("Failed to send OTP. Please check the email and try again.");
console.error(err);
}
};
const handleVerifyCode = async () => {
setError(null);
try {
await signIn({
type: "email-otp",
emailInputName: "email",
otpInputName: "otp",
});
} catch (err) {
setError("Invalid OTP. Please try again.");
console.error(err);
}
};
return (
<form onSubmit={(e) => e.preventDefault()}>
{error && <div style={{ color: "red" }}>{error}</div>}
{!showOtp ? (
<div>
<label>Email:</label>
<KeybanInput name="email" type="email" />
</div>
) : (
<div>
<label>Verification Code:</label>
<KeybanInput
name="otp"
inputMode="numeric"
inputStyles={{ textAlign: "center" }}
/>
</div>
)}
<button
type="button"
onClick={showOtp ? handleVerifyCode : handleSendCode}
disabled={isLoading}
>
{isLoading ? "Processing..." : showOtp ? "Verify Code" : "Send Code"}
</button>
</form>
);
}
The contract between the form and the SDK is the input name: sendOtp and signIn reference inputs by name (emailInputName, otpInputName), so the names you pick here must match exactly.
Step 4 — Verify it works
Drop <EmailOTPLogin /> somewhere inside your KeybanAuthProvider and run the app:
- Enter your email and click Send Code — you should receive an email with a 6-digit code.
- Enter the code and click Verify Code —
signInresolves with a user record and the session is now authenticated. - Confirm by reading
useKeybanAuth().userfrom any descendant component — it returns the signed-in user.
If the email never arrives, double-check that Email OTP is enabled in the Admin Panel and that your account is configured to receive transactional mail.
Step 5 — Handle the authenticated session
Once signed in, useKeybanAuth() exposes the user record and session state. Read it from any component inside KeybanAuthProvider:
import { useKeybanAuth } from "@keyban/sdk-react";
export function UserProfile() {
const { user, isAuthenticated } = useKeybanAuth();
if (!isAuthenticated) return <div>Loading or not authenticated</div>;
return (
<div>
<p>Welcome, {user.email}!</p>
<p>Your wallet is ready to use.</p>
</div>
);
}
Sessions expire after inactivity. For production apps, wrap your pages with RequireAuth to automatically redirect users to sign-in when the session has expired.
Next steps
- Read the conceptual overview if you want to understand the security model and the iframe boundary: Keyban Auth — concepts.
- Add other methods (Phone OTP, Google, Auth0) or integrate Material UI: Authentication recipes.
- Browse the full SDK surface:
useKeybanAuthandKeybanInput.