> For the complete documentation index, see [llms.txt](https://docs.akinon.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.akinon.com/tutorials/commerce/how-to-enable-email-identification-for-otp-login.md).

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

## <mark style="color:red;">1. Overview</mark> <a href="#id-1-overview" id="id-1-overview"></a>

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](https://docs.akinon.com/technical-guides/commerce/environment-variables), entry 93 | [2.1](#id-2-setup) |
| The authentication backends the flow needs     | `AUTHENTICATION_BACKENDS` environment variable | [Environment Variables](https://docs.akinon.com/technical-guides/commerce/environment-variables), entry 72 | [2.2](#id-2-setup) |
| Attempt budget, code lifetime, resend cooldown | `OTP_PURPOSE_SETTINGS` dynamic setting         | [Dynamic Settings](https://docs.akinon.com/technical-guides/commerce/dynamic-settings), section 141        | [2.3](#id-2-setup) |
| Delivery of the codes themselves               | `SMS_GATEWAY`, `SMS_GATEWAYS` dynamic settings | [Dynamic Settings](https://docs.akinon.com/technical-guides/commerce/dynamic-settings)                     | —                  |
| Optional reCAPTCHA on the code-send step       | `REGISTER_FORM_RECAPTCHA_CONF` dynamic setting | [Dynamic Settings](https://docs.akinon.com/technical-guides/commerce/dynamic-settings)                     | [2.1](#id-2-setup) |

{% hint style="info" %}
The SMS gateway must be configured and operational before any of this works: see the `SMS_GATEWAY` and `SMS_GATEWAYS` entries in [Dynamic Settings](https://docs.akinon.com/technical-guides/commerce/dynamic-settings). Every step below assumes codes can actually be delivered.
{% endhint %}

## <mark style="color:red;">2. Setup</mark> <a href="#id-2-setup" id="id-2-setup"></a>

### <mark style="color:red;">2.1. Accepting an email address as the identifier</mark>

Add the following environment variable to the Commerce service in ACC:

```
Name: OTP_LOGIN_VIEW

Value: omnishop.users.views.EmailOrPhoneOTPLoginRedirectView
```

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](https://docs.akinon.com/technical-guides/commerce/environment-variables).

### <mark style="color:red;">2.2. Registering the authentication backends</mark>

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`:

```
omnishop.users.backends.PhoneNumberAuthenticationBackend
```

Email identification needs one more alongside it:

```
omnishop.users.backends.EmailIdentifierAuthenticationBackend
```

See entry 72 of [Environment Variables](https://docs.akinon.com/technical-guides/commerce/environment-variables) for the variable itself.

{% hint style="danger" %}
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.
{% endhint %}

### <mark style="color:red;">2.3. Tuning the code with OTP\_PURPOSE\_SETTINGS</mark>

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](https://docs.akinon.com/technical-guides/commerce/dynamic-settings).

1. Log in to **Omnitron** using your own credentials.

   <figure><img src="/files/i8QWGPZddVK4HOYZ6sAp" alt=""><figcaption></figcaption></figure>
2. Navigate to **Sales Channels → Sales Channel Settings → Dynamic Settings**.

   <figure><img src="/files/SAObd0WUbnLJRQp0Qe0n" alt=""><figcaption></figcaption></figure>
3. Search for **OTP\_PURPOSE\_SETTINGS** in the search box and click on it in the list.
4. Enter the configuration and save it:

```json
{
    "default": {"max_attempts": 5},
    "login": {"max_attempts": 3, "expire_time": 300, "resent_time_gap": 60}
}
```

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

{% hint style="info" %}
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.
{% endhint %}

{% hint style="warning" %}
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.
{% endhint %}

## <mark style="color:red;">3. The sign-in flow</mark> <a href="#id-3-the-sign-in-flow" id="id-3-the-sign-in-flow"></a>

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`:

   ```json
   {"email": "user@example.com"}
   ```

   The response tells you where the code went:

   ```json
   {"mask_phone": "0530******00"}
   ```

   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:

   ```json
   {"email": "user@example.com", "code": "123456"}
   ```

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

{% hint style="warning" %}
Two response shapes are easy to get wrong:

* **The generic rejection arrives under different keys.** An unknown identifier produces `non_field_errors` with the email-capable view, but a `phone` field 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 `detail` message. Parse it if you want to show a countdown; otherwise show a generic "please try again shortly".
  {% endhint %}

## <mark style="color:red;">4. Related: passwordless sign-up</mark> <a href="#id-4-related-passwordless-sign-up" id="id-4-related-passwordless-sign-up"></a>

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](https://docs.akinon.com/technical-guides/commerce/environment-variables); for the request and response contract of `POST /users/registration/`, see the **Users** OpenAPI specification under *API Reference → Commerce OpenAPIs → Users*.

## <mark style="color:red;">5. Verifying the setup</mark> <a href="#id-5-verifying-the-setup" id="id-5-verifying-the-setup"></a>

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.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.akinon.com/tutorials/commerce/how-to-enable-email-identification-for-otp-login.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
