--- title: Fast Login tags: [auth, otp, fast-login, dropins] --- Log a shopper into Simply Club with **phone + OTP only** — no storefront account, no Shopify customer id, no host-side login. The shopper enters a phone number, receives an SMS code, and the widget looks them up in Simply Club by phone (auto-creating the member if they're new). It is the right channel for guest-checkout storefronts, microsites, and kiosks where the host never identifies the shopper. > **TL;DR** — Enable `fastLoginEnabled` on the POI. The widget calls `POST /api/v1/users/fast/get-otp/:poiId/:phone` to send an SMS and get an `otpChallenge`, then `POST /api/v1/users/fast/validate-otp/:poiId` with the code + challenge. On success it issues the **same session** (Bearer token + cookie + fingerprint) that phone+OTP-on-top-of-storefront-login would issue — every other Dropin feature works identically afterward. --- ## 1. When to use it | Use fast-login when… | Use a different channel when… | |---|---| | The storefront does **not** identify the shopper (guest checkout, microsite, kiosk). | The host already authenticated the shopper and you want OTP confirmation on top → use **storefront-asserted** (legacy) login. | | You want a two-tap login with no merchant-side user config. | Your backend already knows who the shopper is and wants **zero** shopper interaction → use [**Login From Backend**](/content/login/server-handoff-login) (server handoff). | Identity in Simply Club is **phone-only** — there is no email or customer-id fallback. The phone number is the single lookup key. (Email can be stored as contact info but is never an identity key — see [Member Profile Update](/content/login/member-profile-update).) --- ## 2. Enabling it on a POI Fast-login is **opt-in per POI**. With the flag off, both fast routes return `403`. - Set `poi.settings.fastLoginEnabled === true` (strict — any non-`true` value blocks the channel). The widget evaluates login channels in **handoff → fast → legacy** order: a valid handoff token short-circuits everything; otherwise, if `fastLoginEnabled` is on, the fast channel runs. --- ## 3. The two-call flow ### 3.1 Request an OTP ``` POST {BASE_URL}/api/v1/users/fast/get-otp/:poiId/:phone ``` | Environment | `BASE_URL` | |---|---| | Production | `https://dropins.simplyclub.co.il` | | Staging | `https://simply-drop-ins-2.onrender.com` | The server normalizes the phone, triggers a Simply SMS, wipes any prior OTP for this `(poiId, phone)`, and stores a fresh one with a **120-second TTL**. **Success response (HTTP 200):** ```json { "success": true, "data": { "message": "OTP sent", "otpChallenge": "<64-hex-char string>", "isOTPSent": true }, "error": null } ``` - **`otpChallenge`** is a 256-bit random token that binds this OTP to the next call. The client **must** echo it back verbatim to `validate-otp`. It is not the OTP itself. - On a **debug POI** (`poi.debugMode === true`) the response also includes `___test_otp` with the code, so staging tests can skip the SMS. Never enabled in production. ### 3.2 Validate the OTP ``` POST {BASE_URL}/api/v1/users/fast/validate-otp/:poiId Content-Type: application/json ``` **Request body:** ```json { "otp": "123456", "otpChallenge": "" } ``` | Field | Required | Notes | |---|---|---| | `otp` | yes | The code the shopper received by SMS. | | `otpChallenge` | yes | The value returned by `get-otp`. Compared timing-safely against the stored challenge. | **Success response (HTTP 200):** ```json { "success": true, "data": { "message": "OTP is valid", "access": "", "member": { /* full Simply member object */ }, "isNewUser": false }, "error": null } ``` - **`access`** is the session Bearer token. `isNewUser` is `true` only when the member was created during this login. --- ## 4. New vs. returning members **Returning member** (Simply finds the phone): the session is issued immediately; `isNewUser: false`. **New member** (no Simply match): the member is created immediately, phone-only. The session is issued with `isNewUser: true`. If the POI has a `profileForm` configured (or the default profile form is active), the widget will ask for first name, last name, birthday, and marketing preferences immediately after login — exactly once. Members who already have those fields set are not asked again. The OTP is consumed (deleted) **only** once a session is actually issued — never on a Simply create failure — so a correct code is never wasted by a transient backend error. --- ## 5. Error codes All errors use the standard envelope (`success: false`, with `errorCode`, `friendlyMessage`, `friendlyMessageHebrew`). | HTTP | `errorCode` | Meaning | |---|---|---| | 400 | `PHONE_REQUIRED` | No phone in the path. | | 400 | `OTP_SEND_FAILED` | Simply did not issue an SMS code. | | 400 | `OTP_REQUIRED` | `otp` missing from the validate body. | | 404 | `OTP_INVALID` | No matching OTP for the submitted code. | | 400 | `OTP_EXPIRED` | OTP older than its 120-second TTL. | | 400 | `OTP_CHALLENGE_REQUIRED` | `otpChallenge` missing. | | 403 | `OTP_CHALLENGE_MISMATCH` | `otpChallenge` does not match the stored value. | | 403 | (POI gate) | `fastLoginEnabled` is not `true`, or domain not allowed. | | 400 | `#1kd9djdm333` | The supplied `customerId` already belongs to a different Simply member (only when an optional `customerId` is sent and the POI has UDF settings configured). Friendly message: "An account already exists with this customer ID." | --- ## 6. Security posture "Fast" refers to **merchant onboarding friction, not security**. The fast routes run the same guard stack as the legacy OTP routes: - **Rate limiting** — `otpRateLimiter` (per-IP) on both routes. - **Bot detection** — `botDetectionMiddleware` on `get-otp`. - **POI + domain check** — `validateFastOtpSecurity` validates the POI id, loads the POI, enforces `fastLoginEnabled`, and checks request-origin domain access. - **OTP binding** — the 256-bit `otpChallenge` is compared with a timing-safe equal, binding the validate call to the exact OTP document issued. --- ## 7. Session parity The session fast-login issues is **byte-identical** to the one the storefront-asserted (legacy) channel issues — same `Token` shape, 90-day expiry, device fingerprint, and origin-host binding. The only internal difference is that `poiCustomerId` is `undefined` (there is no storefront customer to bind to). Downstream consumers — `/my-self`, `/user-benefits`, `/logout`, the secure-storage iframe, benefits-brain — cannot tell a fast-login session apart from any other.