How to Enable Email Identification for OTP Login
OTP login — signing in with a one-time code sent by SMS instead of a password — is an existing Commerce feature. Out of the box the customer is identified by their phone number: they type the number the account was registered with, receive a code on it, and sign in.
This document provides a step-by-step guide on letting the customer be identified by their email address as well as their phone number. It is opt-in and off by default: a project that changes nothing keeps today's phone-only OTP login exactly as it is.
1. Overview
A customer who has forgotten which phone number an old account was registered with, or who has since changed it, cannot get past a phone-only prompt — but they usually still know their email address. Email identification closes that gap without weakening the check: the address only locates the account, and the code still goes to the phone on file.
What changes
Accepted identifier
phone
phone or email
Where the code is sent
The account's phone
The account's phone — unchanged
Code-send response
Empty body
{"mask_phone": "..."}
Throttling
One login budget
Separate otp-login-send and otp-login-verify budgets
OTP limits
Shared default entry
The login entry of OTP_PURPOSE_SETTINGS
Two-step send/verify shape
—
Unchanged
Points worth knowing before integrating:
The code always goes by SMS to the account's phone. An email address is only used to find the account — no code is ever sent by email in this flow. A customer identified by email therefore still needs a phone number on their account; if they have none, the attempt is rejected.
The number is never echoed back. The response carries a masked
mask_phone(first four and last two digits) so the interface can tell the customer where the code went without exposing the number.Nothing reveals whether an identifier exists. An unknown phone number, an unknown email, and an account with no phone on file all produce the same generic rejection.
The two-step shape is unchanged. The same endpoint sends the code and verifies it, and the backend infers the step from the session — a request without a code sends one, a request with a code verifies it. Existing phone-based clients keep working untouched.
Configuration surface
Accepting an email address as the identifier
OTP_LOGIN_VIEW environment variable
Environment Variables, entry 93
The authentication backends the flow needs
AUTHENTICATION_BACKENDS environment variable
Environment Variables, entry 72
Attempt budget, code lifetime, resend cooldown
OTP_PURPOSE_SETTINGS dynamic setting
Dynamic Settings, section 141
Optional reCAPTCHA on the code-send step
REGISTER_FORM_RECAPTCHA_CONF dynamic setting
The SMS gateway must be configured and operational before any of this works: see the SMS_GATEWAY and SMS_GATEWAYS entries in Dynamic Settings. Every step below assumes codes can actually be delivered.
2. Setup
2.1. Accepting an email address as the identifier
Add the following environment variable to the Commerce service in ACC:
Leaving it unset keeps the default view, which accepts phone only and returns an empty body when the code is sent. Setting it changes three things at once:
emailbecomes an accepted identifier alongsidephone;the response includes
mask_phone;the code-send step is validated against reCAPTCHA — if, and only if, reCAPTCHA is enabled through the
REGISTER_FORM_RECAPTCHA_CONFdynamic setting — and sending and verifying are throttled under separate budgets (otp-login-sendandotp-login-verify).
The full description of this variable is entry 93 of Environment Variables.
2.2. Registering the authentication backends
Each identifier is resolved by its own authentication backend, and they are not wired automatically. A project already running OTP login will have the phone one in AUTHENTICATION_BACKENDS:
Email identification needs one more alongside it:
See entry 72 of Environment Variables for the variable itself.
Do not enable the view without the backend. Missing it does not disable email sign-in — it makes every attempt fail with a generic "unable to log in" message even when the customer typed the correct code, which looks like a broken SMS gateway rather than a missing setting. The platform now refuses to start with that combination, so the misconfiguration surfaces at deployment rather than in production.
2.3. Tuning the code with OTP_PURPOSE_SETTINGS
This dynamic setting holds the OTP knobs, resolved per flow: first the flow's own entry, then the shared default entry, then the built-in fallback. Every field, its type and its fallback are described in section 141 of Dynamic Settings.
Log in to Omnitron using your own credentials.

Navigate to Sales Channels → Sales Channel Settings → Dynamic Settings.

Search for OTP_PURPOSE_SETTINGS in the search box and click on it in the list.
Enter the configuration and save it:
max_attempts
Wrong-guess budget for a single code. On reaching it the challenge is discarded and the customer must request a new one.
expire_time
How long a sent code stays valid, in seconds.
resent_time_gap
Minimum wait before a new code can be sent for the same challenge, in seconds.
The login entry only takes effect with EmailOrPhoneOTPLoginRedirectView, because that is the view which tags its challenges with the login flow. Under the default view the challenge carries no flow tag, so only the default entry applies to it.
Set resent_time_gap deliberately. Every resend is an SMS you pay for and a message the customer may not have asked for, and a cooldown of 0 lets a script drive that cost. A value in the region of a minute is usually enough to stop the abuse without frustrating a customer whose first message was slow to arrive.
3. The sign-in flow
The sequence is the one your phone-based client already implements; only the identifier and the response body differ. Request and response details for POST /users/otp-login/ are in the Users OpenAPI specification, under API Reference → Commerce OpenAPIs → Users.
Send the code. Post the identifier — one of
phoneoremail, not both — with nocode:The response tells you where the code went:
Show that masked value on the code entry screen. It is the only hint the customer gets, and it is what distinguishes "the code is on its way to the right phone" from "I typed the wrong email".
Verify it. Post the same identifier together with the code:
On success the customer is signed in — a session cookie is set — and an authentication token is returned alongside a
redirect_url.Resending. Repeat step 1 exactly as it was. There is no separate resend call; the
resendfield is accepted for compatibility but the flow does not act on it. The cooldown fromresent_time_gapapplies, so disable the resend button for that long rather than letting the customer discover the limit through an error.
Handling the failures
Branch on error_code, never on the message text — messages are translated and will change per language.
error_code
What to show
sms_verification_100_2
"The code is incorrect." Keep the customer on the same screen; the challenge is still alive.
sms_verification_100_4
"The code has expired." Offer a resend.
sms_verification_100_3
"Please wait before requesting a new code."
sms_verification_100_6
The attempt budget is exhausted — the challenge is discarded. Send the customer back to step 1 rather than letting them keep typing.
Two response shapes are easy to get wrong:
The generic rejection arrives under different keys. An unknown identifier produces
non_field_errorswith the email-capable view, but aphonefield error with the default view. Handle both, or the message disappears on projects that have not opted in.HTTP 429 has no numeric retry field. The number of seconds to wait is only inside the
detailmessage. Parse it if you want to show a countdown; otherwise show a generic "please try again shortly".
4. Related: passwordless sign-up
Commerce can also let customers register without choosing a password, by pointing REST_AUTH_REGISTER_SERIALIZERS at PasswordlessRegisterSerializer. It is a separate, independent feature — nothing in this guide requires it — but it depends on this one in a single direction worth flagging:
An account created without a password gets an unusable password, so password sign-in never succeeds for it. OTP login is therefore the only sign-in route those customers have, and it has to be working before the registration variant is offered. (The one other way in is the SMS-OTP password reset flow, POST /users/password/reset-with-sms-otp/, which sets a password first — a detour, not a sign-in.)
For the configuration see entries 70 (REST_REGISTER_VIEW) and 71 (REST_AUTH_REGISTER_SERIALIZERS) of Environment Variables; for the request and response contract of POST /users/registration/, see the Users OpenAPI specification under API Reference → Commerce OpenAPIs → Users.
5. Verifying the setup
Walk through the flow once as a customer would and confirm each of the following:
Signing in with a phone number still works — this is the regression check, since that path existed before.
Signing in with the email address of the same account sends the code to the same phone, and returns the same masked number.
An unknown email address and an unknown phone number produce the same generic rejection, with no hint that one of them exists.
An account that has an email address but no phone number is rejected the same way, rather than appearing to succeed.
Entering a wrong code the configured number of times ends with
sms_verification_100_6, after which a new code is required rather than another attempt.Requesting a resend immediately returns
sms_verification_100_3, and succeeds once the cooldown has elapsed.If passwordless sign-up is also enabled: an account registered without a password can sign in with a code, and cannot sign in with any password.
Last updated
Was this helpful?

