You've been there — you're using a banking app, an OTP arrives, and instead of making you switch to your messages app and type it manually, the app just fills it in automatically. That one small feature feels magical to users. And it's one of those things that's slightly more involved to implement than it looks.
There are three ways to auto-read OTPs in Android — each with different trade-offs around permissions, Play Store policies, and how much control your server has over the SMS format. This guide covers all three, flags the one that will get your app rejected from the Play Store, and shows you exactly what the SMS needs to look like for each method to work.
Three Methods — Which One Should You Use?
| 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 |
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.
Method 1 — SMS Retriever API (Recommended)
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))
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
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)
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 without permissions?
Use the SMS Retriever API — no permissions needed. The SMS must start with <#> and end with your app's 11-character hash. Google Play Services intercepts it and delivers it directly to your app silently.
Why is my SMS Retriever API not working?
Most likely an incorrect app hash. The hash must match the keystore used to sign the installed build. Debug and release builds have different hashes — generate both and give both to your backend. Also verify the SMS starts with <#>, ends with the hash on the last line, and is under 140 bytes.
Can I use READ_SMS permission to auto-read OTP?
Technically yes, but Google Play will reject your app unless it's a default SMS app. Since 2019, READ_SMS and RECEIVE_SMS are restricted to default messaging apps only. Use SMS Retriever API or User Consent API for all other apps.
What is the difference between SMS Retriever and User Consent API?
SMS Retriever works silently with no user interaction — requires server-side SMS format control. User Consent shows a bottom sheet asking the user to confirm. Use Retriever when you control the SMS format; use User Consent when you don't.
- 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_EXPORTEDflag on Android 14+ (API 34) - Always provide manual OTP entry fallback — Retriever times out after 5 minutes
Works fine while the app is running. Throws error when the app is closed. mListener.messageReceived(messageBody); throws Null object reference. Please help me
ReplyDeleteI have republished the Article. Now you can choose from 3 different methodsm depends on your app’s specific needs.
Delete