> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prelude.so/llms.txt
> Use this file to discover all available pages before exploring further.

# OTP Login

> Implement OTP-based authentication with the Prelude JavaScript SDK.

## Start an OTP login

Send a one-time password to a phone number:

```javascript theme={null}
await client.startOTP({
  identifier: {
    type: "phone_number",
    value: "+14155551234",
  },
});
```

If your application is [configured for email OTP](/session/documentation/integration-guide/otp-login), you can use `email_address` instead of `phone_number` to send the code via email:

```javascript theme={null}
await client.startOTP({
  identifier: {
    type: "email_address",
    value: "user@example.com",
  },
});
```

The SDK sends the OTP and sets a verification cookie on the client. The user can then enter the code to complete the login.

## Check the OTP code

Verify the code entered by the user:

```javascript theme={null}
import { PrldErrors } from "@prelude.so/js-sdk";

try {
  await client.checkOTP({ code: "123456" });
  // User is now authenticated
} catch (error) {
  if (error instanceof PrldErrors.BadCheckCode) {
    // Wrong code entered
  } else if (error instanceof PrldErrors.Unauthorized) {
    // Verification expired or invalid
  } else if (error instanceof PrldErrors.BadRequest) {
    // Invalid request
  } else if (error instanceof PrldErrors.RateLimited) {
    // Too many attempts
  }
}
```

On success, the API sets a [refresh cookie](/session/documentation/cookies) on the client.

## Retry sending the OTP

If the user didn't receive the code, retry sending it:

```javascript theme={null}
await client.retryOTP();
```

<Accordion title="Try it" icon="flask">
  Replace `src/App.jsx` with:

  ```jsx src/App.jsx theme={null}
  import "@picocss/pico";
  import { useState } from "react";
  import { PrldSessionClient, PrldErrors, decode } from "@prelude.so/js-sdk";

  const client = new PrldSessionClient({ domain: `${import.meta.env.VITE_APP_ID}.session.prelude.dev` });

  export default function App() {
    const [phone, setPhone] = useState("");
    const [code, setCode] = useState("");
    const [step, setStep] = useState("phone"); // "phone" | "code" | "done"
    const [user, setUser] = useState(null);
    const [error, setError] = useState(null);

    const handleSendOTP = async (e) => {
      e.preventDefault();
      setError(null);
      try {
        await client.startOTP({
          identifier: { type: "phone_number", value: phone },
        });
        setStep("code");
      } catch (err) {
        if (err instanceof PrldErrors.BadRequest) {
          setError("Invalid phone number.");
        } else {
          setError("Something went wrong. Please try again.");
        }
      }
    };

    const handleCheckOTP = async (e) => {
      e.preventDefault();
      setError(null);
      try {
        await client.checkOTP({ code });
        const { user } = await client.refresh();
        setUser(user);
        setStep("done");
      } catch (err) {
        if (err instanceof PrldErrors.BadCheckCode) {
          setError("Wrong code. Please try again.");
        } else if (err instanceof PrldErrors.Unauthorized) {
          setError("Verification expired. Please start over.");
        } else if (err instanceof PrldErrors.RateLimited) {
          setError("Too many attempts. Please try again later.");
        } else {
          setError("Something went wrong. Please try again.");
        }
      }
    };

    const handleRetry = async () => {
      setError(null);
      try {
        await client.retryOTP();
      } catch (err) {
        setError("Could not resend code. Please try again.");
      }
    };

    return (
      <>
        <style>{`body, #root { display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; padding: 0; }`}</style>
        <main style={{ textAlign: "center", maxWidth: "600px", width: "100%" }}>
          <h1>OTP Login</h1>
          {step === "done" && user ? (
            <article>
              <p>Logged in</p>
              <pre style={{ textAlign: "left", whiteSpace: "pre-wrap", wordBreak: "break-all" }}>
                {JSON.stringify(decode(user.accessToken).claims, null, 2)}
              </pre>
            </article>
          ) : step === "code" ? (
            <form onSubmit={handleCheckOTP}>
              <input type="text" placeholder="Enter code" value={code} onChange={(e) => setCode(e.target.value)} required />
              {error && <p role="alert" style={{ color: "var(--pico-color-red-500)" }}>{error}</p>}
              <button type="submit">Verify Code</button>
              <button type="button" className="secondary" onClick={handleRetry}>Resend Code</button>
            </form>
          ) : (
            <form onSubmit={handleSendOTP}>
              <input type="tel" placeholder="Phone number (e.g. +14155551234)" value={phone} onChange={(e) => setPhone(e.target.value)} required />
              {error && <p role="alert" style={{ color: "var(--pico-color-red-500)" }}>{error}</p>}
              <button type="submit">Send Code</button>
            </form>
          )}
        </main>
      </>
    );
  }
  ```
</Accordion>
