Skip to main content
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 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: 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). 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).
A template that satisfies all three:
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).
  • 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).
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:
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:
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 describes. If the chip never appears on a device where it should, work the matrix in section 7 before blaming the template.

4. iOS native

Set the content type on the field that receives the code:
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:
Then receive the broadcast:
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:
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) to the body from section 2, 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.
  1. Pin a test send and read the rendered body. Send once with your sandbox key (dv_test_sk_…, 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.
  2. 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:
  1. 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.

7. Testing matrix

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