SimplyClub

Fast Login

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


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
EnvironmentBASE_URL
Productionhttps://dropins.simplyclub.co.il
Staginghttps://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):

{
  "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:

{
  "otp": "123456",
  "otpChallenge": "<the otpChallenge from step 3.1>"
}
FieldRequiredNotes
otpyesThe code the shopper received by SMS.
otpChallengeyesThe value returned by get-otp. Compared timing-safely against the stored challenge.

Success response (HTTP 200):

{
  "success": true,
  "data": {
    "message": "OTP is valid",
    "access": "<session bearer token>",
    "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).

HTTPerrorCodeMeaning
400PHONE_REQUIREDNo phone in the path.
400OTP_SEND_FAILEDSimply did not issue an SMS code.
400OTP_REQUIREDotp missing from the validate body.
404OTP_INVALIDNo matching OTP for the submitted code.
400OTP_EXPIREDOTP older than its 120-second TTL.
400OTP_CHALLENGE_REQUIREDotpChallenge missing.
403OTP_CHALLENGE_MISMATCHotpChallenge does not match the stored value.
403(POI gate)fastLoginEnabled is not true, or domain not allowed.
400#1kd9djdm333The 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 limitingotpRateLimiter (per-IP) on both routes.
  • Bot detectionbotDetectionMiddleware on get-otp.
  • POI + domain checkvalidateFastOtpSecurity 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.