For the complete documentation index, see llms.txt. This page is also available as Markdown.

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

Phone-only (default)
With email identification

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

What
Where
Reference
Section

Accepting an email address as the identifier

OTP_LOGIN_VIEW environment variable

Environment Variables, entry 93

2.1

The authentication backends the flow needs

AUTHENTICATION_BACKENDS environment variable

Environment Variables, entry 72

2.2

Attempt budget, code lifetime, resend cooldown

OTP_PURPOSE_SETTINGS dynamic setting

Dynamic Settings, section 141

2.3

Delivery of the codes themselves

SMS_GATEWAY, SMS_GATEWAYS dynamic settings

Dynamic Settings

Optional reCAPTCHA on the code-send step

REGISTER_FORM_RECAPTCHA_CONF dynamic setting

Dynamic Settings

2.1

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:

  • email becomes an accepted identifier alongside phone;

  • 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_CONF dynamic setting — and sending and verifying are throttled under separate budgets (otp-login-send and otp-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.

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.

  1. Log in to Omnitron using your own credentials.

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

  3. Search for OTP_PURPOSE_SETTINGS in the search box and click on it in the list.

  4. Enter the configuration and save it:

Key
Description

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.

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.

  1. Send the code. Post the identifier — one of phone or email, not both — with no code:

    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".

  2. 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.

  3. Resending. Repeat step 1 exactly as it was. There is no separate resend call; the resend field is accepted for compatibility but the flow does not act on it. The cooldown from resent_time_gap applies, 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.

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:

  1. Signing in with a phone number still works — this is the regression check, since that path existed before.

  2. Signing in with the email address of the same account sends the code to the same phone, and returns the same masked number.

  3. An unknown email address and an unknown phone number produce the same generic rejection, with no hint that one of them exists.

  4. An account that has an email address but no phone number is rejected the same way, rather than appearing to succeed.

  5. 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.

  6. Requesting a resend immediately returns sms_verification_100_3, and succeeds once the cooldown has elapsed.

  7. 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?