A
Anand Gaur
Guest

In many Android apps, especially banking or payment apps, users need to verify their mobile numbers using an OTP (One-Time Password). Usually, the OTP comes via SMS, and users have to copy or type it manually in the app.
But this process is a bit irritating, right? Thatβs where SMS Retriever API comes in.
With SMS Retriever API, your app can automatically read the OTP SMS without asking for SMS permission, making the verification process smooth and secure.
What is the SMS Retriever API?
The SMS Retriever API is a tool provided by Android that allows your app to automatically receive verification codes sent via SMS, securely and without asking for SMS read permissions. This is commonly used for features like sign-up OTPs (one-time passwords) and two-factor authentication. By using SMS Retriever API, you make the SMS verification process seamless and safer for the user.
Why Use SMS Retriever API?
- No SMS read permission needed: It does not require the user to grant your app access to all their SMS messages.
- Enhanced user experience: Users donβt have to enter OTPs manually.
- Security: Only messages containing a unique app hash code are βretrieved.β
Key Benefits of SMS Retriever API
- No SMS permission needed (unlike the old READ_SMS).
- Works automaticallyβββuser doesnβt need to type OTP.
- More secureβββonly SMS containing your appβs unique hash code can be read.
- Improves user experienceβββOTP auto-filled in seconds.
How SMS Retriever API Works
Think of it like a postal service:
- Your app registers: βHey Google, Iβm expecting a package (SMS) with my special codeβ
- Google gives you a receipt: βHereβs your tracking number (hash code)β
- You tell your server: βPut this tracking number on all packages you send meβ
- Package arrives: When SMS with your tracking number arrives, Google notifies your app
- You get your package: Your app can now read that specific SMS
- Google Play services delivers that SMS content to your app.
- You extract the OTP and auto-fill it.
Step-by-Step Implementation
Step 1: Project Setup
Add Dependencies
In your app/build.gradle file:
dependencies {
// Jetpack Compose BOM
implementation platform('androidx.compose:compose-bom:{latest}')
// Compose essentials
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.activity:activity-compose:_'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:_'
// Google Play Services for SMS Retriever
implementation 'com.google.android.gms

implementation 'com.google.android.gms

// Coroutines for async operations
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:_'
}
Add Permissions
In your AndroidManifest.xml:
<uses-permission android:name="com.google.android.gms.permission.RECEIVE_SMS" />
Important: This is NOT the dangerous android.permission.RECEIVE_SMS!
Step 2: Get Your Appβs Hash Code
Every app needs a unique fingerprint. Letβs create a helper to get it:
import android.content.Context
import android.content.pm.PackageManager
import android.util.Base64
import android.util.Log
import java.security.MessageDigest
class AppSignatureHelper(private val context: Context) {
companion object {
private const val HASH_TYPE = "SHA-256"
private const val NUM_HASHED_BYTES = 9
private const val NUM_BASE64_CHAR = 11
private const val TAG = "AppSignatureHelper"
}
fun getAppSignatures(): List<String> {
val appCodes = mutableListOf<String>()
try {
val packageName = context.packageName
val packageManager = context.packageManager
val signatures = packageManager.getPackageInfo(
packageName,
PackageManager.GET_SIGNATURES
).signatures
signatures.forEach { signature ->
val hash = hash(packageName, signature.toCharsString())
if (hash != null) {
appCodes.add(hash)
Log.d(TAG, "App Hash: $hash")
}
}
} catch (e: Exception) {
Log.e(TAG, "Error getting app signature", e)
}
return appCodes
}
private fun hash(packageName: String, signature: String): String? {
val appInfo = "$packageName $signature"
return try {
val messageDigest = MessageDigest.getInstance(HASH_TYPE)
messageDigest.update(appInfo.toByteArray())
var hashSignature = messageDigest.digest()
hashSignature = hashSignature.copyOfRange(0, NUM_HASHED_BYTES)
val base64Hash = Base64.encodeToString(
hashSignature,
Base64.NO_PADDING or Base64.NO_WRAP
)
base64Hash.substring(0, NUM_BASE64_CHAR)
} catch (e: Exception) {
Log.e(TAG, "Hash generation failed", e)
null
}
}
}
Step 3: Create SMS Broadcast Receiver
This is the ears of your app that listens for SMS:
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import com.google.android.gms.auth.api.phone.SmsRetriever
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
class SMSBroadcastReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "SMSBroadcastReceiver"
}
private var otpListener: ((String?) -> Unit)? = null
fun setOTPListener(listener: (String?) -> Unit) {
this.otpListener = listener
}
override fun onReceive(context: Context, intent: Intent) {
if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
val extras = intent.extras
val status = extras?.get(SmsRetriever.EXTRA_STATUS) as Status
when (status.statusCode) {
CommonStatusCodes.SUCCESS -> {
val message = extras.getString(SmsRetriever.EXTRA_SMS_MESSAGE)
Log.d(TAG, "SMS received: $message")
val otp = extractOTPFromMessage(message)
otpListener?.invoke(otp)
}
CommonStatusCodes.TIMEOUT -> {
Log.d(TAG, "SMS retrieval timeout")
otpListener?.invoke(null)
}
}
}
}
private fun extractOTPFromMessage(message: String?): String? {
return message?.let {
// Extract 4-6 digit numbers
val otpPattern = "\\d{4,6}".toRegex()
otpPattern.find(it)?.value
}
}
}
Step 4: Create ViewModel for Business Logic
Letβs create a ViewModel to handle all the SMS logic:
import android.app.Application
import android.content.IntentFilter
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.gms.auth.api.phone.SmsRetriever
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
data class OTPState(
val otpValue: String = "",
val isLoading: Boolean = false,
val isListening: Boolean = false,
val message: String = "",
val isSuccess: Boolean = false,
val error: String? = null
)
class OTPViewModel(application: Application) : AndroidViewModel(application) {
private val _state = MutableStateFlow(OTPState())
val state: StateFlow<OTPState> = _state.asStateFlow()
private val smsReceiver = SMSBroadcastReceiver()
private val context = getApplication<Application>()
init {
setupSMSReceiver()
// Get app hash for development
getAppHash()
}
private fun setupSMSReceiver() {
smsReceiver.setOTPListener { otp ->
viewModelScope.launch {
if (otp != null) {
_state.value = _state.value.copy(
otpValue = otp,
isListening = false,
message = "OTP received automatically!",
isSuccess = true
)
} else {
_state.value = _state.value.copy(
isListening = false,
message = "SMS timeout. Please try again.",
error = "Timeout"
)
}
}
}
}
private fun getAppHash() {
val helper = AppSignatureHelper(context)
val hashes = helper.getAppSignatures()
if (hashes.isNotEmpty()) {
android.util.Log.d("OTP_HASH", "Your app hash: ${hashes.first()}")
}
}
fun startSMSRetriever() {
viewModelScope.launch {
_state.value = _state.value.copy(
isLoading = true,
message = "Starting SMS listener...",
error = null
)
try {
val client = SmsRetriever.getClient(context)
val task = client.startSmsRetriever()
task.addOnSuccessListener {
_state.value = _state.value.copy(
isLoading = false,
isListening = true,
message = "Waiting for SMS... Check your messages!"
)
// Register receiver
context.registerReceiver(
smsReceiver,
IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)
)
}
task.addOnFailureListener { exception ->
_state.value = _state.value.copy(
isLoading = false,
isListening = false,
error = "Failed to start SMS listener: ${exception.message}",
message = "SMS listener failed"
)
}
} catch (e: Exception) {
_state.value = _state.value.copy(
isLoading = false,
error = "Error: ${e.message}",
message = "Something went wrong"
)
}
}
}
fun updateOTP(newOTP: String) {
// Allow only digits and limit to 6 characters
val filteredOTP = newOTP.filter { it.isDigit() }.take(6)
_state.value = _state.value.copy(
otpValue = filteredOTP,
error = null
)
}
fun verifyOTP() {
val otp = _state.value.otpValue
if (otp.length < 4) {
_state.value = _state.value.copy(
error = "Please enter a valid OTP"
)
return
}
viewModelScope.launch {
_state.value = _state.value.copy(isLoading = true)
// Simulate API call
kotlinx.coroutines.delay(2000)
// TODO: Replace with actual verification logic
val isValid = otp == "123456" // Demo logic
_state.value = _state.value.copy(
isLoading = false,
isSuccess = isValid,
message = if (isValid) "OTP verified successfully!" else "Invalid OTP",
error = if (!isValid) "Verification failed" else null
)
}
}
fun requestNewOTP() {
_state.value = _state.value.copy(
otpValue = "",
isSuccess = false,
error = null,
message = "Requesting new OTP..."
)
// TODO: Call your API to send new OTP
startSMSRetriever()
}
fun clearMessages() {
_state.value = _state.value.copy(
message = "",
error = null
)
}
override fun onCleared() {
super.onCleared()
try {
context.unregisterReceiver(smsReceiver)
} catch (e: Exception) {
// Receiver might not be registered
}
}
}
Step 5: Create Beautiful Compose UI
Now letβs build a stunning UI with Jetpack Compose:
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Sms
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OTPVerificationScreen(
viewModel: OTPViewModel = viewModel()
) {
val state by viewModel.state.collectAsState()
// Clear messages after some time
LaunchedEffect(state.message) {
if (state.message.isNotEmpty()) {
kotlinx.coroutines.delay(3000)
viewModel.clearMessages()
}
}
Column(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0xFF667eea),
Color(0xFF764ba2)
)
)
)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(60.dp))
// Header
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color.White.copy(alpha = 0.95f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Default.Sms,
contentDescription = "SMS",
modifier = Modifier.size(48.dp),
tint = Color(0xFF667eea)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "OTP Verification",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF2D3748)
)
Text(
text = "We'll automatically detect your OTP",
fontSize = 14.sp,
color = Color(0xFF718096),
textAlign = TextAlign.Center
)
}
}
Spacer(modifier = Modifier.height(32.dp))
// OTP Input Section
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color.White.copy(alpha = 0.95f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// OTP Input Field
OutlinedTextField(
value = state.otpValue,
onValueChange = viewModel::updateOTP,
label = { Text("Enter OTP") },
placeholder = { Text("6-digit code") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color(0xFF667eea),
focusedLabelColor = Color(0xFF667eea)
),
trailingIcon = {
AnimatedVisibility(
visible = state.isSuccess,
enter = scaleIn() + fadeIn()
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Verified",
tint = Color(0xFF48BB78)
)
}
}
)
Spacer(modifier = Modifier.height(24.dp))
// Action Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Request OTP Button
Button(
onClick = viewModel::requestNewOTP,
modifier = Modifier.weight(1f),
enabled = !state.isLoading && !state.isListening,
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF667eea)
)
) {
if (state.isLoading && state.isListening) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
color = Color.White,
strokeWidth = 2.dp
)
} else {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
}
Spacer(modifier = Modifier.width(8.dp))
Text("Request OTP")
}
// Verify Button
Button(
onClick = viewModel::verifyOTP,
modifier = Modifier.weight(1f),
enabled = !state.isLoading && state.otpValue.length >= 4,
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF48BB78)
)
) {
if (state.isLoading && !state.isListening) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
color = Color.White,
strokeWidth = 2.dp
)
} else {
Text("Verify")
}
}
}
}
}
Spacer(modifier = Modifier.height(24.dp))
// Status Messages
AnimatedVisibility(
visible = state.message.isNotEmpty() || state.error != null,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = if (state.error != null)
Color(0xFFE53E3E).copy(alpha = 0.1f)
else
Color(0xFF48BB78).copy(alpha = 0.1f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Text(
text = state.error ?: state.message,
modifier = Modifier.padding(16.dp),
color = if (state.error != null) Color(0xFFE53E3E) else Color(0xFF48BB78),
textAlign = TextAlign.Center,
fontWeight = FontWeight.Medium
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// Listening Status
AnimatedVisibility(
visible = state.isListening,
enter = scaleIn() + fadeIn()
) {
Card(
modifier = Modifier
.clip(RoundedCornerShape(50.dp)),
colors = CardDefaults.cardColors(
containerColor = Color(0xFF667eea).copy(alpha = 0.1f)
)
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
color = Color(0xFF667eea),
strokeWidth = 2.dp
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Listening for SMS...",
color = Color(0xFF667eea),
fontSize = 12.sp,
fontWeight = FontWeight.Medium
)
}
}
}
Spacer(modifier = Modifier.weight(1f))
// Help Text
Text(
text = "Don't receive SMS? Check your message app or try requesting again.",
fontSize = 12.sp,
color = Color.White.copy(alpha = 0.8f),
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 32.dp)
)
}
}
Step 6: Create Main Activity
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.yourpackage.ui.theme.YourAppTheme
class MainActivity : ComponentActivity() {
private val otpViewModel: OTPViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
YourAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
OTPVerificationScreen(viewModel = otpViewModel)
}
}
}
}
}
Step 7: Backend SMS Format
Your backend needs to send SMS in this exact format:
Your OTP is: 123456
FA+9qCX9VSu
Critical Points:
- Hash must be at the end
- Newline before the hash
- Message under 140 characters
- Hash is case-sensitive
Example Backend Code (Node.js):
const sendOTP = async (phoneNumber, otp, appHash) => {
const message = `Your OTP is: ${otp}\n\n${appHash}`;
// Send SMS using your preferred service (Twilio, AWS SNS, etc.)
await smsService.send({
to: phoneNumber,
message: message
});
};
Step 8: Testing Your Implementation
1. Development Testing:
// In your MainActivity onCreate, add this for testing:
val helper = AppSignatureHelper(this)
val hash = helper.getAppSignatures().firstOrNull()
Log.d("SMS_HASH", "Your app hash: $hash")
2. Manual Testing:
Send yourself an SMS with this format:
Your verification code is: 123456
FA+9qCX9VSu
3. Production Testing:
Integrate with your backend and test the complete flow.
Common Issues and Solutions
Issue 1: SMS Not Detected
Problem: App doesnβt automatically fill OTP Solutions:
- Verify app hash is correct
- Check SMS format (newline before hash)
- Ensure SMS Retriever started before SMS arrives
- Update Google Play Services
Issue 2: Hash Mismatch
Problem: Different hash for debug/release Solutions:
- Generate hash for both build types
- Use debug hash during development
- Use release hash in production
Issue 3: Compose State Issues
Problem: UI not updating properly Solutions:
- Use StateFlow properly
- Collect state in Compose
- Handle lifecycle correctly
Issue 4: Memory Leaks
Problem: BroadcastReceiver not unregistered Solutions:
- Unregister in ViewModel onCleared()
- Handle exceptions properly
- Use try-catch blocks
Points to Note
- Timeout: The retriever waits up to 5 minutes for the SMS. After that, you have to restart the retriever if needed.
- No read permissions: If you need to read arbitrary SMS, SMS Retriever API is not the tool.
- Play Services: Works only on devices with Google Play Services.
- SMS Format: The message must have the app hash at the end, or it wonβt work.
- OTP length can be adjusted in Regex (here we used 6 digits).
Full Source Code
GitHub - anandgaur22/SMSRetrieverAPI: The SMS Retriever API is a tool provided by Android that allows your app to automatically receive verification codes sent via SMS, securely and without asking for SMS read permissions

Summary
Using SMS Retriever API, you can completely remove the need for SMS permission and improve your appβs OTP verification flow.
Itβs safe, user-friendly, and a must-have for modern apps that rely on OTP login/verification.
Thank you for reading.


Need 1:1 Career Guidance or Mentorship?
If youβre looking for personalized guidance, interview preparation help, or just want to talk about your career path in mobile developmentβββyou can book a 1:1 session with me on Topmate.

Iβve helped many developers grow in their careers, switch jobs, and gain clarity with focused mentorship. Looking forward to helping you too!
Want to Crack Android Interviews Like a Pro?
Donβt miss my best-selling Android Developer Interview Handbookβββbuilt from 8+ years of real-world experience and 1000+ interviews.
Category-wise Questions:








Grab your copy now:

Found this helpful? Donβt forgot to clapand follow me for more such useful articles about Android development and Kotlin or buy us a coffee here
![]()
If you need any help related to Mobile app development. Iβm always happy to help you.
Follow me on:
LinkedIn, Github, Instagram , YouTube & WhatsApp
SMS Retriever API in Android was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.
Continue reading...