WebOTP now reaches into cross-origin iframes
One-time passwords sent over SMS are a common second factor for authentication and payment verification, but the copy-paste dance between browser and messaging app is clumsy. The WebOTP API removes that friction by letting the browser read a specially formatted SMS and offer the code to the page with a single tap. The message is bound to the requesting origin, which also keeps phishing sites from intercepting the code.
A notable gap remained: forms hosted in cross-origin iframes, a setup frequently used for 3D Secure payment confirmation. Starting in Chrome 91, WebOTP can deliver codes to nested origins. The SMS format is now extended to include both the top-level and iframe origins.
Using the API inside an iframe
The core call is unchanged:
… const otp = await navigator.credentials.get({ otp: { transport:['sms'] } }); …
The SMS must follow the origin-bound one-time code format:
Your OTP is: 123456. @web-otp.glitch.me #12345
When the message arrives, the browser shows an info bar asking the user to verify their phone number. Clicking Verify resolves the navigator.credentials.get() promise and delivers the OTP to the page.
Two things are required to make this work from a cross-origin iframe:
- Both the top-frame origin and the iframe origin must be annotated in the SMS text.
- The embedder must grant the iframe access via permissions policy.
Annotate both origins in the SMS
When the API is called from within an iframe, the SMS must include the top-frame origin preceded by @, followed by the OTP preceded by #, then the iframe origin preceded by @:
@shop.example #123456 @bank.exmple
Configure Permissions Policy
The embedding page must explicitly grant the otp-credentials feature to the iframe. That can be done either with an HTTP header:
Permissions-Policy: otp-credentials=(self "https://bank.example")
or via the iframe allow attribute:
<iframe src="https://bank.example/…" allow="otp-credentials"></iframe>
A live demo is available at https://web-otp-iframe-demo.stackblitz.io.
Nesting limitations in Chrome
Chrome currently supports WebOTP calls only from cross-origin iframes with no more than one unique origin in the ancestor chain. In the following scenarios, using WebOTP in b.com is supported:
a.com→b.coma.com→b.com→b.coma.com→a.com→b.coma.com→b.com→c.com
In the last case, however, a call from c.com is not supported. The pattern a.com → b.com → a.com (with the API called in the innermost frame) is also unsupported, due to lack of demand and UX complexity.
Cross-browser behavior
Safari does not implement WebOTP, but it shares the same SMS message format for its input[autocomplete="one-time-code"] support: when an origin-bound SMS arrives with a matching origin, the keyboard offers to fill the OTP into the field. As of April 2021, Safari supported iframes via a unique format using %. The spec discussion ultimately settled on @, and the hope is that Safari's implementation will converge with the rest of the ecosystem.



