Skip to main content

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 to checkOTP to complete the login.
try await client.startOTPLogin(
    StartOTPLoginOptions(
        identifier: PreludeIdentifier(type: .phoneNumber, value: "+14155551234")
    )
)
For email OTP, use .emailAddress:
try await client.startOTPLogin(
    StartOTPLoginOptions(
        identifier: PreludeIdentifier(type: .emailAddress, value: "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.
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
}

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 by startOTPLogin, so there’s no need to start the flow over.
try await client.resendOTP()
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()
    }
}