Read SMS Automatically to Verify OTP in Android: Kotlin + SMS Retriever API

Auto-verify OTP on Android without SMS permissions using Kotlin. This guide shows you three proven methods — SMS Retriever API (recommended), User Consent API, and BroadcastReceiver — with complete code examples, app-hash generation, and server-side SMS formatting. We'll also flag which approach will get your app rejected from Google Play.

You know that moment: a user opens your banking or auth app, an OTP lands, and instead of hunting through their messages app to copy it manually, the app fills it in automatically. That one small feature feels magical. But implementing it is trickier than it looks — and picking the wrong method can get your app blocked from the Play Store.

In this tutorial, you'll learn when to use each method, how to generate your app's SMS hash in Kotlin, format server-side SMS correctly so it works, and handle fallback scenarios when auto-read fails. By the end, you'll know which approach fits your app's constraints.

Comparison of SMS Retriever, User Consent, BroadcastReceiver for OTP auto‑verification Android

Auto-verify OTP on Android without SMS permissions

Three Methods to Read SMS Automatically to Verify OTP (Android)

Method Permission Play Store Safe? Best For
SMS Retriever API None required ✅ Yes When you control the SMS format on the server
User Consent API None required ✅ Yes When you can't control the SMS format
BroadcastReceiver + READ_SMS READ_SMS required 🔴 Rejected unless default SMS app Default SMS apps only
⛔ Play Store Warning: Since 2019, Google Play rejects apps that request READ_SMS or RECEIVE_SMS unless the app is a default SMS/messaging app. If your app is not a default SMS app, use Method 1 or Method 2. Using Method 3 will get your app rejected during review.
Diagram comparing SMS Retriever API, User Consent API, and BroadcastReceiver READ_SMS methods for OTP reading in Android

Comparison of SMS Retriever, User Consent, BroadcastReceiver for OTP auto‑verification Android

Method 1 — SMS Retriever API (Recommended) — Auto‑verify OTP in Android (Kotlin)

No permissions required. Google's SMS Retriever API intercepts a specific SMS that contains your app's unique hash — and delivers it directly to your app. The user never sees a permission prompt.

Step 1: Add the dependency

dependencies {
    implementation "com.google.android.gms:play-services-auth-api-phone:18.0.2"
}

Step 2: Generate your app's hash

This is the step most tutorials skip — and it's the one that makes the API silently fail if you get it wrong. The SMS must contain an 11-character hash unique to your app. Generate it using this utility:

import android.content.Context
import android.content.pm.PackageManager
import android.util.Base64
import java.nio.charset.StandardCharsets
import java.security.MessageDigest

object AppHashHelper {

    fun getAppHash(context: Context): String {
        val packageName = context.packageName
        val signatures = context.packageManager
            .getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
            .signatures
        val appInfo = "$packageName ${signatures[0]}"
        val messageDigest = MessageDigest.getInstance("SHA-256")
        messageDigest.update(appInfo.toByteArray(StandardCharsets.UTF_8))
        val hashBytes = messageDigest.digest()
        val base64Hash = Base64.encodeToString(hashBytes, Base64.NO_WRAP or Base64.NO_PADDING)
        // Return first 11 characters — this is your app hash
        return base64Hash.substring(0, 11)
    }
}

// Log it during development to get your hash
Log.d("AppHash", AppHashHelper.getAppHash(this))
💡 Important: Your app hash changes with each signing keystore. Generate separate hashes for your debug keystore (development) and release keystore (production). Pass both to your backend so the SMS works in both environments.

Step 3: Format the SMS correctly on your server

The SMS must start with <#> and end with your 11-character app hash. The OTP can be anywhere in between:

<#> Your OTP is 483920. It is valid for 10 minutes.
FA+9qCX9VSu

Rules the SMS must follow:

  • Must start with <#>
  • Must be no longer than 140 bytes
  • Must end with the 11-character app hash on the last line

Step 4: Create the BroadcastReceiver

class SmsBroadcastReceiver : BroadcastReceiver() {

    // Callback to pass the OTP back to the Activity/Fragment
    var otpReceived: ((String) -> Unit)? = null

    override fun onReceive(context: Context?, intent: Intent?) {
        if (SmsRetriever.SMS_RETRIEVED_ACTION != intent?.action) return

        val extras = intent.extras ?: return
        val status = extras.get(SmsRetriever.EXTRA_STATUS) as? com.google.android.gms.common.api.Status
            ?: return

        when (status.statusCode) {
            SmsRetriever.RESULT_STATUS_OK -> {
                val message = extras.getString(SmsRetriever.EXTRA_SMS_MESSAGE) ?: return
                val otp = extractOtp(message)
                otp?.let { otpReceived?.invoke(it) }
            }
            SmsRetriever.RESULT_TIMEOUT -> {
                // SMS Retriever timed out after 5 minutes
            }
        }
    }

    // Extracts first 6-digit sequence from the message
    private fun extractOtp(message: String): String? {
        val regex = Regex("\\d{6}")
        return regex.find(message)?.value
    }
}

Step 5: Start the retriever and register the receiver

class OtpVerificationActivity : AppCompatActivity() {

    private val smsBroadcastReceiver = SmsBroadcastReceiver()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_otp)

        startSmsRetriever()

        // Auto-fill OTP when received
        smsBroadcastReceiver.otpReceived = { otp ->
            binding.etOtp.setText(otp)
            verifyOtp(otp)
        }
    }

    private fun startSmsRetriever() {
        val client = SmsRetriever.getClient(this)
        client.startSmsRetriever()
            .addOnSuccessListener {
                // Retriever started — will listen for 5 minutes
            }
            .addOnFailureListener {
                // Failed to start — handle gracefully, let user enter OTP manually
            }
    }

    override fun onStart() {
        super.onStart()
        val intentFilter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)
        // API 34+ requires RECEIVER_NOT_EXPORTED for non-system broadcasts
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            registerReceiver(smsBroadcastReceiver, intentFilter, RECEIVER_NOT_EXPORTED)
        } else {
            registerReceiver(smsBroadcastReceiver, intentFilter)
        }
    }

    override fun onStop() {
        super.onStop()
        unregisterReceiver(smsBroadcastReceiver)
    }
}

Method 2 — User Consent API — Read OTP SMS with Consent

No app hash needed. No SMS format requirements. Google shows the user a bottom sheet with the matching SMS and asks for consent to share it with your app. If they tap "Yes", you get the full message. Great for when you don't fully control the SMS format on your server.

import com.google.android.gms.auth.api.phone.SmsRetriever
import com.google.android.gms.auth.api.phone.SmsCodeRetriever

class OtpActivity : AppCompatActivity() {

    private val SMS_CONSENT_REQUEST = 2

    private val smsBroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            if (intent?.action == SmsRetriever.SMS_RETRIEVED_ACTION) {
                val extras = intent.extras
                val smsRetrieverStatus = extras?.get(SmsRetriever.EXTRA_STATUS)
                        as? com.google.android.gms.common.api.Status

                when (smsRetrieverStatus?.statusCode) {
                    SmsRetriever.RESULT_STATUS_OK -> {
                        // Get consent intent — shows bottom sheet to user
                        val consentIntent = extras.getParcelable<Intent>(SmsRetriever.EXTRA_CONSENT_INTENT)
                        startActivityForResult(consentIntent, SMS_CONSENT_REQUEST)
                    }
                }
            }
        }
    }

    private fun startUserConsent() {
        // Optional: pass sender phone number to filter SMS
        SmsRetriever.getClient(this).startSmsUserConsent(null)
    }

    // Handle user's response to the consent bottom sheet
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == SMS_CONSENT_REQUEST && resultCode == RESULT_OK) {
            val message = data?.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE) ?: return
            val otp = Regex("\\d{6}").find(message)?.value
            otp?.let { binding.etOtp.setText(it) }
        }
    }

    override fun onStart() {
        super.onStart()
        startUserConsent()
        val intentFilter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            registerReceiver(smsBroadcastReceiver, intentFilter, RECEIVER_NOT_EXPORTED)
        } else {
            registerReceiver(smsBroadcastReceiver, intentFilter)
        }
    }

    override fun onStop() {
        super.onStop()
        unregisterReceiver(smsBroadcastReceiver)
    }
}

Method 3 — BroadcastReceiver with READ_SMS (Default SMS Apps Only)

⛔ Stop — read this first. Google Play will reject your app if it requests READ_SMS or RECEIVE_SMS and your app is not a default SMS/messaging app. This method is only included for completeness. Do not use this in a regular Android app.

If you are building a default SMS app and need direct SMS access, here's how to do it with the modern ActivityResultContracts API instead of the deprecated onRequestPermissionsResult:

class SmsDefaultAppActivity : AppCompatActivity() {

    // Modern permission request — replaces deprecated onRequestPermissionsResult
    private val requestSmsPermission = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissions ->
        val receiveSmsGranted = permissions[Manifest.permission.RECEIVE_SMS] == true
        val readSmsGranted = permissions[Manifest.permission.READ_SMS] == true

        if (receiveSmsGranted && readSmsGranted) {
            // Permissions granted — register receiver
            registerSmsReceiver()
        } else {
            // Permission denied — show explanation or fallback to manual entry
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        requestSmsPermission.launch(arrayOf(
            Manifest.permission.RECEIVE_SMS,
            Manifest.permission.READ_SMS
        ))
    }

    private fun registerSmsReceiver() {
        // Register your BroadcastReceiver here
    }
}
// SmsReceiver.kt
class SmsReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (intent?.action != "android.provider.Telephony.SMS_RECEIVED") return

        val bundle = intent.extras ?: return
        val pdus = bundle.get("pdus") as? Array<*> ?: return

        for (pdu in pdus) {
            val smsMessage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                val format = bundle.getString("format") ?: "3gpp"
                SmsMessage.createFromPdu(pdu as ByteArray, format)
            } else {
                @Suppress("DEPRECATION")
                SmsMessage.createFromPdu(pdu as ByteArray)
            }
            val sender = smsMessage.displayOriginatingAddress
            val body = smsMessage.messageBody
            // Process OTP
        }
    }
}

Best Practices

  • Always fall back to manual entry — SMS Retriever has a 5-minute timeout. Network delays, carrier filtering, or incorrect SMS format can all cause it to fail silently. Always keep the OTP input field visible and functional so users can enter it manually if auto-fill doesn't work.
  • Generate both debug and release hashes — your app hash is tied to your signing keystore. Debug and release builds have different hashes. Give both to your backend team so the Retriever API works in both environments.
  • Use Method 1 over Method 2 for better UX — SMS Retriever API fills the OTP without any user interaction. User Consent API requires a tap on a bottom sheet. When you control the SMS format, Method 1 is a smoother experience.
  • Register the receiver in onStart/onStop — not in onCreate/onDestroy. Registering in onStart ensures the receiver is active whenever the screen is visible and automatically unregistered when it's not.
  • Use RECEIVER_NOT_EXPORTED on Android 14+ — API 34 requires explicit export flags for dynamically registered receivers that don't handle system broadcasts. Always add this flag to avoid a crash on newer devices.

Frequently Asked Questions

How do I auto-read OTP SMS in Android without permissions?
Use Google's SMS Retriever API with Kotlin — no SMS permissions required. The SMS must start with <#> and end with your app's 11-character app hash to be delivered silently to your app.

How can I auto-verify OTP on Android using Kotlin?
Start SmsRetrieverClient, register a BroadcastReceiver, extract the OTP with a regex (e.g., \d{4,6}), and verify it on your server. Prefer SMS Retriever for silent auto-fill or User Consent API when you can't control SMS format.

Why is my SMS Retriever API not working for OTP auto-fill?
Common causes: incorrect 11-character app hash, different signing keystore (debug vs release), SMS longer than 140 bytes, missing leading <#>, or carrier filtering. Verify app hash generation and SMS template.

Can I use READ_SMS permission to auto-read OTP in Android?
Only if your app is the device's default SMS app — Google Play restricts READ_SMS and RECEIVE_SMS for non-default messaging apps. Use SMS Retriever or User Consent API instead.

What's the difference between SMS Retriever API and User Consent API for OTP?
SMS Retriever is silent and requires server-controlled SMS format + app hash. User Consent API shows a bottom sheet asking users to approve sharing the SMS and is suitable when you don't control the SMS content.

How do I generate the app hash for SMS Retriever in Kotlin?
Compute the 11-character app signature hash from your package name and signing key (debug and release differ). Log it in development or compute it server-side and include it in the SMS template.

What SMS format should my server send for SMS Retriever OTP auto-verification?
Use: <#> Your OTP is 123456. It expires in 10 minutes. followed by the 11-character app hash on the last line. Start with <#>, keep under 140 bytes, and include the app hash.

How do I extract OTP from SMS in Kotlin reliably?
Use a regex like \b\d{4,6}\b to capture 4–6 digit codes, trim and validate them, and always verify the OTP on the server before granting access.

What are the three methods to auto-read OTP SMS on Android?
Method 1: SMS Retriever API (no permissions, silent, recommended).
Method 2:
User Consent API (no permissions, shows consent sheet).
Method 3:
BroadcastReceiver + READ_SMS (Google Play rejects unless default SMS app).

How do I register a BroadcastReceiver for SMS Retriever in Kotlin?
Create a class extending BroadcastReceiver, override onReceive(), check for SmsRetriever.SMS_RETRIEVED_ACTION, extract the SMS from extras, and parse the OTP. Register in onStart() and unregister in onStop().

📚 Continue Learning
📝 Summary
  • SMS Retriever API — no permission, silent auto-fill, requires server-side SMS format with app hash
  • User Consent API — no permission, shows bottom sheet, no SMS format requirement
  • READ_SMS BroadcastReceiver — Play Store rejected unless you're a default SMS app
  • Always generate debug + release hashes separately — they differ by keystore
  • SMS must start with <#> and end with the 11-char hash for Retriever API
  • Register receiver in onStart/onStop — not onCreate/onDestroy
  • Add RECEIVER_NOT_EXPORTED flag on Android 14+ (API 34)
  • Always provide manual OTP entry fallback — Retriever times out after 5 minutes

Pragnesh Ghoda

A forward-thinking developer offering more than 8 years of experience building, integrating, and supporting android applications for mobile and tablet devices on the Android platform. Talks about #kotlin and #android

2 Comments

Please let us know about any concerns or query.

  1. Works fine while the app is running. Throws error when the app is closed. mListener.messageReceived(messageBody); throws Null object reference. Please help me

    ReplyDelete
    Replies
    1. I have republished the Article. Now you can choose from 3 different methodsm depends on your app’s specific needs.

      Delete
Previous Post Next Post

Contact Form