Developer Tutorials

How to Send OTP in SwiftUI (iOS) — 2026

SwiftUI OTP tutorial using StartMessaging. URLSession, async/await, Observation, iOS keyboard auto-fill, and a clean phone+code flow.

13 May 20269 min read

StartMessaging Team

Engineering

iOS OTP login is one of the cleanest UX wins on the platform — the keyboard surfaces the code automatically. This tutorial wires StartMessaging from SwiftUI.

Overview

  1. URLSession with async/await calls your backend.
  2. @Observable model holds phone / code / requestId.
  3. Two SwiftUI views in a NavigationStack.

Network Client

// AuthApi.swift
struct SendResponse: Codable { let requestId: String; let expiresAt: String }
struct VerifyResponse: Codable { let verified: Bool }

actor AuthApi {
  let base = URL(string: "https://your-backend.example.com")!

  func sendOtp(_ phone: String) async throws -> SendResponse {
    var req = URLRequest(url: base.appending(path: "/auth/send-otp"))
    req.httpMethod = "POST"
    req.setValue("application/json", forHTTPHeaderField: "Content-Type")
    req.httpBody = try JSONEncoder().encode(["phoneNumber": phone])
    let (d, _) = try await URLSession.shared.data(for: req)
    return try JSONDecoder().decode(SendResponse.self, from: d)
  }

  func verifyOtp(requestId: String, code: String) async throws -> VerifyResponse {
    var req = URLRequest(url: base.appending(path: "/auth/verify-otp"))
    req.httpMethod = "POST"
    req.setValue("application/json", forHTTPHeaderField: "Content-Type")
    req.httpBody = try JSONEncoder().encode(["requestId": requestId, "otpCode": code])
    let (d, _) = try await URLSession.shared.data(for: req)
    return try JSONDecoder().decode(VerifyResponse.self, from: d)
  }
}

Observable AuthModel

@Observable
class AuthModel {
  var phone = ""
  var code = ""
  var requestId: String?
  var error: String?
  let api = AuthApi()

  func send() async {
    do { requestId = try await api.sendOtp(phone).requestId }
    catch { self.error = "\(error)" }
  }

  func verify() async -> Bool {
    guard let rid = requestId else { return false }
    do { return try await api.verifyOtp(requestId: rid, code: code).verified }
    catch { self.error = "\(error)"; return false }
  }
}

SwiftUI Views

struct LoginView: View {
  @State private var model = AuthModel()
  var body: some View {
    NavigationStack {
      if model.requestId == nil {
        VStack {
          TextField("Phone", text: $model.phone).keyboardType(.phonePad)
          Button("Send OTP") { Task { await model.send() } }
        }
      } else {
        VStack {
          TextField("OTP", text: $model.code)
            .keyboardType(.numberPad)
            .textContentType(.oneTimeCode)
          Button("Verify") { Task { _ = await model.verify() } }
        }
      }
    }
  }
}

iOS Auto-Fill

textContentType(.oneTimeCode) is all you need. iOS reads the latest SMS, extracts the code, and surfaces it on the keyboard.

FAQ

UIKit version at our Swift iOS guide.

Ready to Send OTPs?

Integrate StartMessaging in 5 minutes. No DLT registration required.