# How Automatic OTP Detection Works on Android

> Follow an OTP from an incoming SMS to an automatically filled verification screen, and learn how Android does it securely without SMS permissions.

- Source: https://aman.is-a.dev/blog/how-automatic-otp-detection-works-on-android
- Author: Aman
- Published: 2026-08-17

![An incoming SMS code moving automatically into six OTP fields on an Android verification screen](https://nasejsbkkaonqcfkxljf.supabase.co/storage/v1/object/public/media/posts/how-automatic-otp-detection-works-on-android/cover-1786942417205.webp)

An OTP arrives. Before you open the message, six digits appear in the verification screen.

It feels as if the app read your inbox. It did not.

On Android, a common way to build this experience is the **SMS Retriever API**. Your app asks Google Play services to wait for one specially formatted message. Google Play services checks whether that message belongs to your app, then gives the message text to the app. Your code extracts the OTP, fills the input, and sends the code to the server for verification.

Let’s follow one code, `482731`, through that entire journey.

## The short version

![The complete automatic OTP journey from the app listening to the server verifying the code](https://nasejsbkkaonqcfkxljf.supabase.co/storage/v1/object/public/media/posts/1786989120722-automatic-otp-complete-flow-diagram-v1.webp)

The full flow is:

1. The app starts listening for a matching SMS.
2. The app asks its server to send an OTP.
3. The server sends an ordinary SMS containing the OTP and the app's hash.
4. Google Play services matches the hash and delivers the message to the app.
5. The app extracts the OTP and puts it into the verification field.
6. The app sends the OTP back to the server.
7. The server decides whether the code is valid.

The important boundary is this:

> Android does not reach into your input and type the OTP. The SMS Retriever API delivers the message. Your app still extracts the code, updates the UI, and requests verification.

## First, the app starts listening

Imagine the user has entered a phone number and tapped **Continue**. Before requesting the SMS, the app starts the SMS Retriever API:

```kotlin
val client = SmsRetriever.getClient(context)

client.startSmsRetriever()
    .addOnSuccessListener {
        requestOtpFromServer(phoneNumber)
    }
```

This is simplified Kotlin, but the order is important: **start listening first, then request the OTP**. If the SMS arrives before the retriever is active, this attempt may miss it.

Once started, the retriever waits for one matching message for up to five minutes. It is not a permanent inbox listener.

Starting the retriever also does not send an SMS. It only tells Google Play services:

> “For the next few minutes, if a verification message meant for this app arrives, let me know.”

The app still needs to ask its own backend to create and send the code.

## The server creates the OTP

The app sends the phone number to a verification endpoint over HTTPS. The server then:

1. Generates an unpredictable one-time code.
2. Connects that code to the pending verification request.
3. Gives it a short expiry time.
4. Sends it through an SMS provider.

The server must not return the OTP in the response to the app. If it did, the app could “verify” the number without proving that the phone received the SMS.

The SMS is the proof-of-possession step: the user has access to the phone number that received the code.

## The SMS contains an app hash

An SMS Retriever message contains two values that matter to this flow: the one-time code and an **app hash**.

![An SMS Retriever message containing a one-time code and an 11-character app hash](https://nasejsbkkaonqcfkxljf.supabase.co/storage/v1/object/public/media/posts/1786989244816-sms-retriever-message-anatomy-diagram-v1.webp)


A message can look like this:

```text
<#> Your code is 482731
FA+9qCX9VSu
```

`482731` is the one-time code. `FA+9qCX9VSu` represents the 11-character app hash.

The complete message must stay within 140 bytes. The `<#>` prefix is commonly used in verification-message formatting; the value that routes the message to the correct app is the hash.

The hash is derived from two pieces of app identity:

- the Android package name
- the certificate used to sign the app

For example, imagine the production app uses:

```text
Package name: com.aman.otpapp
Signing certificate: Play Store production certificate
```

The hash calculation conceptually looks like this:

```text
package name + signing certificate
        ↓
      SHA-256
        ↓
 Base64 encoding
        ↓
 first 11 characters
        ↓
   FA+9qCX9VSu
```

That final value is added to the SMS:

```text
<#> Your code is 482731
FA+9qCX9VSu
```

This is why debug, locally signed, and Play Store builds can have different hashes. A message containing the debug hash may work during local development and fail in production, even when the Kotlin code is identical.

The app hash is not a password and it does not make the OTP secure. Its job is routing: it helps Google Play services decide which installed app should receive the verification message.

## Google Play services matches the message

When the SMS reaches the phone, it is still delivered as a normal SMS. The SMS Retriever flow does not require your app to request `READ_SMS` or `RECEIVE_SMS`.

Instead, Google Play services checks the incoming message while the retriever is active. If the message contains a hash that matches the installed app, Play services sends an explicit broadcast to that app containing the message text.

This is the privacy benefit of the API: your app does not get general access to the user's inbox. It receives the one matching verification message through a narrow, time-limited flow.

The app listens for the `SMS_RETRIEVED_ACTION` broadcast. A simplified receiver looks like this:

```kotlin
when (status.statusCode) {
    CommonStatusCodes.SUCCESS -> {
        val message = intent.getStringExtra(
            SmsRetriever.EXTRA_SMS_MESSAGE
        )

        onVerificationMessage(message.orEmpty())
    }

    CommonStatusCodes.TIMEOUT -> {
        showManualEntryFallback()
    }
}
```

Production code also needs to register the receiver correctly, accept broadcasts protected by the SMS Retriever send permission, and clean up any dynamically registered receiver.

## The app extracts the code

At this point, the app receives the **whole message**, not a ready-made OTP value. Your code still has to find the code inside that text.

If your backend always sends a six-digit numeric OTP, the extraction can be as small as:

```kotlin
val otp = Regex("""\b\d{6}\b""")
    .find(message)
    ?.value
```

The parser should follow the exact format your server generates. A loose rule such as “take the first number” can break when the message contains a support number, an amount, or another unrelated number.

Once the code is found, the app updates the same state that manual typing would update.

In React Native, the final handoff might conceptually look like this:

```tsx
useEffect(() => {
  const subscription = otpRetriever.onCodeDetected((code) => {
    setOtp(code);
    verifyOtp(code);
  });

  return () => subscription.remove();
}, []);
```

The native Android layer talks to the SMS Retriever API. It then sends the detected code to JavaScript. React updates the input because `setOtp(code)` changes the component's state.

That is the moment the user sees the boxes fill. It is an ordinary UI update triggered by a safely delivered message.

## The server performs the real verification

Filling the input is not the same as verifying the phone number.

The app sends `482731` to the server. Only the server can decide whether the code:

- belongs to this phone number and verification attempt
- has not expired
- has not already been used
- is still within the allowed number of attempts

If those checks pass, the server marks the verification as complete and invalidates the OTP so it cannot be reused.

> [!IMPORTANT]
> Never treat a locally matched OTP as proof of verification. The app is an untrusted client. Generate and verify the code on the server.

## Who is responsible for what?

![The responsibilities of Google Play services, the Android app, and the verification server](https://nasejsbkkaonqcfkxljf.supabase.co/storage/v1/object/public/media/posts/1786989368152-otp-responsibility-boundaries-diagram-v1.webp)


This split is the easiest way to reason about the system:

| Part                 | Responsibility                                                                 |
| -------------------- | ------------------------------------------------------------------------------ |
| Google Play services | Waits for a matching SMS and delivers its text to the app                      |
| Your Android app     | Starts the retriever, receives the message, extracts the OTP, and fills the UI |
| Your server          | Creates, sends, expires, rate-limits, and verifies the OTP                     |

If auto-detection fails, use the same boundaries to debug it.

- No SMS arrived: inspect the server and SMS provider.
- SMS arrived but the app received nothing: inspect the retriever timing, app hash, signing certificate, message size, and Google Play services availability.
- The app received the message but the field stayed empty: inspect OTP parsing and UI state.
- The field filled but verification failed: inspect the server-side OTP record, expiry, and attempt.

## What happens when the flow cannot work?

Auto-detection is an enhancement, not the only way through verification.

The retriever can time out. The SMS can be delayed. The user might receive the message on another phone. The installed device might not include Google Play services. The production SMS might contain the wrong app hash.

A good verification screen should therefore:

- keep the input editable
- allow the user to paste or type the OTP
- show a clear resend timer
- explain when the code has expired
- avoid blocking the user while waiting for auto-detection

If you do not control the SMS format and cannot include an app hash, Android also provides the **SMS User Consent API**. It asks the user for permission to share one matching SMS with the app. That adds a confirmation step, but still avoids broad inbox permissions.

The practical choice is:

- Use **SMS Retriever** when you control the message and can include the app hash.
- Use **SMS User Consent** when you cannot control the complete message format.
- Always keep manual entry as a fallback.

## The 30-second mental model

When an OTP appears automatically on Android, remember this sequence:

1. The app asks Google Play services to listen temporarily.
2. The server sends an SMS containing an OTP and the app's 11-character hash.
3. Play services matches the hash and gives the message to the correct app.
4. The app extracts the OTP and updates the verification field.
5. The server verifies and invalidates the code.

The shortest accurate summary is:

> Google Play services finds the message. Your app fills the field. Your server decides whether the OTP is valid.

Once those responsibilities are clear, automatic OTP detection stops looking like inbox access or hidden platform magic. It is a small handoff between the server, Google Play services, and the app. A manual path is ready when any part of that handoff fails.

## Further reading

- [Automatic SMS verification with the SMS Retriever API](https://developers.google.com/identity/sms-retriever/overview)
- [Request SMS verification in an Android app](https://developer.android.com/identity/sms-retriever)
- [Perform SMS verification on a server](https://developers.google.com/identity/sms-retriever/verify)
- [One-tap verification with the SMS User Consent API](https://developers.google.com/identity/sms-retriever/user-consent/overview)
