Every time a shopper authenticates into Simply Club — through the phone+OTP fast channel, the server-handoff channel, or the legacy channel — the Simply server can emit a signed outbound webhook to your backend carrying the shopper’s verified Simply identity plus a short-lived, single-use SSO ticket. Your backend can use that ticket to identify the shopper and, optionally, start a native storefront session (Shopify Multipass, WordPress wp_set_auth_cookie, a custom JWT, etc.).
TL;DR — Enable the outbound webhook in the Dashboard. On every login, Simply will POST a signed JSON payload to your URL. The payload contains the shopper’s Simply identity and a single-use
ssoTicket. Exchange that ticket for the full identity by callingPOST /api/v1/sso/redeem. What you do with the identity to create a storefront session is your adapter’s responsibility — Simply gives you the identity and the ticket; the last inch is yours.
1. When to use it
Use the outbound login event when your backend needs to act on a Simply login — regardless of how the shopper authenticated:
- Auto-log the shopper into your platform (Shopify, WooCommerce, custom app) when they authenticate in the Simply widget.
- Associate a Simply member with a platform-side user account on first login.
- Trigger loyalty or personalization workflows the moment a member is identified.
- Any platform — custom code, WordPress, WooCommerce, Shopify — that can receive a webhook and optionally issue a storefront session.
Do not use the outbound event as an inbound webhook (the order-webhook is separate and unrelated). The outbound login event fires on every authenticated login, not on orders.
2. Enabling it on a POI (Dashboard)
The outbound channel is opt-in per POI. A POI with no active outbound config produces zero outbound HTTP calls.
- Open the SimplyClub Dashboard → your POI → Webhooks → Outbound Webhook.
- Enter an
https://destination URL (only HTTPS is accepted). - Click Save URL — a modal opens and shows your signing secret exactly once. Copy it to your backend’s secret store before closing.
- Flip the Enable toggle to on. Delivery starts on the next login.
Multiple configs per POI. Each config has its own URL, secret, and isActive state. To rotate a secret: click Rotate signing secret, copy the new secret, redeploy your verifier with the new secret, then disable or delete the old config.
Secret shown once. The Dashboard never shows the raw secret again after the creation modal. If you lose it, rotate or delete the config and create a new one.
3. Signed neutral payload schema
Simply POSTs a JSON body to your URL. The payload is platform-neutral — it contains no Shopify gid://, no poiCustomerId, no platform-specific identifier of any kind. The only identity key is the shopper’s Simply member identity.
{
"event": "user.logged_in",
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"poiId": "6821f55303f86ff040461ef4",
"referrer": "yourstore.com",
"member": {
"id": "12345",
"phone": "0501234567",
"tier": "Gold",
"points": 1500
},
"isNewUser": false,
"issuedAt": "2026-06-21T22:00:00.000Z",
"ssoTicket": "<opaque-base64url-string>"
}
3.1 Field reference
| Field | Type | Description |
|---|---|---|
event | "user.created" | "user.logged_in" | user.created for a brand-new Simply Club member created at this login; user.logged_in for a returning member. |
eventId | string (UUID) | Stable dedupe id for this delivery event. Use this to deduplicate at-least-once delivery (see §5). |
poiId | string | The POI (Point of Interaction) that the shopper authenticated at. |
referrer | string | The Storefront origin (i.e. yourStore.com) |
member.id | string | number | The shopper’s Simply Club member id. |
member.phone | string | The shopper’s phone number (normalized Israeli format, e.g. "0501234567"). |
member.tier | string | undefined | Member tier / group (e.g. "Gold"), if present. |
member.points | number | undefined | Member point balance, if present. |
isNewUser | boolean | true only for brand-new Simply Club members created at this login. |
issuedAt | string (ISO-8601) | Timestamp when the event was emitted. Also echoed in the X-Simply-Timestamp header. |
ssoTicket | string | Opaque single-use SSO ticket (5-minute TTL). Exchange via POST /api/v1/sso/redeem. Never log this value — it is a credential. |
No platform-specific id. The outbound payload deliberately contains no Shopify customer id, WooCommerce user id, or any other platform identifier. Simply’s identity key is phone number only. If you need to link this Simply identity to a platform account, use member.phone as the lookup key on your side.
4. Signature verification
Every delivery includes three HTTP headers:
| Header | Value |
|---|---|
X-Simply-Signature | HMAC-SHA256(signingSecret, rawBodyBytes), base64url-encoded. |
X-Simply-Timestamp | ISO-8601 timestamp from payload.issuedAt. Use for replay-window checks. |
X-Simply-Event-Id | Same as payload.eventId. Convenient for routing/logging without parsing the body. |
4.1 Reference verification snippet (Node.js)
const crypto = require('crypto');
function verifySimplySignature(rawBody, receivedSig, signingSecret) {
// HMAC-SHA256 is computed over the RAW request body bytes, not a re-serialized
// object. Always use the raw body buffer — never JSON.parse then JSON.stringify.
const expected = crypto
.createHmac('sha256', signingSecret)
.update(rawBody) // rawBody must be a Buffer or string with the exact bytes received
.digest('base64url');
return crypto.timingSafeEqual(
Buffer.from(expected, 'utf8'),
Buffer.from(receivedSig, 'utf8'),
);
}
// Express example (requires express.raw() or body-parser rawBody option):
app.post('/simply-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-simply-signature'];
if (!verifySimplySignature(req.body, sig, process.env.SIMPLY_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(req.body.toString());
// ... process payload
res.sendStatus(200);
});
Critical: Compute HMAC over the raw body bytes, not over a re-serialized object. If you call
JSON.parsefirst thenJSON.stringifythe result, key ordering or whitespace may differ, causing verification to fail even for a valid payload.
Tampered-body detection. If the body is modified in transit (by a proxy, a CDN, or an attacker), the HMAC computed server-side will not match. Your verifier should reject any request where verifySimplySignature returns false.
4.2 Replay-window check
The X-Simply-Timestamp header echoes issuedAt. To protect against replay attacks, reject requests where the timestamp is more than N seconds old (5–15 minutes is typical):
const ts = new Date(req.headers['x-simply-timestamp']);
const ageMs = Date.now() - ts.getTime();
if (isNaN(ts) || ageMs > 10 * 60 * 1000) { // reject if > 10 minutes old
return res.status(401).json({ error: 'Timestamp too old' });
}
5. Dedupe via eventId
Simply delivers each event at least once. The delivery worker retries failed deliveries with exponential backoff (up to 5 attempts, up to ~1 hour between retries). Your endpoint may receive the same event more than once.
Required: Deduplicate on eventId (a stable UUID per event, not per retry):
// Idempotent handler example (pseudo-code)
const alreadyProcessed = await db.events.exists({ eventId: payload.eventId });
if (alreadyProcessed) {
return res.sendStatus(200); // acknowledge — already handled
}
await db.events.insert({ eventId: payload.eventId });
// ... handle payload
Your endpoint should always return 2xx for successfully received events (even if you choose to skip processing). Non-2xx responses trigger a retry.
6. The POST /api/v1/sso/redeem flow
The ssoTicket in the payload is an opaque, single-use credential. To exchange it for a verified Simply identity, your backend calls the redeem endpoint:
POST https://dropins.simplyclub.co.il/api/v1/sso/redeem
Authorization: Bearer {keyId}.{secret}
Content-Type: application/json
The Authorization header uses the same Bearer format as the Server-Handoff integration key (keyId.secret — see the Dashboard’s Settings → Integrations tab to create a key). The POI is resolved server-side from the integration key; there is no :poiId URL segment.
Request body:
{ "ticket": "<the ssoTicket value from the payload>" }
Success response (HTTP 200):
{
"success": true,
"data": {
"member": { /* full member object */ },
"poiId": "6821f55303f86ff040461ef4"
},
"error": null
}
Failure response (HTTP 401) — generic, all paths:
{
"success": false,
"data": {
"errorCode": "SSO_REDEEM_INVALID",
"message": "SSO ticket is invalid, expired, or already redeemed",
"friendlyMessage": "The SSO ticket could not be redeemed.",
"friendlyMessageHebrew": "כרטיס ה-SSO אינו תקין, פג תוקפו, או כבר מומש."
},
"error": null
}
6.1 Redeem rules
- Single-use. The ticket is burned on the first successful redeem. A second call with the same ticket returns 401.
- 5-minute TTL. Tickets expire 5 minutes after issuance. Expired tickets return 401.
- POI-bound. The ticket is bound to the POI that issued it. Redeeming a ticket with an integration key from a different POI returns 401.
- Generic failure. All failure paths (not-found, already-redeemed, expired, wrong-POI, missing-ticket) return the identical 401 body. This prevents information disclosure — callers cannot probe which failure reason applies.
6.2 Timing the redeem call
The delivery worker sends the webhook and your handler should redeem the ticket promptly. The 5-minute TTL is generous for normal operation, but if your webhook handler queues the event for later processing, ensure the dequeued handler runs within 5 minutes of the issuedAt timestamp.
Node.js example:
async function redeemSsoTicket(ticket) {
const res = await fetch(
'https://dropins.simplyclub.co.il/api/v1/sso/redeem',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SIMPLY_KEY_ID}.${process.env.SIMPLY_KEY_SECRET}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ ticket }),
}
);
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(`SSO redeem failed: ${body?.data?.errorCode ?? res.status}`);
}
const { data } = await res.json();
return data; // { member, poiId }
}
7. The “last inch” is your adapter’s responsibility
Simply provides:
- A verified Simply identity (
member.id,member.phone,member.tier,member.points,isNewUser). - A single-use SSO ticket that lets your backend confirm the identity with a server-to-server call.
Simply does not provide:
- Shopify Multipass token generation.
- WordPress
wp_set_auth_cookiecalls. - OIDC / OAuth token issuance.
- Webhook-to-browser correlation (getting the storefront session into the shopper’s browser after your backend receives a server-side webhook).
- Customer account creation on your platform.
These “last inch” steps are your adapter’s job. The outbound login event gives you a trusted, verified identity to act on — how you materialize a session on your platform is platform-specific and intentionally out of Simply’s scope.
Webhook-to-browser correlation pattern (common approach):
- Your webhook handler receives the event and redeems the ticket → gets the Simply member identity.
- Your handler mints a short-lived one-time token keyed to the
eventId(or a session correlator you embedded in thepoiIdnamespace). - The shopper’s browser polls your backend for that token (or you push it via WebSocket / SSE) and uses it to initiate a platform login.
The specifics depend on your platform and architecture. Simply provides the trusted identity signal; the wiring to your storefront’s session layer is yours.
8. Delivery guarantees and failure handling
The Simply delivery worker uses exponential backoff with up to 5 attempts:
| Attempt | Delay before retry |
|---|---|
| 1st failure | ~5 seconds |
| 2nd failure | ~10 seconds |
| 3rd failure | ~20 seconds |
| 4th failure | ~40 seconds |
| 5th failure (final) | Marked as permanently failed — no further retries |
A permanently failed delivery is recorded for operator review. Your webhook endpoint should return 2xx promptly (before any slow processing) and handle the work asynchronously to avoid accidental non-2xx responses due to processing timeouts.
Delivery timeout: Each attempt has a 10-second HTTP timeout. Your endpoint must respond within 10 seconds.
9. Setup checklist
- Entered an
https://URL in Dashboard → Webhooks → Outbound Webhook. - Copied the signing secret into your backend’s secret store before closing the reveal modal.
- Toggled the outbound webhook Enabled.
- Backend verifier computes HMAC over the raw body bytes (not re-serialized JSON).
- Implement
eventId-based deduplification for at-least-once delivery. - Redeem the
ssoTicketwithin 5 minutes ofissuedAtviaPOST /api/v1/sso/redeem. - Endpoint returns 2xx promptly; heavy processing is async.
- Understand the “last inch” is your platform adapter’s job (§7).