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.
Start an OTP login
Send a one-time code to a phone number. If your application is configured for email OTP, use the email-address identifier instead to send the code via email. The SDK sends the code and tracks the verification on the device. Pass the code the user enters tocheckOTP to complete the login.
- iOS
- Android
- Flutter
- React Native
try await client.startOTPLogin(
StartOTPLoginOptions(
identifier: PreludeIdentifier(type: .phoneNumber, value: "+14155551234")
)
)
.emailAddress:try await client.startOTPLogin(
StartOTPLoginOptions(
identifier: PreludeIdentifier(type: .emailAddress, value: "user@example.com")
)
)
import so.prelude.android.session.*
client.startOTPLogin(
StartOTPLoginOptions(
identifier = PreludeIdentifier(
type = PreludeIdentifierType.PHONE_NUMBER,
value = "+14155551234",
),
),
)
EMAIL_ADDRESS:client.startOTPLogin(
StartOTPLoginOptions(
identifier = PreludeIdentifier(
type = PreludeIdentifierType.EMAIL_ADDRESS,
value = "user@example.com",
),
),
)
import 'package:prelude_flutter_session_sdk/prelude_flutter_session_sdk.dart';
await client.startOTPLogin(
StartOTPLoginOptions(
identifier: PreludeIdentifier.phoneNumber('+14155551234'),
),
);
emailAddress convenience constructor:await client.startOTPLogin(
StartOTPLoginOptions(
identifier: PreludeIdentifier.emailAddress('user@example.com'),
),
);
import { PreludeIdentifier } from "@prelude.so/react-native-session-sdk";
await client.startOTPLogin({
identifier: PreludeIdentifier.phoneNumber("+14155551234"),
});
emailAddress convenience constructor:await client.startOTPLogin({
identifier: PreludeIdentifier.emailAddress("user@example.com"),
});
Check the OTP code
Verify the code entered by the user. On success,checkOTP returns the authenticated PreludeUser and the SDK persists the access and refresh tokens in the platform’s secure store.
- iOS
- Android
- Flutter
- React Native
import PreludeSession
do {
let user = try await client.checkOTP("123456")
// User is now authenticated.
} catch PreludeSessionError.invalidOTPCode {
// Wrong code entered
} catch PreludeSessionError.unauthorized {
// Verification expired or invalid
} catch PreludeSessionError.badRequest {
// Server rejected the request
} catch PreludeSessionError.rateLimited {
// Too many attempts
}
import so.prelude.android.session.*
try {
val user = client.checkOTP("123456")
// User is now authenticated.
} catch (e: PreludeSessionError.InvalidOTPCode) {
// Wrong code entered
} catch (e: PreludeSessionError.Unauthorized) {
// Verification expired or invalid
} catch (e: PreludeSessionError.BadRequest) {
// Server rejected the request
} catch (e: PreludeSessionError.RateLimited) {
// Too many attempts
}
import 'package:prelude_flutter_session_sdk/prelude_flutter_session_sdk.dart';
try {
final user = await client.checkOTP('123456');
// User is now authenticated.
} on InvalidOTPCodeException {
// Wrong code entered
} on UnauthorizedException {
// Verification expired or invalid
} on BadRequestException {
// Server rejected the request
} on RateLimitedException {
// Too many attempts
}
import {
BadRequestError,
InvalidOTPCodeError,
RateLimitedError,
UnauthorizedError,
} from "@prelude.so/react-native-session-sdk";
try {
const user = await client.checkOTP("123456");
// User is now authenticated.
} catch (e) {
if (e instanceof InvalidOTPCodeError) {
// Wrong code entered
} else if (e instanceof UnauthorizedError) {
// Verification expired or invalid
} else if (e instanceof BadRequestError) {
// Server rejected the request
} else if (e instanceof RateLimitedError) {
// Too many attempts
} else {
throw e;
}
}
Resend the OTP
If the user didn’t receive the code, ask the server to resend the most recently-issued OTP. Resend reuses the verification opened bystartOTPLogin, so there’s no need to start the flow over.
- iOS
- Android
- Flutter
- React Native
try await client.resendOTP()
client.resendOTP()
await client.resendOTP();
await client.resendOTP();
Try it
Try it
- iOS
- Android
- Flutter
- React Native
Replace
ContentView.swift with:ContentView.swift
import SwiftUI
import PreludeSession
private let appID = "YOUR_APP_ID"
enum Step { case phone, code, done }
@MainActor
final class OTPModel: ObservableObject {
let client = try! PreludeSessionClient(
endpoint: .custom("https://\(appID).session.prelude.dev")
)
@Published var step: Step = .phone
@Published var user: PreludeUser?
@Published var error: String?
func sendCode(to phone: String) async {
error = nil
do {
try await client.startOTPLogin(
StartOTPLoginOptions(
identifier: PreludeIdentifier(type: .phoneNumber, value: phone)
)
)
step = .code
} catch PreludeSessionError.badRequest {
error = "Invalid phone number."
} catch {
self.error = "Something went wrong. Please try again."
}
}
func checkCode(_ code: String) async {
error = nil
do {
user = try await client.checkOTP(code)
step = .done
} catch PreludeSessionError.invalidOTPCode {
error = "Wrong code. Please try again."
} catch PreludeSessionError.unauthorized {
error = "Verification expired. Please start over."
} catch PreludeSessionError.rateLimited {
error = "Too many attempts. Please try again later."
} catch {
self.error = "Something went wrong. Please try again."
}
}
func resend() async {
error = nil
do { try await client.resendOTP() }
catch { self.error = "Could not resend code. Please try again." }
}
}
struct ContentView: View {
@StateObject private var model = OTPModel()
@State private var phone = ""
@State private var code = ""
var body: some View {
VStack(spacing: 12) {
switch model.step {
case .phone:
TextField("Phone (e.g. +14155551234)", text: $phone)
.keyboardType(.phonePad)
Button("Send Code") {
Task { await model.sendCode(to: phone) }
}
.buttonStyle(.borderedProminent)
case .code:
TextField("Enter code", text: $code)
.keyboardType(.numberPad)
Button("Verify Code") {
Task { await model.checkCode(code) }
}
.buttonStyle(.borderedProminent)
Button("Resend Code") {
Task { await model.resend() }
}
case .done:
if let user = model.user {
Text("Logged in").font(.headline)
Text(user.profile.userID ?? "—").font(.caption.monospaced())
}
}
if let error = model.error {
Text(error).foregroundStyle(.red).font(.caption)
}
}
.textFieldStyle(.roundedBorder)
.padding()
}
}
Replace (
MainActivity.kt with:MainActivity.kt
package com.example.sessiontest
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.KeyboardOptions
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.launch
import so.prelude.android.session.*
import java.net.URL
private const val APP_ID = "YOUR_APP_ID"
enum class Step { Phone, Code, Done }
class OtpViewModel(ctx: android.content.Context) : ViewModel() {
val client = PreludeSessionClient(
context = ctx,
baseUrl = URL("https://$APP_ID.session.prelude.dev"),
)
var step by mutableStateOf(Step.Phone); private set
var user by mutableStateOf<PreludeUser?>(null); private set
var error by mutableStateOf<String?>(null); private set
fun sendCode(phone: String) {
error = null
viewModelScope.launch {
try {
client.startOTPLogin(
StartOTPLoginOptions(
identifier = PreludeIdentifier(
type = PreludeIdentifierType.PHONE_NUMBER,
value = phone,
),
),
)
step = Step.Code
} catch (e: PreludeSessionError.BadRequest) {
error = "Invalid phone number."
} catch (e: PreludeSessionError) {
error = "Something went wrong. Please try again."
}
}
}
fun checkCode(code: String) {
error = null
viewModelScope.launch {
try {
user = client.checkOTP(code)
step = Step.Done
} catch (e: PreludeSessionError.InvalidOTPCode) {
error = "Wrong code. Please try again."
} catch (e: PreludeSessionError.Unauthorized) {
error = "Verification expired. Please start over."
} catch (e: PreludeSessionError.RateLimited) {
error = "Too many attempts. Please try again later."
} catch (e: PreludeSessionError) {
error = "Something went wrong. Please try again."
}
}
}
fun resend() {
error = null
viewModelScope.launch {
runCatching { client.resendOTP() }
.onFailure { error = "Could not resend code. Please try again." }
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface {
val vm: OtpViewModel = viewModel(factory = viewModelFactory(applicationContext))
var phone by remember { mutableStateOf("") }
var code by remember { mutableStateOf("") }
Column(Modifier.fillMaxSize().padding(24.dp)) {
when (vm.step) {
Step.Phone -> {
OutlinedTextField(
value = phone, onValueChange = { phone = it },
label = { Text("Phone (e.g. +14155551234)") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.height(12.dp))
Button(onClick = { vm.sendCode(phone) }) { Text("Send Code") }
}
Step.Code -> {
OutlinedTextField(
value = code, onValueChange = { code = it },
label = { Text("Enter code") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.height(12.dp))
Row {
Button(onClick = { vm.checkCode(code) }) { Text("Verify Code") }
Spacer(Modifier.width(8.dp))
OutlinedButton(onClick = { vm.resend() }) { Text("Resend Code") }
}
}
Step.Done -> {
Text("Logged in", style = MaterialTheme.typography.headlineSmall)
Text(vm.user?.profile?.userId ?: "—")
}
}
vm.error?.let {
Spacer(Modifier.height(12.dp))
Text(it, color = MaterialTheme.colorScheme.error)
}
}
}
}
}
}
}
viewModelFactory is the small helper defined in Introduction.)Replace
lib/main.dart with:lib/main.dart
import 'package:flutter/material.dart';
import 'package:prelude_flutter_session_sdk/prelude_flutter_session_sdk.dart';
const appID = 'YOUR_APP_ID';
void main() => runApp(const MyApp());
enum _Step { phone, code, done }
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late final client = PreludeSessionClient(
endpoint: Endpoint.custom('https://$appID.session.prelude.dev'),
);
final _phone = TextEditingController();
final _code = TextEditingController();
_Step _step = _Step.phone;
PreludeUser? _user;
String? _error;
Future<void> _send() async {
setState(() => _error = null);
try {
await client.startOTPLogin(
StartOTPLoginOptions(
identifier: PreludeIdentifier.phoneNumber(_phone.text),
),
);
setState(() => _step = _Step.code);
} on BadRequestException {
setState(() => _error = 'Invalid phone number.');
} on PreludeSessionException {
setState(() => _error = 'Something went wrong. Please try again.');
}
}
Future<void> _check() async {
setState(() => _error = null);
try {
final user = await client.checkOTP(_code.text);
setState(() {
_user = user;
_step = _Step.done;
});
} on InvalidOTPCodeException {
setState(() => _error = 'Wrong code. Please try again.');
} on UnauthorizedException {
setState(() => _error = 'Verification expired. Please start over.');
} on RateLimitedException {
setState(() => _error = 'Too many attempts. Please try again later.');
} on PreludeSessionException {
setState(() => _error = 'Something went wrong. Please try again.');
}
}
Future<void> _resend() async {
setState(() => _error = null);
try {
await client.resendOTP();
} on PreludeSessionException {
setState(() => _error = 'Could not resend code. Please try again.');
}
}
@override
void dispose() {
_phone.dispose();
_code.dispose();
client.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
Widget body;
switch (_step) {
case _Step.phone:
body = Column(mainAxisSize: MainAxisSize.min, children: [
TextField(
controller: _phone,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Phone (e.g. +14155551234)',
),
),
const SizedBox(height: 12),
FilledButton(onPressed: _send, child: const Text('Send Code')),
]);
case _Step.code:
body = Column(mainAxisSize: MainAxisSize.min, children: [
TextField(
controller: _code,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'Enter code'),
),
const SizedBox(height: 12),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
FilledButton(onPressed: _check, child: const Text('Verify Code')),
const SizedBox(width: 8),
OutlinedButton(onPressed: _resend, child: const Text('Resend Code')),
]),
]);
case _Step.done:
body = Column(mainAxisSize: MainAxisSize.min, children: [
const Text('Logged in', style: TextStyle(fontSize: 20)),
const SizedBox(height: 8),
Text(_user?.profile.userID ?? '—',
style: const TextStyle(fontFamily: 'monospace')),
]);
}
return MaterialApp(
home: Scaffold(
body: Padding(
padding: const EdgeInsets.all(24),
child: Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
body,
if (_error != null) ...[
const SizedBox(height: 12),
Text(_error!, style: const TextStyle(color: Colors.red)),
],
]),
),
),
),
);
}
}
Replace
app/index.tsx with:app/index.tsx
import {
Endpoint,
InvalidOTPCodeError,
PreludeIdentifier,
PreludeSessionClient,
PreludeSessionError,
PreludeUser,
RateLimitedError,
UnauthorizedError,
} from "@prelude.so/react-native-session-sdk";
import { useEffect, useRef, useState } from "react";
import { Button, Text, TextInput, View } from "react-native";
const APP_ID = "YOUR_APP_ID";
type Step = "phone" | "code" | "done";
export default function Home() {
const clientRef = useRef<PreludeSessionClient | null>(null);
const [step, setStep] = useState<Step>("phone");
const [phone, setPhone] = useState("");
const [code, setCode] = useState("");
const [user, setUser] = useState<PreludeUser | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const c = new PreludeSessionClient({
endpoint: Endpoint.custom(`https://${APP_ID}.session.prelude.dev`),
});
clientRef.current = c;
return () => { c.dispose().catch(() => {}); };
}, []);
async function sendCode() {
setError(null);
try {
await clientRef.current!.startOTPLogin({
identifier: PreludeIdentifier.phoneNumber(phone),
});
setStep("code");
} catch (e) {
setError("Something went wrong. Please try again.");
}
}
async function checkCode() {
setError(null);
try {
const u = await clientRef.current!.checkOTP(code);
setUser(u);
setStep("done");
} catch (e) {
if (e instanceof InvalidOTPCodeError) setError("Wrong code. Please try again.");
else if (e instanceof UnauthorizedError) setError("Verification expired. Please start over.");
else if (e instanceof RateLimitedError) setError("Too many attempts. Please try again later.");
else if (e instanceof PreludeSessionError) setError("Something went wrong.");
else throw e;
}
}
async function resend() {
setError(null);
try {
await clientRef.current!.resendOTP();
} catch {
setError("Could not resend code. Please try again.");
}
}
return (
<View style={{ padding: 24, gap: 8 }}>
{step === "phone" && (
<>
<TextInput
placeholder="Phone (e.g. +14155551234)"
value={phone}
onChangeText={setPhone}
keyboardType="phone-pad"
style={{ borderWidth: 1, padding: 8 }}
/>
<Button title="Send Code" onPress={sendCode} />
</>
)}
{step === "code" && (
<>
<TextInput
placeholder="Enter code"
value={code}
onChangeText={setCode}
keyboardType="number-pad"
style={{ borderWidth: 1, padding: 8 }}
/>
<Button title="Verify Code" onPress={checkCode} />
<Button title="Resend Code" onPress={resend} />
</>
)}
{step === "done" && user && (
<>
<Text style={{ fontWeight: "600" }}>Logged in</Text>
<Text>{user.profile.userID ?? "—"}</Text>
</>
)}
{error && <Text style={{ color: "red" }}>{error}</Text>}
</View>
);
}