Skip to main content

Authenticate with Email OTP

Tutorial

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-react installed

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.

  1. Userend user
  2. SDK@keyban/sdk-react
  3. BackendKeyban API
  1. 1enters email
  2. 2POST /auth/send-otp
  3. 3email with one-time code
  4. 4enters OTP
  5. 5POST /auth/verify-otp
  6. 6session JWT + user record
A two-step exchange: the SDK requests an OTP from the backend, the backend mails it to the user, the user enters it back into the SDK which exchanges it for a session JWT.

Step 1 — Enable Email OTP in the Admin Panel

Authentication methods are configured per organization, not per application.

  1. Open the Admin Panel and navigate to Organization → Settings → Authentication.
  2. Toggle Email OTP on.
  3. 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:

  1. Enter your email and click Send Code — you should receive an email with a 6-digit code.
  2. Enter the code and click Verify CodesignIn resolves with a user record and the session is now authenticated.
  3. Confirm by reading useKeybanAuth().user from 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