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
fastLoginEnabledon the POI. The widget callsPOST /api/v1/users/fast/get-otp/:poiId/:phoneto send an SMS and get anotpChallenge, thenPOST /api/v1/users/fast/validate-otp/:poiIdwith 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-truevalue 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):
{
"success": true,
"data": {
"message": "OTP sent",
"otpChallenge": "<64-hex-char string>",
"isOTPSent": true
},
"error": null
}
otpChallengeis a 256-bit random token that binds this OTP to the next call. The client must echo it back verbatim tovalidate-otp. It is not the OTP itself.- On a debug POI (
poi.debugMode === true) the response also includes___test_otpwith 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>"
}
| 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):
{
"success": true,
"data": {
"message": "OTP is valid",
"access": "<session bearer token>",
"member": { /* full Simply member object */ },
"isNewUser": false
},
"error": null
}
accessis the session Bearer token.isNewUseristrueonly 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 —
botDetectionMiddlewareonget-otp. - POI + domain check —
validateFastOtpSecurityvalidates the POI id, loads the POI, enforcesfastLoginEnabled, and checks request-origin domain access. - OTP binding — the 256-bit
otpChallengeis 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.