1. Introduction & Definitions
General Purpose The primary purpose of this plugin is to act as a standardized, lightweight bridge between third-party commerce platforms (e.g., CRM, ERP, POS, e-commerce: Shopify, WooCommerce, etc.) and the SimplyClub loyalty and marketing automation ecosystem. By utilizing a pre-built connection layer (called Dropins), the plugin minimizes custom frontend development while delivering a native-feeling loyalty experience. It handles user synchronization, dynamic cart benefit application, and secure order lifecycle reporting via webhooks.
Note for product requirements: This Dropins.md must be used in conjunction with the PRD.md file. Any technical specification generated from this document must synthesize the product requirements outlined here with the specific technical implementation details, events, and commands provided in the Dropins documentation.
Definitions & Terminology
- SimplyClub / Simply Club / Service / Platform: All refer to the core SimplyClub backend loyalty and marketing automation solution.
- SimplyClub Dropins / Dropins / Loyalty Dropins: All refer to the connection layer that facilitates the integration. This encompasses the visual UI/GUI components (each individually referred to as a “dropin”), as well as the underlying SDK events, functions, and webhook architecture.
- POI (Point of Interaction) / Touch Point / Host / Host System / Hosting System / System: All refer to the third-party platforms (e.g., E-commerce websites, ERPs, POS, or CRM systems) that are integrating with the SimplyClub solution via the Dropins.
2. Dropins Installation & Core API
Prerequisite: Obtain your poiId from SimplyClub support — it identifies your store/location and is required for every Dropin to initialize.
Shopify users: install the official SimplyClub Shopify app instead — it wires up
setUser,setCart, webhooks, andtransactionCodecapture automatically. The manual snippet below targets WordPress, WooCommerce, custom storefronts, and any other host system.
Inject the loader before the </body> tag.
<button id="simply-btn">Join Club</button>
<script type="module">
import { dropInLoader } from "[https://dropins.simplyclub.co.il/drop-ins/drop-in-loader/index.mjs](https://dropins.simplyclub.co.il/drop-ins/drop-in-loader/index.mjs)";
const instance = await dropInLoader.setConfig({
poiId: "YOUR_POI_ID",
// env: "prod", // "prod" (default) | "test" | "local" — picks the API base URL
// starterBtn: { selector: "#simply-btn" } // Target custom element, leave commented for the default Dropin
});
// Pub/Sub Implementations go here
</script>
setConfig options:
| Option | Type | Default | Notes |
|---|---|---|---|
poiId | string | — (required) | Identifies your store/location. Get it from SimplyClub support. |
env | "prod" | "test" | "local" | "prod" | Switches API base URL. Use "test" for staging, "local" only when running the Frontend repo locally. |
starterBtn.selector | CSS selector | — | Use your own element as the launcher instead of the default Dropin button. |
user | { id, firstName, lastName, email, phone } | — | Pre-seed user state at init (alternative to calling setUser later). |
Language note: the widget’s UI language is server-driven — it is resolved from the POI’s settings in the server response, not from a
setConfigparameter. Alanguage_codeoption is not consumed by the loader.
2.1 API Commands (Host -> Dropins)
The Hosting System must call these publishers when its native state changes:
instance.commands.show(tab?): Open the SimplyClub visual dropin programmatically (the dropin layer picks the right view based on user status). Optionaltabargument jumps straight to a dashboard tab. Valid values:"dashboard","settings","notifications","renewal","howToGetPoints","transactionBenefits".instance.commands.hide(): Close the visual dropin.instance.commands.setUser({ id, firstName, lastName, email, phone }): Sync login state.instance.commands.setUser(null): Sync logout state.instance.commands.logout(): Programmatic sign-out — clears the Dropin’s authenticated session (calls the SimplyClub logout API and clearscurrent-member). Use this when the host’s own logout fires.instance.commands.setCart({ token, items: [{ id, sku, title, price, quantity, ... }] }): Sync cart mutations. Prices must reflect native hosting system discounts. The Dropin accepts the host’s native cart shape and reads the fields it needs — most Shopify/Woo cart objects can be passed through verbatim. Fields read by the Dropin:id(required) — line / variant ID (string or number)sku(required) — product SKU; falls back toidif missingtitle(preferred) orname— display name;titlewins when both exist (matches Shopify cart shape)price(required) — net unit price after host-side discounts (number)quantity(required) — units in the line (number)variant_id,image,url,description,extended_attributes(optional) — passed through for richer UI / rulestoken(optional, on the cart root — not on items) — host cart token used by the SimplyClub transaction endpoint. Field must be namedtoken(the dev demo’scartTokenis silently ignored).
instance.commands.resetBenefitSelections(): Force recalculation/clear benefits if the cart is altered post-benefit application.
Read-only / utility command helpers (all return Promises):
instance.commands.getCart()— current cart in the signal store.instance.commands.getSession()— current SimplyClub session info.instance.commands.getTransactionCode()— current SimplyClubtransactionCodeif a transaction is open.instance.commands.getBenefitSelections()— currently selected benefits.instance.commands.dispatchExampleTransaction(promos?)— returnsPromise<SelectedBenefitsObject | null>with a “seed” benefit-selection payload that can be persisted into the host cart on first load (used by the WordPress plugin to pre-populate the rewards screen). Resolves tonullwhen no transaction code is open yet. The returned payload hassource: "modal".
2.2 API Events (Dropins -> Host)
The Hosting System must subscribe and react to these events:
instance.On.benefitSelectionsChanged((data) => { ... }): Extract payload and apply host-side checkout discounts. See §5. The payload includes asource: "modal" | "embed"field —"modal"for the main rewards screen,"embed"for the in-page CTA. Hosts that want to redirect to checkout after applying benefits in the modal can use this field to skip the redirect for embed clicks.instance.On.transactionCodeCreated((code) => { ... }): Storecodeas order metadata for the webhook payload.instance.On.redirectToLogin(() => { ... }): Executewindow.location.href = '/login'(or the native login URL).instance.On.addItemToCart((e) => { ... }): Fires when the Dropin needs the host to add a non-membership product (e.g. a free gift redemption). Payload is{ product, resolve, reject }. Adde.productto the host cart, callsetCart()to re-sync, then calle.resolve()on success ore.reject(reason)on failure so the Dropin can show an error.instance.On.membershipProductSelected((e) => { ... }): Fires when the user selects a membership product inside the Dropin. Same{ product, resolve, reject }shape asaddItemToCart— adde.productnatively → callsetCart()→ calle.resolve()(ore.reject(reason)on failure).instance.On.userLoggedIn((member) => { ... }): Fires after a successful OTP login inside the Dropin. Payload is the current member object (sourced from thecurrent-membersignal). Useful for refreshing the host UI or pulling personalized data.instance.On.goToCheckout((data) => { ... }): Fires when the Dropin asks the host to navigate to checkout (e.g. the member taps a checkout CTA inside the dashboard). Redirect to your native checkout URL.
3. Visual Widgets (Dropins) & API Correlation
The Dropins layer consists of several pre-built GUI components. The Hosting System does not need to build these interfaces; it only needs to trigger them or react to them using the Pub/Sub API.
3.1 Starter Button
The primary gateway for club interactions.
- UI Behavior: * Logged Out: Displays an entry point for the Join/Registration flow.
- Logged In: Transforms to display the member’s personal club data summary.
- Correlated API: * Initialized via the
starterBtn.selectorconfig property.- The state (Logged In vs. Logged Out) is controlled strictly by the host calling
instance.commands.setUser(user)orsetUser(null).
- The state (Logged In vs. Logged Out) is controlled strictly by the host calling
3.2 Mobile OTP & Verification
Automated UI flows for secure authentication and mobile number validation.
- UI Behavior: Identifies if a user is new or returning and handles the standard OTP (One-Time Password) SMS loop.
- Correlated API: * If a guest tries to access a gated Dropin, it triggers
instance.On.redirectToLogin.- Upon successful SimplyClub verification, it triggers
instance.On.userLoggedIn.
- Upon successful SimplyClub verification, it triggers
3.3 Member Personal Club Zone
A dedicated modal view for authenticated members which shows the member’s club data, for example: reward list, club number, membership expiary date etc.
- UI Behavior: Displays the user’s reward list, point balance, and active tier. Crucially, it shows a real-time total of their current E-commerce cart and calculates what they can afford.
- Correlated API: * Opened programmatically via
instance.commands.show().- The real-time cart total inside this UI is kept accurate by the host continuously calling
instance.commands.setCart(cart)in the background.
- The real-time cart total inside this UI is kept accurate by the host continuously calling
3.4 Reward Selection Screen / Transation Benefit
The interface for choosing specific transaction benefits during checkout.
- UI Behavior: Lists available rewards (Cashback, Discounts, Free Gifts) that are mathematically compatible with the current cart.
- Correlated API: Clicking “Apply” inside this UI locks the benefits on the server to prevent double-dipping.
- Clicking “Apply” is the exact trigger that fires the
instance.On.benefitSelectionsChanged(callback)event, passing the JSON discount data back to the Hosting System for application. - Selecting a free physical product triggers
instance.On.addItemToCart. - Opened programmatically via
instance.commands.show('transactionBenefits').
- Clicking “Apply” is the exact trigger that fires the
3.5 Embedded Call to action - Register / Reward Selection
This is an embedded element that can be integrated in the POI frontend UI, for example:
- product pages
- checkout
- mini cart
It has few states:
- Member is not logged in to the dropins: it will show an option to login/register
- Member is looged in to the dropins: it will show an option to calculate the current transaction benefits and to select the available ones for the current transaction
How to apply in code:
The CTA is a separate module — it must be imported once per page in addition to the main drop-in loader. Loading the module is what registers the <simply-club-membership-cta> custom element.
<script type="module">
import "https://dropins.simplyclub.co.il/drop-ins/membership-cta-web-component/index.mjs";
</script>
<simply-club-membership-cta></simply-club-membership-cta>
The element reads poiId and member state from the main drop-in instance via the shared signal layer, so the main dropInLoader.setConfig({ poiId }) must run on the same page.
- Correlated API:
- The CTA renders three internal states automatically: guest, logged-in with empty cart, logged-in with items.
- In the guest state, clicking the CTA dispatches a DOM CustomEvent
simply-club:loginon the element (bubbles, composed). Listen ondocumentto trigger your own login flow:<script> document.addEventListener('simply-club:login', () => { window.location.href = '/login'; // or open your login modal }); </script> - In the logged-in state, clicking “Choose benefits” reveals the embedded benefits selector. Clicking “Apply” inside it fires the same
instance.On.benefitSelectionsChanged(callback)event as §3.4, passing the JSON discount data back to the Hosting System for application. - Selecting a membership product inside the embedded benefits view triggers
instance.On.membershipProductSelected.
4. User Registration/Login Flows
The Dropin supports three login channels. They coexist on the same POI — the widget picks one per page-load based on which signals are present. The session issued is byte-identical across all three; downstream consumers (/my-self, /user-benefits, /logout, secure-storage iframe) cannot tell them apart.
| Channel | When to use | Shopper sees | Requires |
|---|---|---|---|
| 4.1 Storefront-asserted | Host already authenticated the user and you want OTP confirmation on top | Phone + OTP screen | setUser({ id }) synced from host |
| 4.2 Independent mode (fast) | Guest-checkout stores, microsites — no host login | Phone + OTP screen | poi.settings.fastLoginEnabled === true |
| 4.3 Server-handoff | Host backend already knows the user and wants zero shopper interaction | Nothing — the authenticated UI mounts directly | poi.settings.handoffEnabled === true + a valid handoff token in setConfig |
The widget evaluates channels in handoff → fast → legacy order. A successful handoff token short-circuits everything below it; a missing/invalid handoff token silently falls through to fast-login (if enabled) or legacy.
4.1 Storefront-asserted identity (legacy)
The classic flow — the host already has the user logged in on its own side.
- Host Login: User authenticates on the Hosting System.
- Sync & Show: Host system notifies Dropins of the active user via
setUser(user)and callsshow()to continue the flow. - Validation: User completes OTP mobile validation inside the Dropins UI.
(Note: Unauthenticated users interacting with Dropin features will trigger the redirectToLogin event.)
4.2 Independent mode — phone + OTP
A parallel channel for storefronts that don’t identify the shopper themselves (guest checkout, microsites, kiosks). The widget asks for phone, sends an OTP, looks up the SimplyClub member by phone, and auto-creates the member if no match exists. Enabled per-POI via poi.settings.fastLoginEnabled === true. The session shape is identical to §4.1.
Routes: POST /api/v1/users/fast/get-otp/:poiId/:phone, POST /api/v1/users/fast/validate-otp/:poiId. Full spec — the two-call OTP flow, new-vs-returning member handling, error codes, and session parity — is in the dedicated Fast Login article.
4.3 Server-handoff — merchant-signed token
The shopper sees no login UI at all. A host backend that has already authenticated the shopper mints an HMAC-SHA256-signed token and passes it into the widget via setConfig({ handoffToken }); the widget POSTs the token to /api/v1/users/handoff/login/:poiId and on success the authenticated UI mounts directly.
Full spec — token format, payload constraints, minting examples in Node/PHP/Python, verify-endpoint contract with the complete error-code table, admin keys CRUD, and the security model — is in the dedicated Login From Backend article.
4.4 Reacting to a login on your backend (outbound webhook)
Independent of which channel the shopper used, the Simply server can emit a signed outbound webhook to your backend on every successful login, carrying the shopper’s verified identity plus a single-use SSO ticket. This is not a login channel for the widget — it is a server-to-server notification you can use to start a native storefront session. See the dedicated Login Through Webhook article.
5. Handling Selected Benefits (benefitSelectionsChanged)
When a user approves benefits, the benefitSelectionsChanged event passes a JSON payload detailing the applied discounts.
5.1 The Benefit Selection Payload (selectedBenefits JSON)
The Dropins will supply the following JSON structure to the callback:
{
"simply": {
"transaction": {
"number": "7770928",
"docType": "sale_tran",
"status": "in_progress",
"date": "2025-07-13T08:06:25Z",
"poi": {
"platform": "shopify",
"externalTransactionNumber": "333666999",
"externalCustomerId": "7602153062442"
},
"customer": {
"internalCustomerId": 1200621,
"type": "club_member",
"membershipCardNumber": "2886100108",
"membershipExpirationDate": "2025-07-13T08:06:25Z"
},
"selectedDiscountsAndActions": {
"totalSelectedDiscountAmount": 10.0,
"pointsDiscounts": [
{
"type": "discount",
"promoId": 3168,
"name": "Anniversary Reward Redemption",
"description": "",
"unitDiscountAmount": 5.0,
"quantity": 2,
"totalDiscountAmount": 10.0,
"promotedItems": []
}
],
"generalDiscounts": [
{
"type": "cashback",
"promoId": 10144,
"name": "10% Off Pickles",
"description": "",
"unitDiscountAmount": 12,
"quantity": 1,
"totalDiscountAmount": 12,
"promotedItems": [],
"discountPercent": 0
}
]
}
}
},
"statusInfo": {
"errorCode": 0,
"errorMessage": ""
},
"source": "modal"
}
5.2 Implementation Guide: V1 (Total Order Discount)
- State Cleanup (CRITICAL): Remove any existing native discount named
sc_club_discountand clear any previous “Pretty Print” data from the native cart/session. - Validate: Check
statusInfo.errorCode === 0. If > 0, halt execution, ensure the cart remains clear of SC discounts, and handle the error message. - Apply Total: Apply
simply.transaction.selectedDiscountsAndActions.totalSelectedDiscountAmountas a single native discount at the order level. The identifier/code must be set tosc_club_discount. - Pretty Print: Iterate through the
pointsDiscountsandgeneralDiscountsarrays. Extract thenameproperties and generate a readable string (e.g., “SimplyClub Rewards: Anniversary Reward Redemption, 10% Off Pickles”). Display this in the UI and save it to order metadata. - Transaction Linking: Save
simply.transaction.numberas the activetransactionCodemetadata to be sent in the final payment webhook.
5.3 Implementation Guide: V2 (Line-Item Allocation - Preview)
For integrations requiring accurate per-item tax calculation:
- Loop through
pointsDiscountsandgeneralDiscounts. - Map the
name,unitDiscountAmount, andquantitynatively to individual line items within the host system’s cart, rather than relying on the single aggregatetotalSelectedDiscountAmount.
6. Backend Unified Webhook
After a customer pays, refunds, or cancels an order on the host, the host backend must POST the lifecycle event to the SimplyClub order webhook. This is what closes the SimplyClub transaction, deducts/awards points, and writes back the audit trail. Without it the points/cashback are never finalized.
6.1 Endpoint
POST {BASE_URL}/api/v1/transaction/order-webhook
Content-Type: application/json
| Environment | BASE_URL |
|---|---|
| Production | https://dropins.simplyclub.co.il |
| Staging | https://simply-drop-ins-2.onrender.com |
No bearer/API-key header is required on this route — the request is authenticated by sc_transaction_code (which is unguessable and bound to a single open session) plus the resolved poiId. Use HTTPS only.
6.2 When to send
| Host event | topic value | Required toggle |
|---|---|---|
| Order paid / completed | orders/paid | enabled by default |
| Order refunded (full or partial) | refunds/create | disabled by default — ask SimplyClub support to flip webhookTopicToggles["refunds/create"] on your POI |
Other topics return HTTP 404 Unhandled webhook topic.
6.3 Request payload — required fields
The webhook accepts the host’s native order payload with four required top-level fields added. For orders/paid, customer_id is also required.
| Field | Type | Notes |
|---|---|---|
topic | string | "orders/paid" or "refunds/create". |
domain | string | Host store domain — used for logging and admin links. |
sc_transaction_code | string | The transactionCode you captured at checkout via On.transactionCodeCreated. Resolves the SimplyClub session + POI. |
order_id | string | number | Native order ID on the host. |
customer_id | string | number | Native customer ID. Required for orders/paid. |
order_number | string | number (optional) | Human-readable order number — surfaced in admin UI and SimplyClub additional info. |
The rest of the body follows the Shopify order shape because that is what the downstream processor reads — most platforms can map their native cart cleanly to it:
| Field | Type | Notes |
|---|---|---|
items or line_items | array | Required for orders/paid. Each line: { id, price, quantity, title, sku, variant_id, discount_allocations[], extended_attributes? }. items is checked first; line_items is the Shopify fallback. |
discount_applications | array | Shopify-shape array of applied discount headers. The processor matches discount_application.title against the SimplyClub discount label (הנחת מועדון) to separate SimplyClub discounts from host promo codes. |
total_price | string | Stored on the session for refund reconciliation. |
refund fields | object | For refunds/create — includes order_id, refund_line_items[], order_adjustments[], note. |
6.4 Example — orders/paid body
{
"topic": "orders/paid",
"domain": "demo-store.myshopify.com",
"sc_transaction_code": "7770928",
"order_id": "6647180362050",
"customer_id": "7602153062442",
"order_number": "1042",
"total_price": "139.90",
"items": [
{
"id": 44550337626248,
"sku": "1004",
"title": "Heart Fitted Crop Top",
"variant_id": 44550337626248,
"price": 39.9,
"quantity": 2,
"discount_allocations": [
{ "amount": "10.00", "discount_application_index": 0 }
]
}
],
"discount_applications": [
{ "title": "הנחת מועדון", "value": "10.00", "value_type": "fixed_amount" }
]
}
6.5 Response shape
Every response (success and business error) follows:
{
success: boolean,
data: { ... },
error: any | null
}
| HTTP | success | When |
|---|---|---|
200 | true | Transaction successfully closed in SimplyClub. data includes transactionResponse, sessionId, transactionCode, customer: { clubId, cardNumber }, and updates (tags + metafields the host should write back to the order). |
200 | false | Reached SimplyClub but the call failed (validation, member not found, transaction error). data includes errorCode, errorMessage, processName, and updates containing an sc_error_message metafield + paid_error tag. Do not retry — fix the underlying issue. |
200 | true | Topic toggle is off — data: { skipped: true }. Treat as a no-op. |
400 | false | Missing required fields (topic, domain, sc_transaction_code, order_id, or customer_id), or POI not found. |
404 | false | Unhandled topic. |
500 | false | Uncaught server error. Safe to retry with the same payload. |
Idempotency: if the session is already in a terminal state (paid, refunded, cancelled, expired, closed), the webhook logs a warning but does not re-process — re-delivering the same payload is safe.
Sessions are bound to one sc_transaction_code: never reuse a transaction code across orders. Capture a fresh one per checkout via On.transactionCodeCreated.
7. Admin UI Integration & The Admin Dropin (V2 Roadmap)
7.1 Basic Admin Mapping (MVP)
- Metadata flags: Save
is_simplyclub_order: trueandsimplyclub_sync_status: pendingwhen an order is created with atransactionCode. - Webhook Response: Parse the webhook response log. If
status: "success", update sync status toapproved. Ifstatus: "error", update tofailed. Display the full Info Log in the order view. - Manual Resync: Expose a “Resync SC” UI button on failed orders to manually reconstruct and resend the webhook payload.
7.2 The Admin Dropin (J5 / Auth & Capture Recalculation)
For post-checkout backoffice edits, the host will embed a specialized Admin Dropin rather than using server-to-server APIs.
Initialization (Admin Context):
<div id="simply-admin-widget"></div>
<script type="module">
import { adminDropInLoader } from "[https://dropins.simplyclub.co.il/drop-ins/admin-loader/index.mjs](https://dropins.simplyclub.co.il/drop-ins/admin-loader/index.mjs)";
const adminInstance = await adminDropInLoader.setConfig({
poiId: "YOUR_POI_ID",
container: { selector: "#simply-admin-widget" },
context: { transactionCode: "TXN-xxx", customerId: "HOST-123" }
});
</script>
Admin Pub/Sub Workflow:
adminInstance.commands.loadOrder({ originalItems, appliedDiscounts }): Host loads the original order state into the widget.- Merchant edits order: Merchant alters the uncaptured order natively in the host admin.
adminInstance.commands.updateOrderItems(newItems): Host passes updated items array to the Admin Dropin. The Dropin calculates the delta and displays suggested adjustments.adminInstance.On.benefitsRecalculated((data) => { ... }): Fired when the merchant confirms recalculation. Host receives the updated V2 allocation payload, overwrites the existingsc_club_discount, and captures the final payment.