Skip to content

Email OTP

Email OTP (One-Time Password) provides passwordless authentication using 6-digit numeric codes. Better for mobile where clicking links is less convenient.

  1. Request an OTP

    Via the built-in endpoint:

    POST /auth/otp
    Content-Type: application/json
    
    {"email": "user@example.com"}
    POST /auth/otp
    Content-Type: application/json
    
    {"email": "user@example.com"}

    Always returns 200 regardless of whether the email exists (prevents enumeration).

    Or programmatically:

    code = await auth.create_email_otp(email)
    # Returns 6-digit string, or None if user not found
    code = await auth.create_email_otp(email)
    # Returns 6-digit string, or None if user not found
  2. Deliver the code

    Use the email_otp_requested event hook:

    @auth.on("email_otp_requested")
    async def send_otp(event):
        await send_email(
            to=event.email,
            body=f"Your code: {event.code}",
        )
    @auth.on("email_otp_requested")
    async def send_otp(event):
        await send_email(
            to=event.email,
            body=f"Your code: {event.code}",
        )
  3. Verify and login

    Via the built-in endpoint:

    POST /auth/otp/verify
    Content-Type: application/json
    
    {"email": "user@example.com", "code": "847291"}
    POST /auth/otp/verify
    Content-Type: application/json
    
    {"email": "user@example.com", "code": "847291"}

    Returns an AuthResponse with tokens and user data. Sets cookies automatically if cookie mode is enabled.

    Or programmatically:

    result = await auth.verify_email_otp(email, code)
    # Returns AuthResponse with tokens and user
    result = await auth.verify_email_otp(email, code)
    # Returns AuthResponse with tokens and user

By default, OTP only works for existing users. Set allow_passwordless_signup=True to auto-create accounts:

auth = AuthFort(
    database_url="...",
    allow_passwordless_signup=True,
)
auth = AuthFort(
    database_url="...",
    allow_passwordless_signup=True,
)

OTP login automatically sets email_verified=True — no separate verification step needed.

PropertyDetail
Code format6-digit numeric (1,000,000 combinations)
Code lifetimeSingle-use, expires after email_otp_ttl (default: 5 minutes)
Cross-user protectionThe email in the verify request must match the user who requested the OTP
Ban enforcementBanned users cannot use OTP

Emits email_otp_requested (includes code for delivery), email_otp_login, and optionally user_created (if auto-signup creates a new account).

See Events & Hooks for all events and their payloads.

await auth.requestOTP('user@example.com');

// User enters the 6-digit code
const user = await auth.verifyOTP('user@example.com', '847291');
await auth.requestOTP('user@example.com');

// User enters the 6-digit code
const user = await auth.verifyOTP('user@example.com', '847291');