> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Mobile OTP autofill for Verify

> Shape your Verify SMS template and your input fields so iOS, Android, and the web offer the one-time code for autofill: one-time-code autocomplete, the Android SMS Retriever app hash, and domain-bound suggestions.

Every modern OS offers to fill a one-time code straight from the incoming SMS: iOS shows a "From Messages" suggestion banner, Android hands the message to your app through the SMS Retriever API, and browsers fill `<input autocomplete="one-time-code">` fields. Whether any of those affordances fires is decided by two things you own: the **shape of your message template** and the **autocomplete hints on your input field**. Orbit Verify generates the code and delivers the message exactly as your template renders it; nothing is appended or reformatted on your behalf. This guide shows the body shape each platform reads, the client-side wiring for web, iOS, and Android, and how to prove on a real device that the suggestion appeared.

If you have not wired the send/check loop yet, run [Verify in 30 minutes](/guides/verify-in-30-min) first; this guide starts where it ends, with a working `/send` and `/check`.

## 1. Why autofill lifts OTP conversion

Three OS-level affordances read your SMS body:

| Affordance | Platform | What it reads from your template | What your client must set |
| - | - | - | - |
| "From Messages" suggestion banner | iOS 12+, best on iOS 17+ | The numeric code as the last digit run in the body; an `@domain` line binds the suggestion to your domain | `textContentType = .oneTimeCode` on the field |
| SMS Retriever API | Android (Play services) | The 11-character app hash on the message's last line; the message is delivered to your app only when the hash matches | `SmsRetrieverClient` listener + broadcast receiver |
| `autocomplete="one-time-code"` | Web, plus Chrome on Android via the Web OTP API | The code on any platform; on Chrome for Android the `@domain` line ties the SMS to the focused page | The autocomplete attribute on a focused, in-form input |

All three consume the same rendered components: the code digits and the single-line suffixes described next. Autofill removes the app-switch, read, and retype loop, which is where wrong-digit entries and abandoned verifications concentrate. A user who taps a suggestion submits the exact code seconds after delivery; a user who retypes it is your `VALIDATION_ERROR` and `EXPIRED_TOKEN` traffic.

## 2. Template the message body for autofill

Your SMS template lives under **Verify → Configuration → Templates** (walkthrough in [Verify configuration console](/guides/verify-configuration-console)). Whatever you write there is what the device receives verbatim, so the template has to carry the platform suffixes itself. Three rules make one body satisfy every OS:

1. **The code is the last digit run in the body.** Platform scanners pick the final numeric run that looks like a code. Any number after it (an order id, a year, a support line) becomes the suggested "code" instead.
2. **End the body with an `@your-domain` line.** iOS uses it to bind the suggestion to your domain for web-to-app handoff, and Chrome's Web OTP API uses it to offer the code to your focused web page.
3. **End the body with your Android app hash line.** The SMS Retriever delivers a message to your app only when the last line is that app's 11-character hash (computation in [section 5](#5-android-native)).

A template that satisfies all three:

```text theme={null}
{{app_name}}: your verification code is {{code}}
@verify.example.com
sR7gcWr2hBH
```

How each line is read:

* **Line 1** renders your app name and the 6-digit code. `{{code}}` is the last digit run; keep every later line free of digit runs (the hash's isolated digits never parse as a code).
* **Line 2** is the domain token. Replace `verify.example.com` with a domain you control and, for the iOS handoff, one you have registered as an associated domain ([section 4](#4-ios-native)).
* **Line 3** is the Android app hash, one per Android app package and signing key. If you ship two Android apps that both consume the code, you cannot put two hashes on one body; use the SMS User Consent API for the second app ([section 5](#5-android-native)).

Keep each suffix on its own line and keep the body short enough to stay in one SMS segment where your sender setup allows it. The unbound iOS banner and plain web autofill work without any suffix; the suffixes are what add binding and the Android Retriever path.

## 3. Web

Mark the field and keep it in a form:

```html theme={null}
<form action="/verify/complete" method="post">
  <label for="otp">Enter the 6-digit code</label>
  <input
    id="otp"
    name="otp"
    type="text"
    inputmode="numeric"
    autocomplete="one-time-code"
    pattern="[0-9]{6}"
    maxlength="6"
    autofocus
  />
  <button type="submit">Verify</button>
</form>
```

Two Safari specifics: the `<input>` must sit inside a `<form>`, and it must be focused when the message arrives (on macOS the code arrives via iPhone message forwarding; on iOS via the SMS itself). Render the form up front and focus the field when the user taps "send code"; a field swapped into the DOM after the send loses the suggestion.

On Chrome for Android you can also pull the code programmatically with the Web OTP API, which reads the `@domain` line from [section 2](#2-template-the-message-body-for-autofill):

```js theme={null}
const input = document.getElementById('otp');
if (window.OTPCredential) {
  const abort = new AbortController();
  navigator.credentials
    .get({ otp: { transport: 'sms' }, signal: abort.signal })
    .then((cred) => {
      if (cred) input.value = cred.code;
    })
    .catch(() => abort.abort()); // unsupported or timed out: manual entry still works
}
```

Guard the origin: the API resolves only when the page is served over HTTPS on the domain named in the SMS.

**Fallback when the suggestion chip ignores the hint.** Every OS treats autofill as a suggestion, never a guarantee: low-power modes, filtered message folders, and dual-SIM routing all suppress the banner. Manual entry must remain a first-class path: keep `maxlength` and `pattern` permissive of paste, and on a failed check surface `attempts_remaining` from the `422` response exactly as [Verify without our SDK](/guides/verify-no-sdk) describes. If the chip never appears on a device where it should, work the matrix in [section 7](#7-testing-matrix) before blaming the template.

## 4. iOS native

Set the content type on the field that receives the code:

```swift theme={null}
// UIKit
codeField.textContentType = .oneTimeCode
codeField.keyboardType = .numberPad

// SwiftUI
TextField("6-digit code", text: $code)
    .textContentType(.oneTimeCode)
    .keyboardType(.numberPad)
```

The "From Messages" banner appears when a message arrives while a marked field is first responder and the body contains a code-like digit run. No entitlement is needed for this unbound banner; the template's line 1 alone triggers it.

The `@domain` line adds the **domain-bound** form. For the web-to-app handoff (the user starts verification on your mobile site and finishes in your app, or you want the suggestion to show your domain so users trust it), the domain in the SMS must be one your app claims via the Associated Domains capability (`applinks:verify.example.com` in the Signing & Capabilities tab, backed by the `apple-app-site-association` file on that domain). With the entitlement and the `@domain` line both in place, the banner shows the domain and a tap can hand the code from your web page to your app; without either, you get the unbound banner only.

## 5. Android native

The **SMS Retriever API** gives your app the matching message with no SMS permission and no user prompt, in exchange for the app-hash line in the body. Start the listener when the code entry screen appears:

```kotlin theme={null}
SmsRetriever.getClient(this).startSmsRetriever()
```

Then receive the broadcast:

```kotlin theme={null}
class OtpSmsReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action != SmsRetriever.SMS_RETRIEVED_ACTION) return
        val status = intent.getParcelableExtra<Status>(SmsRetriever.EXTRA_STATUS) ?: return
        when (status.statusCode) {
            CommonStatusCodes.SUCCESS -> {
                val body = intent.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE) ?: return
                val code = Regex("\\d{6}").find(body)?.value
                code?.let { sendToYourBackend(it) } // backend calls POST /api/v1/verify/check
            }
            CommonStatusCodes.TIMEOUT -> showManualEntry() // no matching SMS within 5 minutes
        }
    }
}
```

Register the receiver (context-registered with an `IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)`) while the verification screen is visible and unregister it on destroy. The Retriever watches for 5 minutes per `startSmsRetriever()` call and only delivers messages whose last line is your app's hash.

**The app hash** is 11 characters, derived from your package name and signing certificate: SHA-512 of `"<package name> <base64 signing certificate>"`, base64-encoded, first 11 characters. The same computation over your release key:

```bash theme={null}
CERT_B64=$(keytool -exportcert -alias release -keystore release.keystore | base64 | tr -d '\n')
printf 'com.example.app %s' "$CERT_B64" | openssl dgst -sha512 -binary | base64 | cut -c1-11
```

If Google Play App Signing re-signs your app, compute the hash from the upload and the Play signing certificate both, and test with the Play-distributed build, not only your local one.

**SMS User Consent API** is the alternative when you cannot embed the hash: `SmsRetriever.getClient(this).startSmsUserConsent()` delivers the next OTP-shaped message regardless of its suffix, and your receiver launches the intent in `EXTRA_CONSENT_INTENT`, which shows a system sheet with the full message text the user approves once. It works with any template, at the cost of that one-tap consent prompt. Pick the Retriever when you control the template (the usual Orbit case); pick User Consent for a second Android app that shares one sender, since one body carries one hash.

## 6. Wire it to Orbit Verify

The whole loop with autofill shapes:

1. **Edit the template** under **Verify → Configuration → Templates** ([configuration console guide](/guides/verify-configuration-console)) to the body from [section 2](#2-template-the-message-body-for-autofill), with your domain and hash.
2. **Send through the normal SMS factor**: `POST /api/v1/verify/send` with `channel: "sms"`, exactly as in [Verify in 30 minutes](/guides/verify-in-30-min).

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/verify/send" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "channel": "sms",
    "profile_id": "vprof_7a1c0e2b…"
  }'
```

3. **Pin a test send and read the rendered body.** Send once with your sandbox key (`dv_test_sk_…`, [magic numbers](/sandbox/magic-numbers)) and open that send's delivery log row: the rendered body should show the real code digits plus both suffix lines, untouched. This catches template-engine mistakes (a swallowed `@` line, a trailing space merging the hash into the previous line) before you spend a live send.
4. **Send one live verification to a test device** and confirm the device receives exactly that body. The rendered body a phone receives is your template, verbatim, with `{{code}}` replaced:

```text theme={null}
Acme: your verification code is 482910
@verify.example.com
sR7gcWr2hBH
```

5. **Check as usual.** The client (web field, iOS banner tap, or Android receiver) extracts `482910`; your backend validates it with `POST /api/v1/verify/check`. Autofill changes nothing about the check call.

Orbit never rewrites your template for autofill. If a platform stops offering the code after a template edit, diff the rendered body in the delivery log against the three rules in [section 2](#2-template-the-message-body-for-autofill).

## 7. Testing matrix

| Surface | How to exercise it | Pass looks like |
| - | - | - |
| Template rendering | Sandbox key (`dv_test_sk_…`) send to a [magic number](/sandbox/magic-numbers); open the delivery log row | Rendered body shows code plus `@domain` and hash lines, each on its own line; costs \$0 |
| Web, Chrome on Android | Live send while the field is focused on HTTPS | Suggestion chip offers the code; `input.value` fills without typing |
| Web, Safari | Live send with the in-form field focused | Autofill suggestion appears; on macOS via iPhone forwarding |
| iOS 17+ native | Live send with a `.oneTimeCode` field first responder | "From Messages" banner above the keyboard, showing your domain when the associated-domain setup is in place |
| Android 13+ native | Live send with the receiver registered | `SMS_RETRIEVED_ACTION` broadcast delivers the body, no permission prompt, no consent sheet |

To confirm a fill came from the suggestion rather than manual typing, timestamp the client event where the field goes from empty to complete in a single change (autofill and paste fill in one step; typing does not) and compare it with the delivered timestamp on that send's delivery log row: an autofill fill lands one or two seconds after delivery, manual entry many seconds later. Sandbox sends are simulated and never reach a real device, so banner behavior is only testable on live sends; use the sandbox strictly for the rendered-body check above.

## Next steps

* [Verify in 30 minutes](/guides/verify-in-30-min) — the send/check loop this guide builds on.
* [Verify factor suite](/guides/verify-factor-suite) — TOTP, push, and passkey factors alongside SMS.
* [SMS channel](/channels/sms) — sender IDs, segments, and delivery semantics for the SMS factor.
* [Verification lifecycle](/concepts/verification-lifecycle) — statuses a verification moves through from send to approval.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.