Developer Tutorials

Send Phone OTP in Swift / iOS — 2026 Tutorial

Verify phone numbers in iOS apps with Swift, calling your backend that proxies the StartMessaging OTP API. Includes URLSession, async/await, and SwiftUI form examples.

22 April 20268 min read

StartMessaging Team

Engineering

Phone OTP is the standard way to verify users on iOS in India, especially for fintech, food delivery, and quick-commerce apps. This guide shows the SwiftUI + async/await pattern that calls your own backend, which in turn proxies the StartMessaging OTP API.

Architecture: Backend Proxy

Never call StartMessaging directly from the iOS app. The flow looks like:

iOS app  ──POST /auth/send-otp──▶ Your backend ──▶ StartMessaging API
iOS app  ──POST /auth/verify-otp─▶ Your backend ──▶ StartMessaging API

Swift API Client

import Foundation

struct SendOtpRes: Decodable {
    let requestId: String
    let expiresAt: String
}

struct VerifyOtpRes: Decodable { let verified: Bool }

enum OtpError: Error { case server, decoding }

actor AuthClient {
    let base = URL(string: "https://api.yourapp.com")!

    func sendOtp(phone: String) async throws -> SendOtpRes {
        var req = URLRequest(url: base.appendingPathComponent("auth/send-otp"))
        req.httpMethod = "POST"
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")
        req.httpBody = try JSONSerialization.data(withJSONObject: ["phoneNumber": phone])

        let (data, res) = try await URLSession.shared.data(for: req)
        guard let http = res as? HTTPURLResponse, http.statusCode < 400 else { throw OtpError.server }
        return try JSONDecoder().decode(SendOtpRes.self, from: data)
    }

    func verifyOtp(requestId: String, code: String) async throws -> Bool {
        var req = URLRequest(url: base.appendingPathComponent("auth/verify-otp"))
        req.httpMethod = "POST"
        req.setValue("application/json", forHTTPHeaderField: "Content-Type")
        req.httpBody = try JSONSerialization.data(withJSONObject: [
            "requestId": requestId, "otpCode": code
        ])

        let (data, _) = try await URLSession.shared.data(for: req)
        return try JSONDecoder().decode(VerifyOtpRes.self, from: data).verified
    }
}

SwiftUI Phone Login Form

struct PhoneLoginView: View {
    @State private var phone = ""
    @State private var requestId: String?
    @State private var code = ""
    let client = AuthClient()

    var body: some View {
        VStack {
            if requestId == nil {
                TextField("+91 98765 43210", text: $phone)
                    .keyboardType(.phonePad)
                Button("Send OTP") {
                    Task {
                        let res = try await client.sendOtp(phone: phone)
                        requestId = res.requestId
                    }
                }
            } else {
                TextField("Enter code", text: $code)
                    .keyboardType(.numberPad)
                    .textContentType(.oneTimeCode) // iOS autofill from SMS banner
                Button("Verify") {
                    Task {
                        let ok = try await client.verifyOtp(
                            requestId: requestId!, code: code)
                        if ok { /* navigate */ }
                    }
                }
            }
        }
    }
}

iOS One-Time Code Autofill

Setting .textContentType(.oneTimeCode) on the field tells iOS to suggest the code from the most recent SMS in QuickType. There is no template requirement on the SMS body — iOS recognises 4–6 digit codes automatically. For Android-side parity see OTP autofill on Android & iOS.

Best Practices

  1. Use App Transport Security — never plain HTTP.
  2. Pin your backend’s certificate or public key.
  3. Store the requestId only in memory, not in UserDefaults.
  4. Disable the Verify button until 4–6 digits are entered.
  5. Show a 30-second resend timer to prevent abuse.

FAQ

See pricing or read the React Native / Flutter version here.

Ready to Send OTPs?

Integrate StartMessaging in 5 minutes. No DLT registration required.