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.
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.security.SecureRandom
import java.util.UUID
@Serializable data class SendBody(
val phoneNumber: String,
val variables: Map<String, String>, // must contain "otp"
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 otpRequestId: String, val messageId: String, val status: String)
@Serializable data class VerifyData(val verified: Boolean)
object StartMessaging {
private val key = System.getenv("STARTMESSAGING_API_KEY")
private val random = SecureRandom()
private val client = HttpClient(OkHttp) {
install(ContentNegotiation) { json() }
}
// Generated on the server, never in the Android app
private fun generateOtp(): String = (100000 + random.nextInt(900000)).toString()
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, mapOf("otp" to generateOtp()), UUID.randomUUID().toString()))
}.body<Envelope<SendData>>().data // return data.otpRequestId to the app as requestId
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) // the otpRequestId from your backend
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
- Never embed the API key in the Android app.
- Use OkHttp connection pooling on the server (Ktor + OkHttp engine).
- Set a 10-second client timeout.
- Reuse the HttpClient instance — do not create a new one per request.
- Cancel verify coroutines on screen rotation with viewModelScope.
FAQ
Curious about pricing? Rs 0.25 per OTP with no DLT registration required.
StartMessaging Team
StartMessaging Team