8.1 Rewards block (renderRewardsBlock)
Shows: Current credit balance, VIP tier badge (Silver/Gold), estimated earn for a product price, unused Merly coupon codes.
await sdk.renderRewardsBlock('#merly-block', {
productPrice: 79.0, // optional — enables “Earn +X credits”
currency: 'USD', // ISO 4217, default USD
});
Underlying HTTP:
GET {apiBase}/storefront/rewards-block?shop_domain={storeKey}&shopify_customer_id={id}
&product_price=79¤cy=USD
Or with guest session:
GET {apiBase}/storefront/rewards-block?mr_id={uuid}
Example response (200):
{
"enabled": true,
"balance": 12500,
"earn_rate_bps": 100,
"estimated_earn": 790,
"estimated_earn_boosted": 1185,
"plan_multiplier": 1.5,
"plan_tier": "SILVER",
"coupons": [
{
"code": "MERLY-AB12CD",
"discount_amount": "10.00",
"currency_code": "USD"
}
]
}
Disabled example:
{ "enabled": false, "reason": "no_customer_context" }
Reasons: no_customer_context, session_expired, shopify_customer_not_linked.
8.2 Nudges (renderNudges)
Shows: Merchant-configured prompts (balance reminder, expiring credits, etc.) + optional active campaign banner (platform-wide earn multiplier).
await sdk.renderNudges('#merly-nudges', 'cart');
HTTP:
GET {apiBase}/storefront/nudges?{customerQuery}&page_context=cart
Example response:
{
"nudges": [
{
"id": "balance_reminder",
"type": "balance_reminder",
"title": "You have 12,500 Merly credits",
"body": "Redeem on your next order or explore the network directory.",
"cta_label": "View rewards",
"cta_url": "/pages/rewards",
"dismissible": true,
"priority": 10
}
],
"active_campaign": {
"id": "camp_…",
"name": "Spring 2× earn",
"multiplier": 2,
"ends_at": "2026-07-01T00:00:00.000Z"
}
}
Dismissed nudges are hidden for 60 minutes via sessionStorage (per nudge id).
---
8.3 Loyalty hub (renderLoyaltyHub)
Shows: Balance, subscription tier, ways to earn (read-only platform rules), recent earn activity, links to Customer PWA wallet / Apple & Google Wallet passes, network directory.
await sdk.renderLoyaltyHub('#loyalty-page');
HTTP:
GET {apiBase}/storefront/loyalty-hub?{customerQuery}
Example response (abbreviated):
{
"balance": "12500.0000",
"subscription_tier": {
"plan_tier": "SILVER",
"label": "Silver",
"multiplier": 1.5,
"next_plan_tier": "GOLD",
"next_tier_label": "Gold",
"upgrade_cta_url": "https://app.merly.com/subscription"
},
"earn_methods": [
{ "rule_type": "purchase", "label": "Earn 1% back in Merly credits on every purchase" },
{ "rule_type": "birthday", "label": "5000 credits — Birthday bonus" }
],
"recent_activity": [
{ "type": "EARN", "points": "250", "label": "Purchase rewards", "at": "2026-06-15T12:00:00.000Z" }
],
"redeem": {
"pwa_wallet_url": "https://app.merly.com/wallet",
"pass_apple_url": "https://app.merly.com/wallet?pass=apple",
"pass_google_url": "https://app.merly.com/wallet?pass=google",
"cart_redeem_available": true
},
"directory_cta_url": "https://app.merly.com/directory"
}
---
8.4 Launcher (renderLauncher)
Floating action button (bottom-right) that opens a panel with loyalty hub content. Nudge CTAs call Merly.openLauncher() by default.
await sdk.renderLauncher('#merly-launcher');
// Later:
Merly.openLauncher();
Merly.closeLauncher();
---
8.5 Cart redeem (renderCartRedeem) — headless integration
This is the most important custom integration surface.
Flow:
sequenceDiagram
participant Browser as Your storefront
participant SDK as Merly SDK
participant API as Merly integration
participant Ledger as Merly ledger
participant Cart as Your cart/checkout
Browser->>SDK: renderCartRedeem(subtotal, currency)
SDK->>API: GET /storefront/cart-redemption-quote
API-->>SDK: max_credits_applicable, discount_amount_shop
Browser->>SDK: shopper clicks Apply
SDK->>API: POST /storefront/cart-redemption-apply
API->>Ledger: REDEEM credits
API-->>SDK: coupon_code
SDK->>Cart: onCouponApplied(coupon_code)
Cart->>Cart: apply discount in your system
Render UI:
await sdk.renderCartRedeem('#merly-redeem', {
cartSubtotal: '199.50', // string, decimal shop currency
currency: 'USD',
storeId: undefined, // optional catalog store override
appliedCouponCodes: 'SAVE10', // optional — Merly adjusts max if Merly codes already applied
});
Quote HTTP:
GET {apiBase}/storefront/cart-redemption-quote?{customerQuery}
&cart_subtotal=199.50¤cy=USD
Quote response (eligible):
{
"eligible": true,
"wallet_balance": "12500.0000",
"max_credits_applicable": "9950",
"discount_amount_shop": "99.50",
"currency_code": "USD",
"cap_band_max_shop": "50.00",
"message": "Apply 9950 credits for $99.50 off"
}
Apply HTTP:
POST {apiBase}/storefront/cart-redemption-apply
Content-Type: application/json
x-idempotency-key: headless-cart-{unique}
{
"shop_domain": "boutique.example.com",
"shopify_customer_id": "42",
"cart_subtotal": "199.50",
"currency": "USD",
"credits_amount": "9950",
"platform": "woocommerce"
}
Use "platform": "shopify" only for native Shopify Online Store cart flows. Custom/headless sites typically use "woocommerce" when the merchant catalog connection is WooCommerce, or as advised by Merly onboarding.
Apply response (success):
{
"coupon_code": "MERLY-X7K9M2",
"credits_redeemed": "9950",
"discount_amount_shop": "99.50",
"currency_code": "USD",
"idempotency_replayed": false
}
Your responsibility: Implement onCouponApplied to inject coupon_code into your cart API (session discount, promo code field, order draft, etc.). The SDK does not know your cart system.
Merly.init({
apiBase: API_BASE,
storeKey: STORE_KEY,
externalCustomerId: customer.id,
onCouponApplied: async (code, credits) => {
const res = await fetch('/api/cart/apply-coupon', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
if (!res.ok) throw new Error('Cart rejected Merly coupon');
await refreshCartTotals();
},
});
Idempotency: Each apply sends x-idempotency-key (auto-generated). Retries with the same key return the same coupon without double-redeeming (idempotency_replayed: true).
Common errors:
| 401 | `session_expired` | `mr_id` invalid/expired |
| 401 | `shopify_customer_not_linked` | Customer not in Merly Identity |
| 404 | `store_not_connected` | `storeKey` not in Merly catalog |
| 400 | `insufficient_balance` | Ledger rejected redeem |
| 503 | `fx_rate_missing_for_currency` | Unsupported currency for FX table |
---