Developer Tutorials

Send OTP from Kotlin (Android Backend / Ktor) — 2026 Guide

Send and verify SMS OTPs from a Kotlin Ktor backend or Android app using the StartMessaging API. Includes Ktor client examples, retrofit option, and Android best practices.

21 April 20269 min read

StartMessaging Team

Engineering

Kotlin powers both Android apps and a growing share of JVM backends. This guide shows how to wire phone OTP through a Kotlin Ktor server that proxies to the StartMessaging OTP API, and how to call that backend from an Android app.

Where to Call StartMessaging From

Always from your backend, never directly from the Android app. The backend holds the API key, applies rate limiting, and stores the request ID securely. For a deeper dive on mobile architecture see mobile app OTP backend patterns.

Ktor Server: Send and Verify

// build.gradle.kts (server)
implementation("io.ktor:ktor-server-netty:2.3.12")
implementation("io.ktor:ktor-client-okhttp:2.3.12")
implementation("io.ktor:ktor-client-content-negotiation:2.3.12")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.Serializable
import java.util.UUID

@Serializable data class SendBody(val phoneNumber: String, val idempotencyKey: String)
@Serializable data class VerifyBody(val requestId: String, val otpCode: String)
@Serializable data class Envelope<T>(val data: T)
@Serializable data class SendData(val requestId: String, val expiresAt: String, val attemptsLeft: Int)
@Serializable data class VerifyData(val verified: Boolean)

object StartMessaging {
    private val key = System.getenv("STARTMESSAGING_API_KEY")
    private val client = HttpClient(OkHttp) {
        install(ContentNegotiation) { json() }
    }

    suspend fun sendOtp(phone: String): SendData =
        client.post("https://api.startmessaging.com/otp/send") {
            header("X-API-Key", key)
            contentType(ContentType.Application.Json)
            setBody(SendBody(phone, UUID.randomUUID().toString()))
        }.body<Envelope<SendData>>().data

    suspend fun verifyOtp(rid: String, code: String): Boolean =
        client.post("https://api.startmessaging.com/otp/verify") {
            header("X-API-Key", key)
            contentType(ContentType.Application.Json)
            setBody(VerifyBody(rid, code))
        }.body<Envelope<VerifyData>>().data.verified
}

Calling Your Backend with Retrofit

interface AuthApi {
    @POST("auth/send-otp")
    suspend fun sendOtp(@Body req: SendOtpReq): SendOtpRes

    @POST("auth/verify-otp")
    suspend fun verifyOtp(@Body req: VerifyOtpReq): VerifyOtpRes
}

data class SendOtpReq(val phoneNumber: String)
data class SendOtpRes(val requestId: String, val expiresAt: String)
data class VerifyOtpReq(val requestId: String, val otpCode: String)
data class VerifyOtpRes(val verified: Boolean)

Auto-Read with SMS Retriever API

Configure your StartMessaging template to include the 11-character app hash and Google’s SMS Retriever API can autofill the code with zero SMS permissions. See our deep-dive on OTP autofill on Android & iOS.

Best Practices

  1. Never embed the API key in the Android app.
  2. Use OkHttp connection pooling on the server (Ktor + OkHttp engine).
  3. Set a 10-second client timeout.
  4. Reuse the HttpClient instance — do not create a new one per request.
  5. Cancel verify coroutines on screen rotation with viewModelScope.

FAQ

Curious about pricing? Rs 0.25 per OTP with no DLT registration required.

Ready to Send OTPs?

Integrate StartMessaging in 5 minutes. No DLT registration required.