SMS Retriever API in Android

A

Anand Gaur

Guest
1*LlLbx8FaC4BahUl8JTNchA.png


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​

  1. No SMS permission needed (unlike the old READ_SMS).
  2. Works automaticallyβ€Šβ€”β€Šuser doesn’t need to type OTP.
  3. More secureβ€Šβ€”β€Šonly SMS containing your app’s unique hash code can be read.
  4. Improves user experienceβ€Šβ€”β€ŠOTP auto-filled in seconds.

How SMS Retriever API Works​


Think of it like a postal service:

  1. Your app registers: β€œHey Google, I’m expecting a package (SMS) with my special code”
  2. Google gives you a receipt: β€œHere’s your tracking number (hash code)”
  3. You tell your server: β€œPut this tracking number on all packages you send me”
  4. Package arrives: When SMS with your tracking number arrives, Google notifies your app
  5. You get your package: Your app can now read that specific SMS
  6. Google Play services delivers that SMS content to your app.
  7. 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:play-services-auth:_'
implementation 'com.google.android.gms:play-services-auth-api-phone:_'

// 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

1*QDXX6GNzHpVl463_Om41-g.png

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.

πŸ”— Book a session here

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:
1️⃣ Android Core Concepts
2️⃣ Kotlin
3️⃣ Android Architecture
4️⃣ Jetpack Compose
5️⃣ Unit Testing
6️⃣ Android Security
7️⃣ Real-World Scenario-Based Q&As
8️⃣ CI/CD, Git, and Detekt in Android

Grab your copy now:
πŸ‘‰ https://topmate.io/anand_gaur/1623062

Found this helpful? Don’t forgot to clap πŸ‘ and 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

stat



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...
 


Join 𝕋𝕄𝕋 on Telegram
Channel PREVIEW:
Back
Top