🚧
Work in progress
We're still writing this guide — details may change as it's finalized.
Independent login Auth OTP Phone Shopify Web

Fast Login — Independent Mode

Add phone+OTP login to any storefront without storefront customer identification — opt-in per POI.


Fast Login — Independent Mode

Fast Login lets a shopper authenticate into SimplyClub with phone + OTP only, without the storefront having to identify the customer first. We call this independent mode — the SimplyClub drop-in runs as its own login surface, decoupled from whatever account system the storefront has (or doesn't have).

For a guest-checkout shop, a microsite, or any page where the visitor has not signed in to the storefront, the legacy "give us a customer ID, then we'll send an OTP" flow is a non-starter. There is no customer ID yet. Independent mode flips that: the drop-in asks for a phone number, sends an OTP, looks up (or creates) the SimplyClub member by phone, and issues the same authenticated session the legacy flow does.

Independent mode is opt-in per POI. It does not replace the legacy flow — both can coexist. A POI that has it disabled continues to behave exactly as today.

What fast-login is

Fast-login is a parallel authentication channel for the SimplyClub drop-in. From the shopper's point of view it is two taps:

  1. Tap to enter their phone number, press Send code.
  2. Tap to enter the 6-digit OTP they receive by SMS, press Verify.

That's it. There is no email step, no "do you have an account with the storefront?" prompt, no customer-ID handoff from the storefront. Phone is the only identity key SimplyClub uses to look up a member in independent mode.

Under the hood:

  • The widget calls POST /api/v1/users/fast/get-otp/:poiId/:phone to send the OTP.
  • It calls POST /api/v1/users/fast/validate-otp/:poiId with the code to verify.
  • If a SimplyClub member with that phone already exists, the route returns an authenticated session immediately.
  • If no member with that phone exists, the route auto-creates one (you'll see how this works in section 5) and returns the same session shape.

The session itself — Bearer token, device-fingerprint cookie binding, all downstream /my-self / /user-benefits / /logout semantics — is identical to today's legacy flow. Once a shopper is logged in via fast-login, no other part of the system needs to know which channel they came through. Benefits, points, secure storage, and logout all work the same way.

A few things to note up front:

  • Phone-only identity. There is no email fallback, no customer-ID fallback. If a shopper changes phone numbers, they'll appear as a new member. This is by design — the goal of independent mode is removing friction, and identity-lookup fallbacks reintroduce per-store configuration.
  • Configurable post-OTP profile collection. Independent mode often runs on storefronts that don't already know the shopper, so the drop-in supports a configurable per-POI form to collect a few extra fields (name, birthday, marketing preferences, ...) immediately after the first successful OTP. The configuration lives in the Dashboard alongside the fast-login toggle — see section 6 (lands in the next phase of this article).
  • Same security posture. Fast-login applies the same rate limiter, POI-domain guard, OTP-security checks, and bot detection as the legacy flow. "Fast" refers to merchant-side onboarding friction, not to the security model.

When to use it (and when NOT)

Fast-login is the right fit when the storefront does not have authoritative customer identity at the moment the drop-in needs to authenticate the shopper. Concretely:

Use fast-login when:

  • The storefront supports guest checkout and most shoppers never sign in.
  • The site is a microsite, single-page shop, or campaign landing that has no account system at all.
  • The storefront does have accounts, but plumbing the customer ID through to the drop-in (via setConfig({ user: { id: ... } })) is awkward or unreliable.
  • You want a "no merchant config" install path so a vendor can be live in minutes — phone is the only identity, no UDF mapping required.
  • You're prototyping a new storefront and don't want to commit to a customer-ID story yet.

Do NOT use fast-login when:

  • The storefront already has authoritative customer identity and the legacy flow is working. There's no need to migrate. Legacy continues to work for any POI that does not turn fast-login on — the two flows coexist.
  • You need to enforce storefront-side account linking before SimplyClub login (rare — for example, a B2B portal where every shopper must be a known account on the storefront first). Fast-login intentionally bypasses that gate.
  • The storefront's identity model requires email or external customer ID as the primary key, not phone. Independent mode does not look up members by email or customer ID; if your data model requires this, stay on legacy.

Cross-link: see also the API Reference for the full setConfig option table and the commands.setUser flow that the legacy mode uses.

Enable it on a POI

Fast-login is opt-in per POI via a single toggle in the Dashboard. There is no global flag — every POI starts with fast-login off, and a vendor has to consciously turn it on.

  1. Sign in to the SimplyClub Dashboard at https://dropins.simplyclub.co.il/.
  2. Pick the POI you want to enable from the top-bar POI selector.
  3. Click Settings in the sidebar.
  4. On the General tab, find the switch labeled "התחברות מהירה (טלפון + OTP בלבד)" (Hebrew: "Fast login — phone + OTP only"). Turn it on.
  1. Switch to the טופס הצטרפות ("Onboarding form") tab. This is where you decide which post-OTP fields a brand-new shopper sees on their first login (full name, birthday, marketing-consent toggles, and so on). Pick the fields you want, drag them into the order you want them collected, mark which are required, and choose between a multi-step ("stepper") layout or a single combined form. Section 6 covers the configuration in detail — that section lands in the next slice of this article.
  1. Click Save at the bottom of the page. The Dashboard uses a single bottom-of-page Save for all tabs — settings on every tab are committed in one round-trip.
  2. Reload the page to confirm the toggle is still on. If it isn't, the save did not persist — re-check the snackbar message at the bottom of the page.
  3. Test it: open your storefront in a private window, trigger the drop-in, and confirm the first step is the phone-entry screen (not the legacy "you must be logged in" screen).

Note: the configuration round-trip routes through the existing PATCH /api/v1/poi/:poiId/styling endpoint and persists to poi.settings.fastLoginEnabled on the POI document. No separate "fast-login config" object — it's a single boolean on the POI you already have.

Storefront integration

For most storefronts, switching to fast-login is less code, not more. The drop-in already reads the POI settings it needs at startup, and the settings.fastLoginEnabled flag drives the in-widget routing automatically (see simply/Frontend/src/components/OnboardingModal/flows/modes/ecommerce.tsx:77-82 for the implementation). You don't need to call a different API — you just pass less data into setConfig.

Compare the two flows:

// Legacy (still works — for non-fast-login POIs):
//
// The storefront has to know who the shopper is BEFORE the drop-in
// can authenticate them. `user.id` is required and is typically the
// Shopify customer GID or a custom identifier from the storefront's
// own account system.
const instance = await dropInLoader.setConfig({
  poiId: "YOUR_POI_ID",
  user: {
    id: "shopify-customer-id-123",
    email: "shopper@example.com",
    firstName: "Dana",
    lastName: "Cohen",
  },
});

// Fast login (POI with settings.fastLoginEnabled === true):
//
// No `user` required. The drop-in reads `fastLoginEnabled` off the
// POI config it already fetches at startup and routes login through
// the fast routes (/users/fast/get-otp + /users/fast/validate-otp).
// The shopper goes straight to the phone-entry step.
const instance = await dropInLoader.setConfig({
  poiId: "YOUR_POI_ID",
  // No `user` object needed.
});

That's the minimum diff: drop the user object from your setConfig call. The drop-in detects fast-login mode on its own and skips the legacy "login to POI first" screen.

Optional: prefill first/last name to skip a step

If the storefront does know the shopper's first or last name — for example, from a checkout form they just filled in — you can still pass it. The drop-in will use it to skip the name-collection step on the first OTP submit for a brand-new member:

// Fast login with optional name prefill — skips the NameStep for new members:
const instance = await dropInLoader.setConfig({
  poiId: "YOUR_POI_ID",
  user: {
    firstName: "Dana",
    lastName: "Cohen",
  },
});

What you should NOT pass in fast-login mode:

  • user.id — ignored. Fast-login looks up by phone, not by ID.
  • user.email — ignored for identity purposes. Email collection happens in the post-OTP profile form (section 6 below), not in setConfig.

Coexistence with legacy

A storefront that uses both fast-login POIs and legacy POIs from the same script-tag install does NOT need any conditional logic. Each setConfig call is per-POI, and each POI's fastLoginEnabled setting drives its own widget behavior independently. The same drop-in bundle handles both flows.

The full setConfig option table — including events, commands, and the legacy user shape — lives in the API Reference article. The fast-login-specific routes (/users/fast/get-otp and /users/fast/validate-otp) land in section 5 of this article, in the next phase.

API Reference

The fast-login server exposes two routes under /api/v1/users/fast/.... They are parallel to the legacy /api/v1/users/get-otp and /api/v1/users/validate-otp routes (which remain untouched and continue to serve POIs that have not opted in to fast-login), and they share the same security middleware family: otpRateLimiter, validateFastOtpSecurity (POI-domain + opt-in gate), and — on the SMS-dispatching side — botDetectionMiddleware.

All responses use the standard SimplyClub resulter envelope:

{
  "success": true,
  "data": { /* payload */ },
  "error": null
}

or, on error:

{
  "success": false,
  "data": null,
  "error": { "message": "..." }
}

Every user-facing 4xx error includes both friendlyMessage (English) and friendlyMessageHebrew so the client can render either directly without translation. The bilingual envelopes below are pulled verbatim from the controller and middleware source — they are the authoritative copy.


POST /api/v1/users/fast/get-otp/:poiId/:phone

Sends an OTP SMS via Simply Club for the given POI + phone, persists a 120-second-TTL OTP document, and returns the random 256-bit otpChallenge that must be echoed back on the validate call. There is no request body — both inputs are URL parameters.

Path parameters:

Parameter Type Required Description
poiId string yes Your POI's ObjectId. Must be a valid 24-character hex ObjectId.
phone string yes International format preferred. The server normalizes via normalizePhoneForOtp and accepts Israeli local format as well.

Body: (empty — no body needed)

Middleware chain:

  • otpRateLimiter — per-IP+phone rate limit on the OTP issuance surface.
  • botDetectionMiddleware — heuristic bot screening before any SMS is dispatched (only applied on get-otp; validate is gated by the OTP itself).
  • validateFastOtpSecurity — validates the POI id format, loads the POI, enforces the fastLoginEnabled === true opt-in flag, and runs the POI-domain check. Attaches req['poi'] for the controller.

200 Success:

{
  "success": true,
  "data": {
    "message": "OTP sent",
    "otpChallenge": "<256-bit hex string>",
    "isOTPSent": true
  }
}

Note: debug-mode POIs additionally receive ___test_otp inside the data block. Production POIs never see it — debugMode is a per-POI dev flag.

Error envelopes:

Status errorCode friendlyMessage friendlyMessageHebrew
400 INVALID_POI_ID The store identifier is invalid. מזהה החנות אינו תקין.
404 POI_NOT_FOUND Store not found. החנות לא נמצאה.
403 FAST_LOGIN_DISABLED Fast login is not enabled for this store. התחברות מהירה לא מופעלת עבור חנות זו.
403 DOMAIN_NOT_ALLOWED This domain is not allowed to access fast login. הדומיין הזה אינו מורשה לגשת להתחברות המהירה.
403 ORIGIN_REQUIRED Request origin is required. נדרש מקור בקשה.
400 PHONE_REQUIRED Phone is required טלפון נדרש
400 OTP_SEND_FAILED Could not send verification code. Please try again. לא ניתן לשלוח קוד אימות. אנא נסו שוב.
500 INTERNAL_ERROR (no friendly fields — generic 500) (no friendly fields)

The middleware errors (INVALID_POI_ID, POI_NOT_FOUND, FAST_LOGIN_DISABLED, DOMAIN_NOT_ALLOWED, ORIGIN_REQUIRED) gate every fast-login route, not just get-otp — they also apply to validate-otp below.

Curl example:

curl -X POST 'https://dropins.simplyclub.co.il/api/v1/users/fast/get-otp/<POI_ID>/<PHONE>' \
  -H 'Origin: https://your-storefront.com'

POST /api/v1/users/fast/validate-otp/:poiId

Validates the OTP, looks up the SimplyClub member by phone (or auto-creates one if no member exists for that phone yet), and on success returns an authenticated session token + member object. The OTP doc is deleted on success; on failure paths (including NAME_REQUIRED for new members) the doc is preserved so the user can retry within the 120-second TTL window without re-requesting a code.

Path parameters:

Parameter Type Required Description
poiId string yes Your POI's ObjectId. Must be a valid 24-character hex ObjectId.

Body:

{
  "otp": "<6-digit code from SMS>",
  "otpChallenge": "<challenge from the get-otp response>",
  "name": "<full name — required ONLY for first-time members>"
}

name is optional on the first call. If the OTP resolves to an existing SimplyClub member (looked up by phone), name is ignored. If no member exists for the phone, the server attempts to auto-create one — and at that point it requires name to be a non-empty string. If name is missing in the new-member branch, the route returns 400 with errorCode: NAME_REQUIRED, and the widget automatically re-prompts on its dedicated NameStep before retrying. If your storefront already knows the shopper's first and last name (for example, from a setConfig({ user: { firstName, lastName } }) prefill — see section 4), pass them concatenated on the first validate-otp call to skip the round-trip entirely.

Middleware chain:

  • otpRateLimiter — same per-IP+phone rate limit as get-otp.
  • validateFastOtpSecurity — same POI + opt-in + domain gate. No botDetectionMiddleware on validate — only the SMS-dispatching get-otp route triggers bot screening.

200 Success (existing member):

{
  "success": true,
  "data": {
    "message": "OTP is valid",
    "access": "<bearer-token>",
    "member": { /* Simply Club member object — same shape as legacy /validate-otp */ },
    "isNewUser": false
  }
}

200 Success (new member auto-created):

{
  "success": true,
  "data": {
    "message": "OTP is valid",
    "access": "<bearer-token>",
    "member": { /* Simply Club member object */ },
    "isNewUser": true
  }
}

The access token is the same generateToken() output the legacy /validate-otp issues, so all downstream endpoints (/users/my-self, /users/user-benefits, /users/logout, the secure-storage iframe) authenticate fast-login sessions identically to legacy sessions. isNewUser: true is the signal the widget uses to route into the post-OTP profile form (section 6) for fresh members.

Error envelopes:

Status errorCode friendlyMessage friendlyMessageHebrew
400 INVALID_POI_ID The store identifier is invalid. מזהה החנות אינו תקין.
404 POI_NOT_FOUND Store not found. החנות לא נמצאה.
403 FAST_LOGIN_DISABLED Fast login is not enabled for this store. התחברות מהירה לא מופעלת עבור חנות זו.
403 DOMAIN_NOT_ALLOWED This domain is not allowed to access fast login. הדומיין הזה אינו מורשה לגשת להתחברות המהירה.
403 ORIGIN_REQUIRED Request origin is required. נדרש מקור בקשה.
400 OTP_REQUIRED Please enter the verification code נא להזין את קוד האימות
404 OTP_INVALID Invalid or incorrect code. Please try again. הקוד שגוי או לא תקין. אנא נסו שוב או בקשו קוד חדש.
400 OTP_EXPIRED This code has expired. Request a new one. פג תוקף הקוד. אנא בקשו קוד חדש.
403 OTP_POI_MISMATCH This code is not valid for this site. הקוד אינו תקף עבור אתר זה.
403 OTP_BINDING_INVALID This code could not be verified. Request a new code. לא ניתן לאמת את הקוד. אנא בקשו קוד חדש.
400 OTP_CHALLENGE_REQUIRED Session verification is missing. Request a new code. חסר אימות סשן. אנא בקשו קוד חדש.
403 OTP_CHALLENGE_MISMATCH This verification session is invalid. Request a new code. סשן האימות אינו תקף. אנא בקשו קוד חדש.
400 NAME_REQUIRED Please tell us your name to finish signing up. אנא הזינו את שמכם לסיום ההרשמה.
400 #1kd901,f8$3 Could not create account. לא ניתן ליצור חשבון.
400 207 This account already exists. החשבון כבר קיים.
400 (Simply ErrorCode pass-through) (Simply ErrorMessage) (n/a)
500 MEMBER_LOAD_FAILED Account created but could not be loaded. Please try again. החשבון נוצר אך לא ניתן לטעון אותו. אנא נסו שוב.

When NAME_REQUIRED fires, the widget automatically advances to a dedicated NameStep (introduced in Phase 02.1) — your storefront does not need to handle this manually unless you're integrating fast-login from a non-widget client.

A note on the more cryptic codes:

  • OTP_POI_MISMATCH is a defensive belt-and-braces guard — the OTP lookup query is already filtered by poiId, so a matched document has the correct POI by construction. The code is preserved so a future query-widening change does not silently weaken the binding.
  • OTP_BINDING_INVALID fires when the stored OTP document has no challenge string — i.e., it was inserted before the challenge-binding feature shipped. New OTP docs always carry one.
  • #1kd901,f8$3 is the sentinel for "Simply addMember returned a falsy response" in the new-member branch. It is intentionally not a friendly code — it's a tombstone for an unrecoverable Simply API condition.
  • 207 is Simply's "user already exists" race condition, surfaced if a parallel request created the member between the lookup and the create.

Curl example (existing member):

curl -X POST 'https://dropins.simplyclub.co.il/api/v1/users/fast/validate-otp/<POI_ID>' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: device_fingerprint=<from-get-otp-response>' \
  -d '{ "otp": "123456", "otpChallenge": "<challenge>" }'

Curl example (new member auto-create):

curl -X POST 'https://dropins.simplyclub.co.il/api/v1/users/fast/validate-otp/<POI_ID>' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: device_fingerprint=<from-get-otp-response>' \
  -d '{ "otp": "123456", "otpChallenge": "<challenge>", "name": "Dana Cohen" }'

Session lifecycle & TTLs

A few timing and lifecycle constants are worth knowing when you reason about user-facing edge cases:

  • OTP document TTL: 120 seconds. When get-otp succeeds, the OTP doc is persisted with expiresAt = now + 120s. The MongoDB TTL index on the OTP collection sweeps expired docs in the background, but the controller also re-checks expiresAt at runtime on every validate-otp call, so a doc that has been swept can never be replayed by a racing client. After 120s, the user has to request a fresh OTP.
  • OTP doc is preserved on failure paths. A failed validate-otp — wrong code, missing name, addMember race, anything in the 400 range — leaves the OTP doc in place so the user can retry within the TTL window without re-requesting a code. The doc is deleted ONLY when the route commits to issuing a session (after getMember resolves to a real member, before Token.create).
  • OTP doc cleared on a new get-otp call. Each get-otp for a given { poiId, phone } pair calls OTP.deleteMany({ poiId, phone }) before insert, so requesting a fresh code invalidates any in-flight one. There's no race between a stale and a fresh OTP for the same phone+POI.
  • Bearer token lifetime: 90 days. Identical to the legacy session — a fast-login token is a peer of a legacy token from the moment validate-otp returns it. There is no fast-token shorter lifetime, and no separate refresh flow.
  • device_fingerprint cookie binding. The validate-otp response binds the issued token to the device_fingerprint cookie that was sent on the request (typically established earlier in the page lifecycle by the drop-in loader). Downstream endpoints check the binding on every request, so a token leaked without the cookie is not replayable.

Troubleshooting

Common scenarios you might see when integrating, and what they typically mean.

FAST_LOGIN_DISABLED on every fast-route call — the POI has settings.fastLoginEnabled !== true. Re-check the General tab toggle in the Dashboard; confirm the Save snackbar succeeded; reload the Dashboard page and confirm the toggle is still on. If the toggle is on in the Dashboard but the API still rejects, the in-memory poiCache on the server may need to be evicted — re-saving the POI on the General tab clears it. Cache invalidation runs on every settings mutation, so this should resolve itself within a single round-trip.

DOMAIN_NOT_ALLOWED on requests from your storefront — the storefront's origin or referer is not on the POI's allowedDomains list. Open Settings → General → Domains and confirm your storefront's exact origin (scheme + host + optional port) is in the list. Watch for www. vs. apex — https://example.com and https://www.example.com are different origins and both have to be listed if both are reachable.

ORIGIN_REQUIRED on a curl request that worked yesterday — your request has no Origin AND no Referer header. Most browsers send one or both automatically, so this usually points at a server-to-server call that needs to pass -H 'Origin: https://your-storefront.com' explicitly. The middleware does not infer the origin from any other source.

OTP_CHALLENGE_MISMATCH after a clean get-otp / validate-otp pair — the otpChallenge you echoed back on validate does not match the one returned by get-otp. The match is done via crypto.timingSafeEqual over the byte-buffer of the string, so any whitespace, casing, or encoding drift causes a mismatch. The widget copies the challenge directly out of the JSON response without modification — if you're integrating from a non-widget client, double-check that you're not URL-encoding or trimming the value.

OTP_EXPIRED on what feels like a fast retry — 120 seconds includes SMS arrival + user typing + any post-OTP screen the user lingers on. If you're seeing this consistently, consider whether your client is delaying validate-otp until after the post-OTP profile form (it shouldn't — validate-otp runs as soon as the user enters the code).

NAME_REQUIRED on a returning shopper — this is the new-member branch firing for someone you expected to already have a SimplyClub member. Most likely the phone number was normalized differently this time than at member-creation time; the canonical form is the result of normalizePhoneForOtp on the server. If you suspect this, log a support ticket with the phone in question and we can confirm whether a SimplyClub member exists for it.

#1kd901,f8$3 or MEMBER_LOAD_FAILED — these are Simply API failure tombstones for the new-member branch. They are not user-actionable; surface a generic "something went wrong, please try again later" to the shopper and check the server logs for the Simply error envelope detail. If they happen at non-trivial volume, that's a server-side incident, not a client integration bug.


Profile form configuration

After a shopper authenticates via OTP, the widget can optionally collect a handful of additional profile fields — name, birthday, marketing consent, and so on — before showing them their loyalty dashboard. Each POI configures which fields to collect, in which order, whether to mark them required, and whether to present the collection as a multi-step stepper (one field per screen) or as a single combined form. The Dashboard's "טופס הצטרפות" ("Onboarding form") tab — the 5th tab under Settings — is the form-builder.

The form-builder is intentionally scoped to a fixed allowlist of fields. The set was chosen to cover the high-frequency loyalty profile data without exposing payment-card surfaces or fields that have their own auth-binding rules.

Allowlisted fields

The eight fields you may add to a profile-form configuration:

FieldId Input type What it collects Notes
first_name text The shopper's given name Often pre-filled from setUser({ firstName }) and skipped at runtime
last_name text The shopper's family name Often pre-filled from setUser({ lastName }) and skipped at runtime
email email Email address Can be marked required; gates marketing-email consent if collected
personal_id tel Israeli T.Z. (or generic personal ID) No checksum validation in v1
birthday date Date of birth, DD/MM/YYYY Validates year ≥ 1920
wedding_day date Wedding anniversary, DD/MM/YYYY Same date validation as birthday
if_send_sms checkbox Consent to receive SMS marketing See "Consent fields" below
if_send_email checkbox Consent to receive email marketing See "Consent fields" below

Drag the rows in the form-builder to reorder them — the order in the Dashboard matches the order shoppers will see at runtime.

Denied fields

Two Simply API fields are explicitly never collectable via the post-OTP profile form:

FieldId Reason for deny
card_field Payment surface — never collected via the profile form
cell_number Phone changes still require a fresh OTP (covered by updateMember.ts)

If a request to PATCH a profileForm configuration includes either of these FieldIds, the server validator rejects it with a 400 and errorCode: PROFILE_FORM_FIELD_DENIED. The Dashboard form-builder UI does not expose these in the field picker — the deny-list is enforced both at the UI layer (the row never appears in the "Add field" menu) and at the server layer (a hand-crafted PATCH is rejected).

Stepper vs. form mode

The configuration includes a top-level mode selector with two options:

  • Stepper (mode: 'stepper'): one field per screen, with Next and Skip controls. Skip is allowed only when the field is not marked required. This is the legacy parity layout — it matches today's default three-screen flow (birthday → SMS consent → email consent).
  • Form (mode: 'form'): all configured fields appear on one screen, with a single Submit button. The widget PATCHes a single updateMember request containing all the values the shopper filled in; skipped optional fields are dropped from the body so they don't overwrite anything server-side.

When to pick stepper:

  • Few fields configured, want low cognitive load per screen.
  • Want default parity with the existing 3-screen flow.
  • Mobile-first storefronts where vertical space is tight.

When to pick form:

  • More than three or four fields configured, and you want shoppers to see everything before committing.
  • Desktop-heavy traffic where the extra screen real estate makes a multi-field form readable.

Consent fields (if_send_sms, if_send_email)

The two marketing-consent fields have special handling:

  • Default value is always unchecked. If a configuration tries to set defaultValue: true on either consent field, the server validator silently strips it and logs a warning to the POI log. Pre-checked consent is non-compliant in most jurisdictions (GDPR, CCPA-style frameworks), so this is enforced at the validator boundary rather than left to merchant discretion.
  • Can be marked required: true. Some merchants in stricter jurisdictions want explicit consent before completing onboarding. The validator accepts required: true on consent fields without complaint — it is a merchant decision. Consult your legal counsel about exposure under your applicable privacy regime.
  • Renders as a checkbox in both modes. In stepper mode each consent appears on its own screen; in form mode they appear inline with the rest of the form.

Labels

The form-builder deliberately does NOT expose per-row label inputs. Labels are rendered via the existing POI language system, so a single label edit propagates to every place that fieldId appears (the join flow, the My-Self page, anywhere a label is needed). The widget's runtime label resolver picks the right language per the POI's language setting.

To customize a label for a fieldId, edit it in Settings → Texts (the language system tab), not in the form-builder. The form-builder's job is structural — which fields, which order, which required, which mode — not copy.

Save with 0 fields included

The Dashboard form-builder will refuse to save if no fields are toggled on. You'll see the inline error "יש לבחור לפחות שדה אחד כדי לשמור את הטופס." ("Select at least one field to save the form.") under the Save button.

This is a Dashboard-side UX choice. The server validator does accept fields: [] as a legitimate opt-out configuration — a merchant who specifically wants zero post-OTP collection can craft a direct PATCH today, or wait for a future "Disable post-OTP form" toggle which will surface it more cleanly in the UI.


Migration guide

Migrating an existing legacy POI to fast-login is non-destructive. The legacy /users/get-otp and /users/validate-otp routes remain in service; the fast routes are additive. Flipping fastLoginEnabled: true on a POI does NOT remove the legacy routes from that POI — it just lights up the new ones alongside. The widget chooses which family to use based on what setConfig was given, so a careful rollout can flip the toggle, observe traffic, and roll back without ever taking the legacy path offline.

The phases below are sequential, but each is small enough to do in a single Dashboard session.

Step 1 — enable fast-login on the POI

Refer back to section 3 ("Enable it on a POI") for the click-through. The summary: open Settings → General, switch on "התחברות מהירה (טלפון + OTP בלבד)", click Save at the bottom of the page.

Flipping the toggle does not change how the legacy /users/get-otp route behaves on its own. It also does not change how an already-shipped widget on your storefront talks to that POI — your live shoppers won't notice anything until the widget code that reads the new flag is also deployed. This is intentional: the server-side change and the storefront-side change can be staged independently, and the server side is safe to ship first.

Step 2 — configure the post-OTP profile form

Switch to the "טופס הצטרפות" tab and configure which fields to collect after a successful OTP. Refer back to section 6 for the field-by-field details and the stepper-vs-form decision. If you don't touch this tab, the widget falls back to the Phase-5 default: birthday → SMS consent → email consent, in stepper mode. New POIs get this default automatically; existing legacy POIs that haven't touched the form-builder are also covered by the default.

Step 3 — verify legacy still works alongside

The fast-login channel is built on top of two coexistence guarantees verified in Phase 3 (the SES-03 session-parity proof and the POI-03 legacy-routes-untouched proof):

  • The legacy /users/get-otp and /users/validate-otp routes are byte-for-byte unchanged. The product owner's hard constraint that live merchants depending on them must see zero behavior change holds across the fast-login work.
  • A POI with fastLoginEnabled: false answers ONLY the legacy routes. A POI with fastLoginEnabled: true answers BOTH the legacy AND the fast routes simultaneously — the new routes are additive, not a replacement.
  • Downstream consumers — /users/my-self, /users/user-benefits, /users/logout, the secure-storage iframe — authenticate fast-login sessions and legacy sessions identically. The Bearer token, the device-fingerprint cookie binding, and the origin host check are all the same machinery. Phase 3 SES-03 proved this end-to-end with a side-by-side audit of both session types hitting every downstream route.

The practical implication for migration is that a storefront still calling setConfig({ poiId, user: { id: "..." } }) continues to work on a fast-login-enabled POI. The widget sees user.id is present and routes through legacy. Only when the storefront stops passing user.id AND the POI has settings.fastLoginEnabled === true does the widget switch to the fast routes. So you can ship the server-side flip and the storefront-side setConfig change in either order without ever creating a broken state.

Step 4 — verify with a test storefront

Once the toggle is on and the form is configured, try it on a test storefront (a staging mirror, a private window on production, or a local dev page that points at the same POI):

  1. Open the test storefront.
  2. Trigger the drop-in — click your usual entry button.
  3. Confirm the first screen is the phone-entry step. There should be no email field, no "are you a returning customer?" prompt, no customer-ID lookup.
  4. Enter a phone you control. The SMS should arrive within a few seconds.
  5. Enter the OTP. For a phone that already has a SimplyClub member, you should land directly on the dashboard. For a brand-new phone, you should see the NameStep asking for a full name.
  6. After name entry (if shown), confirm you land on the post-OTP profile form — either a stepper or a single form, matching what you configured in Step 2.
  7. Walk through the form to the end. Confirm the dashboard renders normally: points, benefits, the membership state.
  8. Open /users/my-self for that session (via the widget's network panel or a direct curl with the Bearer token) and confirm it returns the same shape it returns for a legacy-logged-in shopper. This is the SES-03 parity guarantee in practice.

Rollback

If you observe a problem, flip the toggle OFF. Open Settings → General and switch off "התחברות מהירה (טלפון + OTP בלבד)", then Save. The legacy paths immediately become the only option for that POI on every subsequent request.

What rollback does NOT do: it does not invalidate sessions that were already issued through fast-login. Per Phase 3 SES-03, tokens are channel-agnostic — once a shopper has a valid Bearer + fingerprint + originHost combination, every downstream endpoint authenticates it regardless of how the session was first issued. So a rollback removes the entry point but leaves existing fast-login shoppers logged in. That's almost always what you want.

The recommended posture: leave the toggle ON unless you observe a regression. Fast-login is additive; it cannot break the legacy flow because the legacy code path is unchanged at the server.

Observability after rollout

After you enable fast-login, here is what you should expect to see on the server logs and the Dashboard's POI log view over the first few days:

  • A new line of LogService.info entries tagged with (fast-login) for every successful login, and (fast-login, new member) for every auto-create. These are the same shape as the legacy login-info logs and feed into the same Telegram notification surface (best-effort, swallowed on failure).
  • Legacy /users/get-otp traffic on this POI declining as the widget routes more requests through the fast path. The decline is gradual — older browser tabs and cached page-loads will continue to call legacy until they re-fetch the POI settings.
  • A new bucket of NAME_REQUIRED 400s in your validate-otp metrics. These are not errors in the operational sense — they are the normal new-member round-trip on phones that had no SimplyClub member before. If you see them at high volume, that is a sign you have a lot of new shoppers, which is the whole point of fast-login.
  • Occasional OTP_EXPIRED 400s for users who took longer than 120 seconds. These were always possible on the legacy flow too, but the shorter fast-login TTL makes them more visible. If volume seems unusually high, walk through the flow on a test phone and confirm there is no extra screen sitting between OTP entry and validate-otp dispatch.

When fast-login is NOT the right move

A reminder from section 2 ("When to use it (and when NOT)"): if your storefront already has authoritative customer identity AND uses it for downstream authorization decisions (e.g., loyalty-tier gating tied to a Shopify customer GID), fast-login alone may not be enough. In that case keep both paths in play — your storefront continues to call setConfig({ user: { id: ... } }) for authenticated shoppers, and fast-login picks up the guest-checkout long tail.

The two channels are designed to coexist indefinitely on the same POI. There is no roadmap item to deprecate legacy. If your storefront is in the "both customer-identified and guest" middle ground, running both channels at once is fully supported and is the expected configuration for most live POIs that turn fast-login on.


Need help? Reach out to SimplyClub support.