{"openapi":"3.1.0","info":{"title":"LoyalTribe Public API","version":"1.0.0","summary":"Build your own loyalty widget on top of LoyalTribe.","description":"# Introduction\n\nThe **LoyalTribe Public API** lets you build a fully custom loyalty experience —\nyour own widget, your own design, your own stack — on top of the LoyalTribe\nrewards platform. Everything the official widget can do is available here:\n\n- Show your loyalty **program** — branding, tiers, earning rules and the reward catalog\n- Authenticate **customers** securely without exposing any secrets in the browser\n- Read a customer's **points balance**, tier, history and coupons\n- **Redeem** rewards and receive discount codes / store credit in real time\n\nAll endpoints live under:\n\n```\n/api/public/v1\n```\n\nResponses are JSON. All timestamps are RFC 3339 / ISO 8601 in UTC.\n\n# Authentication\n\nThe API uses two credentials, issued per shop from the LoyalTribe admin\n(ask your account manager, or your platform administrator, for a key pair):\n\n| Credential | Looks like | Where it lives | What it can do |\n|---|---|---|---|\n| **Publishable key** | `pk_live_…` | Browser / storefront JS — safe to expose | Read public program data, exchange signed identities for customer tokens |\n| **Secret key** | `sk_live_…` | **Your server only** — never in a browser | Sign customer identities (HMAC-SHA256) |\n\nEvery request must carry the publishable key:\n\n```http\nX-API-Key: pk_live_4f6a09...\n```\n\nCustomer endpoints (`/customer/*`) additionally require a **customer token**:\n\n```http\nAuthorization: Bearer eyJhbGciOi...\n```\n\n> ⚠️ **Never ship your secret key to a browser, mobile app, or repository.**\n> If a secret leaks, revoke the key in the dashboard immediately — revocation\n> is instant.\n\n# Customer authentication flow\n\nCustomer identity is asserted by **your server**, the only party holding the\nsecret key. The flow has three steps:\n\n```\nYour server                     Storefront widget                LoyalTribe API\n───────────                     ─────────────────                ──────────────\n1. customer logs in\n   sign identity with sk_… ──▶  2. POST /auth/customer-token ──▶ verify HMAC\n                                   (pk_… + signed identity)      look up customer\n                                3. Bearer <token>            ◀── short-lived JWT (1h)\n                                   on every /customer/* call\n```\n\n### Step 1 — sign the identity (server-side)\n\nBuild the canonical message and sign it with your **secret key** using\nHMAC-SHA256, hex-encoded:\n\n```\nmessage   = \"{customer_id}:{email}:{timestamp}\"\nsignature = hex( HMAC_SHA256( secret_key, message ) )\n```\n\n- `customer_id` — the platform customer id (e.g. the Shopify customer ID). May be empty if you identify by email.\n- `email` — the customer's email. May be empty if you identify by customer_id.\n- `timestamp` — current unix time in **seconds**. Signatures expire after **5 minutes**.\n- Empty fields stay empty in the message — e.g. identifying by email only:\n  `\":jane@example.com:1718200000\"`.\n\n**Node.js**\n\n```js\nconst crypto = require(\"crypto\");\n\nfunction signCustomer(secretKey, customerId, email) {\n  const timestamp = Math.floor(Date.now() / 1000);\n  const message = `${customerId}:${email}:${timestamp}`;\n  const signature = crypto\n    .createHmac(\"sha256\", secretKey)\n    .update(message)\n    .digest(\"hex\");\n  return { customer_id: customerId, email, timestamp, signature };\n}\n```\n\n**PHP**\n\n```php\nfunction signCustomer(string $secretKey, string $customerId, string $email): array {\n  $timestamp = time();\n  $message   = \"{$customerId}:{$email}:{$timestamp}\";\n  $signature = hash_hmac('sha256', $message, $secretKey);\n  return [\n    'customer_id' => $customerId,\n    'email'       => $email,\n    'timestamp'   => $timestamp,\n    'signature'   => $signature,\n  ];\n}\n```\n\n**Python**\n\n```python\nimport hmac, hashlib, time\n\ndef sign_customer(secret_key: str, customer_id: str, email: str) -> dict:\n    timestamp = int(time.time())\n    message = f\"{customer_id}:{email}:{timestamp}\"\n    signature = hmac.new(\n        secret_key.encode(), message.encode(), hashlib.sha256\n    ).hexdigest()\n    return {\n        \"customer_id\": customer_id,\n        \"email\": email,\n        \"timestamp\": timestamp,\n        \"signature\": signature,\n    }\n```\n\nRender the resulting object into the storefront page for the logged-in\ncustomer (e.g. a `<script>window.LOYALTY_IDENTITY = {...}</script>` block,\nor an endpoint on your own backend the widget can call).\n\n### Step 2 — exchange it for a customer token (browser)\n\nThe widget posts the signed identity to\n[`POST /auth/customer-token`](#tag/customer-authentication/post/auth/customer-token)\nwith the publishable key and receives a JWT valid for **1 hour**.\n\n### Step 3 — call customer endpoints\n\nSend the token as `Authorization: Bearer <token>` on every `/customer/*`\nrequest. When it expires (`401 invalid_token`), exchange a fresh signed\nidentity — your server signs a new one on the next page load.\n\n# Platform-agnostic integration (any commerce stack)\n\nLoyalTribe is not Shopify-only. A merchant on **any platform** — a custom\nstorefront, WooCommerce, a POS — integrates with three server-to-server\nendpoints, authenticated with the **secret key**\n(`Authorization: Bearer sk_live_…`, never from a browser):\n\n| Step | Endpoint | When |\n|---|---|---|\n| Enroll customers | `POST /customers` | At registration / opt-in |\n| Earn points | `POST /orders` | When an order is paid |\n| Honor coupons | `POST /coupons/validate` + `POST /coupons/consume` | At your checkout |\n\nThe flow end to end:\n\n1. Your backend enrolls the customer (`POST /customers`).\n2. On every paid order, your backend posts it (`POST /orders`) — the rules\n   engine awards points asynchronously (typically within a second), with\n   full dedup per `order_id` (safe to retry).\n3. Customers browse and redeem rewards through the same widget endpoints as\n   Shopify shops. On non-Shopify platforms a redemption mints an **internal\n   coupon code** instead of a platform discount.\n4. At checkout, your backend calls `POST /coupons/validate` to apply the\n   discount, then `POST /coupons/consume` once the order is placed.\n   Consumption is atomic — a code can never be used twice.\n\nNot yet available on non-Shopify platforms: store-credit rewards and\nrule-awarded vouchers (rules that *grant* points work everywhere).\n\n# Rate limits\n\nLimits are applied **per API key**:\n\n| Scope | Limit |\n|---|---|\n| All endpoints | 120 requests / minute |\n| `POST /auth/customer-token` | 30 requests / minute |\n| `POST /customer/redeem` | 20 requests / minute |\n\nExceeding a limit returns `429` with code `rate_limited`. Back off and retry.\n\n# Errors\n\nErrors use a consistent envelope with a stable machine-readable `code`:\n\n```json\n{\n  \"error\": {\n    \"code\": \"invalid_signature\",\n    \"message\": \"Signature verification failed. Sign \\\"{customer_id}:{email}:{timestamp}\\\" with your secret key using HMAC-SHA256 (hex).\"\n  }\n}\n```\n\n| HTTP | Code | Meaning |\n|---|---|---|\n| 400 | `invalid_request` | Malformed body or missing required fields |\n| 401 | `missing_api_key` / `invalid_api_key` | No / unknown / revoked publishable key |\n| 401 | `missing_token` / `invalid_token` | No / expired customer token |\n| 401 | `signature_expired` / `invalid_signature` | Identity signature too old or wrong |\n| 403 | `merchant_mismatch` | Token was issued for a different shop's key |\n| 403 | `customer_not_enrolled` | Customer exists but hasn't joined the program |\n| 404 | `customer_not_found` | No matching loyalty customer |\n| 429 | `rate_limited` | Per-key rate limit exceeded |\n\n> Some endpoints reused from the core platform (redeem, redemptions,\n> dashboard) return their legacy shapes — e.g. `{\"error\": \"...\"}` as a plain\n> string. Always handle both forms.\n\n# Versioning\n\nThe API is versioned in the path (`/api/public/v1`). Backwards-compatible\nadditions (new fields, new endpoints) happen without notice — build your\nparser to ignore unknown fields. Breaking changes ship as `/v2`.\n","contact":{"name":"LoyalTribe Developer Support","email":"projects@ssdc.co"},"x-logo":{"url":"/assets/logo.png","altText":"LoyalTribe"}},"servers":[{"url":"https://api.staging.loyaltriberewards.com/api/public/v1","description":"Staging"},{"url":"https://api.loyaltriberewards.com/api/public/v1","description":"Production"}],"tags":[{"name":"Program","description":"Public, read-only data about the shop's loyalty program — everything a\nwidget needs to render before any customer signs in. Requires only the\npublishable key.\n"},{"name":"Customer Authentication","description":"Exchange a merchant-signed customer identity for a short-lived customer\ntoken. See the **Customer authentication flow** section above for the\nsigning recipe.\n"},{"name":"Gamification","description":"Spin-the-wheel and scratch-card mechanics. Available only when the\nmerchant has the corresponding feature enabled (`SPIN_WHEEL` /\n`VOUCHER_SCRATCH`) — otherwise these endpoints return\n`403 feature_disabled`-style errors. All of them require the\npublishable key **and** a customer token.\n\n**Implementing a wheel:** fetch the config (`GET\n/customer/gamification/wheel`) and render the `segments` in\n`sortOrder` with their `label`/`color` (the merchant may also supply\n`backgroundImageUrl`/`pointerImageUrl` art). `status.canPlay` tells\nyou whether to enable the spin button — show `status.reason`\n(`cooldown`, `daily_limit`, `needs_purchase`, `already_played`)\notherwise. On spin, the **server** picks the prize: animate the wheel\nto the returned `segment`, then show the prize from `play`\n(points, a discount code, store credit, or `no_win`).\n\n**Implementing scratch cards:** list the cards, render\n`coverImageUrl`, call play on scratch, reveal the prize from the\nresponse. A card's last reveal is included so you can keep showing\nan already-scratched prize across reloads.\n"},{"name":"Server-to-Server","description":"Trusted endpoints for the merchant's backend, authenticated with the\n**secret key** (`Authorization: Bearer sk_live_…`). This is how any\nplatform — custom storefront, WooCommerce, POS — enrolls customers,\npushes paid orders into the points engine, and honors internally minted\ncoupon codes at checkout.\n"},{"name":"Customer","description":"Endpoints acting on behalf of one authenticated customer. All of them\nrequire the publishable key **and** a `Bearer` customer token.\n"}],"security":[{"ApiKeyAuth":[]}],"paths":{"/program":{"get":{"operationId":"getProgram","tags":["Program"],"summary":"Get program configuration","description":"Branding, copy, FAQ and currency for the shop's loyalty program.\nUse it to render the widget shell — title, intro text, accent color —\nbefore the customer authenticates.\n","x-codeSamples":[{"lang":"cURL","source":"curl https://api.staging.loyaltriberewards.com/api/public/v1/program \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\"\n"},{"lang":"JavaScript","source":"const res = await fetch(`${BASE_URL}/program`, {\n  headers: { \"X-API-Key\": \"pk_live_YOUR_KEY\" },\n});\nconst { program } = await res.json();\n"}],"responses":{"200":{"description":"Program configuration","content":{"application/json":{"schema":{"type":"object","properties":{"program":{"$ref":"#/components/schemas/Program"}}},"example":{"program":{"shopDomain":"my-store.myshopify.com","currency":"IDR","widgetTitle":"My Store Rewards","widgetIntroduction":"Earn points on every purchase.","joinLoyaltyLabel":"Join now","joinLoyaltyDescription":"Become a member and start earning.","colorAccent":"#e91e63","faq":[{"question":"How do I earn points?","answer":"You earn points automatically on every paid order."}],"autoEnrollLoyalty":true,"storeCreditEnabled":false}}}}},"401":{"$ref":"#/components/responses/UnauthorizedKey"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/program/tiers":{"get":{"operationId":"listProgramTiers","tags":["Program"],"summary":"List tiers","description":"Active membership tiers, ordered from lowest to highest spend threshold.","x-codeSamples":[{"lang":"cURL","source":"curl https://api.staging.loyaltriberewards.com/api/public/v1/program/tiers \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\"\n"}],"responses":{"200":{"description":"Tier list","content":{"application/json":{"schema":{"type":"object","properties":{"tiers":{"type":"array","items":{"$ref":"#/components/schemas/Tier"}}}},"example":{"tiers":[{"id":1,"name":"Bronze","minSpend":0,"pointMultiplier":1,"benefits":"Earn 1x points","sortOrder":0},{"id":2,"name":"Gold","minSpend":5000000,"pointMultiplier":2,"benefits":"Earn 2x points, birthday gift","sortOrder":1}]}}}},"401":{"$ref":"#/components/responses/UnauthorizedKey"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/program/rewards":{"get":{"operationId":"listProgramRewards","tags":["Program"],"summary":"List reward catalog","description":"Active rewards customers can redeem points for, cheapest first.\n\n`rewardType` values:\n\n| Type | Meaning |\n|---|---|\n| `discount_coupon` | A discount code (see `discountType`, `discountValue`, `discountSubType`) |\n| `store_credit` | Fixed store credit (`storeCreditAmount`) |\n| `points_conversion` | Flexible: customer chooses how many points to convert (`pointsPerDollar`, `minRedeemPoints`, `maxRedeemPoints`) |\n\nIf `tierIds` is non-empty, the reward is restricted to customers in\nthose tiers.\n","x-codeSamples":[{"lang":"cURL","source":"curl https://api.staging.loyaltriberewards.com/api/public/v1/program/rewards \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\"\n"}],"responses":{"200":{"description":"Reward catalog","content":{"application/json":{"schema":{"type":"object","properties":{"rewards":{"type":"array","items":{"$ref":"#/components/schemas/Reward"}}}},"example":{"rewards":[{"id":12,"name":"Rp 50.000 off","description":"Get Rp 50.000 off your next order.","voucherDescription":"Valid for 30 days. One use per customer.","rewardType":"discount_coupon","pointsCost":500,"tierIds":[],"discountType":"amount","discountValue":50000,"discountSubType":"order","minPurchaseAmount":200000,"startsAt":null,"endsAt":null}]}}}},"401":{"$ref":"#/components/responses/UnauthorizedKey"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/program/rules":{"get":{"operationId":"listProgramRules","tags":["Program"],"summary":"List earning rules","description":"Active earning rules — how customers earn points. Use it to render a\n\"Ways to earn\" section. Common `type` values: `pointsPerSpend`,\n`firstPurchaseBonus`, `birthdayBonus`, `anniversaryBonus`,\n`referralBonus`, `reviewPoints`, `completeProfileBonus`,\n`newsletterSignupBonus`.\n","x-codeSamples":[{"lang":"cURL","source":"curl https://api.staging.loyaltriberewards.com/api/public/v1/program/rules \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\"\n"}],"responses":{"200":{"description":"Earning rules","content":{"application/json":{"schema":{"type":"object","properties":{"rules":{"type":"array","items":{"$ref":"#/components/schemas/Rule"}}}},"example":{"rules":[{"id":3,"name":"Points per purchase","type":"pointsPerSpend","rewardType":"points","pointsPerSpend":1,"bonusPoints":null,"pointMultiplier":null,"minPurchase":10000,"startDate":null,"endDate":null,"pointLifetimeDays":365}]}}}},"401":{"$ref":"#/components/responses/UnauthorizedKey"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/auth/customer-token":{"post":{"operationId":"issueCustomerToken","tags":["Customer Authentication"],"summary":"Exchange a signed identity for a customer token","description":"Exchanges a **merchant-signed customer identity** for a short-lived\ncustomer JWT (1 hour). The signature must be produced **on your\nserver** with your secret key — see the *Customer authentication flow*\nguide at the top of this reference.\n\n```\nmessage   = \"{customer_id}:{email}:{timestamp}\"   // empty string for omitted fields\nsignature = hex( HMAC_SHA256( secret_key, message ) )\n```\n\nSignatures are valid for **5 minutes** after `timestamp`.\n","x-codeSamples":[{"lang":"cURL","source":"# signature generated server-side — see the signing guide\ncurl -X POST https://api.staging.loyaltriberewards.com/api/public/v1/auth/customer-token \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"customer_id\": \"7234567890123\",\n    \"email\": \"jane@example.com\",\n    \"timestamp\": 1718200000,\n    \"signature\": \"3f1c4e0a9b...\"\n  }'\n"},{"lang":"JavaScript","source":"// window.LOYALTY_IDENTITY was rendered by YOUR server (it holds\n// customer_id, email, timestamp, signature — never the secret key)\nconst res = await fetch(`${BASE_URL}/auth/customer-token`, {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"pk_live_YOUR_KEY\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify(window.LOYALTY_IDENTITY),\n});\nconst { token, expiresAt } = await res.json();\n"}],"security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["timestamp","signature"],"properties":{"customer_id":{"type":"string","description":"Platform customer id (e.g. Shopify customer ID). Provide this, email, or both.","example":"7234567890123"},"email":{"type":"string","format":"email","description":"Customer email. Provide this, customer_id, or both.","example":"jane@example.com"},"timestamp":{"type":"integer","description":"Unix time in seconds when the identity was signed. Valid for 5 minutes.","example":1718200000},"signature":{"type":"string","description":"Hex-encoded HMAC-SHA256 of `\"{customer_id}:{email}:{timestamp}\"` using your secret key.","example":"3f1c4e0a9b2d8c7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e"}}}}}},"responses":{"200":{"description":"Customer token issued","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","description":"JWT — send as `Authorization Bearer` on `/customer/*` endpoints."},"tokenType":{"type":"string","example":"Bearer"},"expiresAt":{"type":"string","format":"date-time"},"expiresIn":{"type":"integer","description":"Seconds until expiry.","example":3600},"customer":{"type":"object","properties":{"id":{"type":"string","example":"7234567890123"},"email":{"type":"string","example":"jane@example.com"},"firstName":{"type":"string","example":"Jane"},"lastName":{"type":"string","example":"Doe"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"Invalid or expired signature, or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_signature","message":"Signature verification failed. Sign \"{customer_id}:{email}:{timestamp}\" with your secret key using HMAC-SHA256 (hex)."}}}}},"403":{"description":"Customer not enrolled in the loyalty program","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"customer_not_enrolled","message":"This customer exists but is not enrolled in the loyalty program."}}}}},"404":{"description":"No matching customer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"customer_not_found","message":"No loyalty customer matches this identity for your shop."}}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/customer/me":{"get":{"operationId":"getCustomerMe","tags":["Customer"],"summary":"Get the authenticated customer","description":"Profile, live points balance, tier and referral code in one call —\ntypically the first request after the token exchange.\n","security":[{"ApiKeyAuth":[],"CustomerToken":[]}],"x-codeSamples":[{"lang":"cURL","source":"curl https://api.staging.loyaltriberewards.com/api/public/v1/customer/me \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\" \\\n  -H \"Authorization: Bearer CUSTOMER_TOKEN\"\n"}],"responses":{"200":{"description":"The authenticated customer","content":{"application/json":{"schema":{"type":"object","properties":{"customer":{"$ref":"#/components/schemas/Customer"}}},"example":{"customer":{"id":"7234567890123","email":"jane@example.com","firstName":"Jane","lastName":"Doe","phone":"+628123456789","birthday":"1995-04-12","gender":"female","enrolled":true,"points":1250,"tier":{"id":2,"name":"Gold","pointMultiplier":2,"benefits":"Earn 2x points, birthday gift"},"referralCode":"JANE-X7K2","memberSince":"2024-11-02T08:15:00Z"}}}}},"401":{"$ref":"#/components/responses/UnauthorizedToken"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/customer/balance":{"get":{"operationId":"getCustomerBalance","tags":["Customer"],"summary":"Get points balance","description":"Lightweight balance check — current available points and the shop currency.","security":[{"ApiKeyAuth":[],"CustomerToken":[]}],"x-codeSamples":[{"lang":"cURL","source":"curl https://api.staging.loyaltriberewards.com/api/public/v1/customer/balance \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\" \\\n  -H \"Authorization: Bearer CUSTOMER_TOKEN\"\n"}],"responses":{"200":{"description":"Current balance","content":{"application/json":{"schema":{"type":"object","properties":{"totalPoints":{"type":"integer","example":1250},"currency":{"type":"string","example":"IDR"}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedToken"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/customer/dashboard":{"get":{"operationId":"getCustomerDashboard","tags":["Customer"],"summary":"Get dashboard summary","description":"Aggregate view used by the official widget's home screen: total points,\ntier, the 10 most recent history entries, and points expiring in the\nnext 30 days.\n","security":[{"ApiKeyAuth":[],"CustomerToken":[]}],"x-codeSamples":[{"lang":"cURL","source":"curl https://api.staging.loyaltriberewards.com/api/public/v1/customer/dashboard \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\" \\\n  -H \"Authorization: Bearer CUSTOMER_TOKEN\"\n"}],"responses":{"200":{"description":"Dashboard summary","content":{"application/json":{"schema":{"type":"object","properties":{"totalPoints":{"type":"integer","example":1250},"tier":{"type":["object","null"],"properties":{"id":{"type":"integer"},"name":{"type":"string"},"pointMultiplier":{"type":"number"},"benefits":{"type":"string"}}},"histories":{"type":"array","description":"10 most recent point-history entries.","items":{"$ref":"#/components/schemas/HistoryEntry"}},"expiringPoints":{"type":"integer","description":"Points expiring within 30 days.","example":200},"expiryDate":{"type":["string","null"],"description":"Earliest upcoming expiry date (YYYY-MM-DD), or null.","example":"2026-07-01"}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedToken"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/customer/history":{"get":{"operationId":"listCustomerHistory","tags":["Customer"],"summary":"List point history","description":"Full point history for the customer, newest first, paginated.","security":[{"ApiKeyAuth":[],"CustomerToken":[]}],"parameters":[{"name":"page","in":"query","description":"1-based page number.","schema":{"type":"integer","default":1,"minimum":1}},{"name":"limit","in":"query","description":"Items per page (max 100).","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}}],"x-codeSamples":[{"lang":"cURL","source":"curl \"https://api.staging.loyaltriberewards.com/api/public/v1/customer/history?page=1&limit=20\" \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\" \\\n  -H \"Authorization: Bearer CUSTOMER_TOKEN\"\n"}],"responses":{"200":{"description":"Paginated point history","content":{"application/json":{"schema":{"type":"object","properties":{"history":{"type":"array","items":{"$ref":"#/components/schemas/HistoryEntry"}},"pagination":{"type":"object","properties":{"page":{"type":"integer","example":1},"limit":{"type":"integer","example":20},"total":{"type":"integer","example":57},"totalPages":{"type":"integer","example":3}}}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedToken"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/customer/redemptions":{"get":{"operationId":"listCustomerRedemptions","tags":["Customer"],"summary":"List active coupons & redemptions","description":"The customer's unused coupons: point redemptions plus vouchers awarded\nby earning rules (birthday, referral, …). Each item carries enough\ncontext to render a coupon card — name, type, discount code, targeted\nproducts/collections.\n","security":[{"ApiKeyAuth":[],"CustomerToken":[]}],"x-codeSamples":[{"lang":"cURL","source":"curl https://api.staging.loyaltriberewards.com/api/public/v1/customer/redemptions \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\" \\\n  -H \"Authorization: Bearer CUSTOMER_TOKEN\"\n"}],"responses":{"200":{"description":"Unused coupons and redemptions","content":{"application/json":{"schema":{"type":"object","properties":{"redemptions":{"type":"array","items":{"$ref":"#/components/schemas/Redemption"}}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedToken"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/customer/redeem":{"post":{"operationId":"redeemReward","tags":["Customer"],"summary":"Redeem a reward","description":"Spends the customer's points on a reward from the catalog\n([`GET /program/rewards`](#tag/program/get/program/rewards)).\n\n- For `points_conversion` rewards, pass `pointsToRedeem` (between the\n  reward's `minRedeemPoints` and `maxRedeemPoints`).\n- For product discounts with `applyMode: \"customer_picks\"`, pass\n  `selectedProductId` — the product the customer chose.\n\nRedemption is atomic and double-spend-safe: concurrent requests for the\nsame customer are serialized server-side.\n","security":[{"ApiKeyAuth":[],"CustomerToken":[]}],"x-codeSamples":[{"lang":"cURL","source":"curl -X POST https://api.staging.loyaltriberewards.com/api/public/v1/customer/redeem \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\" \\\n  -H \"Authorization: Bearer CUSTOMER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"rewardId\": 12}'\n"},{"lang":"JavaScript","source":"const res = await fetch(`${BASE_URL}/customer/redeem`, {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"pk_live_YOUR_KEY\",\n    \"Authorization\": `Bearer ${customerToken}`,\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({ rewardId: 12 }),\n});\nconst result = await res.json();\nif (result.discountCode) showCoupon(result.discountCode);\n"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["rewardId"],"properties":{"rewardId":{"type":"integer","description":"ID of the reward to redeem.","example":12},"pointsToRedeem":{"type":"integer","description":"Required for `points_conversion` rewards — how many points to convert.","example":1000},"selectedProductId":{"type":"string","description":"Required when the reward's `applyMode` is `customer_picks` — the chosen product GID.","example":"gid://shopify/Product/8123456789"}}}}}},"responses":{"200":{"description":"Reward redeemed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"Your discount code is ready!"},"pointsUsed":{"type":"integer","example":500},"rewardName":{"type":"string","example":"Rp 50.000 off"},"discountCode":{"type":"string","description":"Present for discount-coupon rewards.","example":"LT-X7K2-9QPM"},"isAutoApplied":{"type":"boolean","description":"True when the discount applies automatically at checkout (no code entry needed)."},"storeCredit":{"type":"object","description":"Present for store-credit rewards.","properties":{"amount":{"type":"number","example":50000},"currencyCode":{"type":"string","example":"IDR"},"balanceAfter":{"type":"number","example":150000},"transactionId":{"type":"string"}}}}}}}},"400":{"description":"Insufficient points, invalid reward, or missing fields (legacy `{\"error\": \"…\"}` shape)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"insufficient points"}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedToken"},"404":{"description":"Reward not found or not active","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"reward not found"}}}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/customer/gamification/wheel":{"get":{"operationId":"getCustomerWheel","tags":["Gamification"],"summary":"Get the spin wheel","description":"The merchant's active wheel configuration plus this customer's play\neligibility. `wheel` is `null` when no active wheel exists. Segments\nthe customer is not eligible for (tier/segment-restricted prizes) are\nfiltered out server-side.\n","security":[{"ApiKeyAuth":[],"CustomerToken":[]}],"x-codeSamples":[{"lang":"cURL","source":"curl https://api.staging.loyaltriberewards.com/api/public/v1/customer/gamification/wheel \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\" \\\n  -H \"Authorization: Bearer CUSTOMER_TOKEN\"\n"}],"responses":{"200":{"description":"Wheel config + eligibility","content":{"application/json":{"schema":{"type":"object","properties":{"wheel":{"type":["object","null"],"properties":{"id":{"type":"integer"},"name":{"type":"string"},"description":{"type":"string"},"triggerType":{"type":"string"},"playLimitPerDay":{"type":"integer"},"cooldownHours":{"type":"integer"},"spinButtonLabel":{"type":"string"},"backgroundImageUrl":{"type":"string","description":"Optional merchant-designed wheel face."},"pointerImageUrl":{"type":"string"},"segments":{"type":"array","items":{"$ref":"#/components/schemas/WheelSegment"}}}},"status":{"$ref":"#/components/schemas/PlayStatus"}}},"example":{"wheel":{"id":3,"name":"Lucky Spin","triggerType":"manual","playLimitPerDay":1,"cooldownHours":24,"segments":[{"id":11,"label":"100 Points","color":"#f59e0b","sortOrder":0,"rewardName":"100 bonus points","rewardType":"bonus_points"},{"id":12,"label":"Try again","color":"#64748b","sortOrder":1,"rewardType":"no_win"}]},"status":{"canPlay":true,"playsRemaining":1}}}}},"401":{"$ref":"#/components/responses/UnauthorizedToken"},"403":{"description":"SPIN_WHEEL feature not enabled for this shop","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/customer/gamification/wheel/spin":{"post":{"operationId":"spinWheel","tags":["Gamification"],"summary":"Spin the wheel","description":"Performs one spin. The server picks the winning segment and issues the\nprize atomically (points are credited / the coupon is created before\nthe response returns). Animate your wheel to land on the returned\n`segment`, then reveal the prize from `play`.\n","security":[{"ApiKeyAuth":[],"CustomerToken":[]}],"x-codeSamples":[{"lang":"cURL","source":"curl -X POST https://api.staging.loyaltriberewards.com/api/public/v1/customer/gamification/wheel/spin \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\" \\\n  -H \"Authorization: Bearer CUSTOMER_TOKEN\"\n"}],"responses":{"200":{"description":"Spin result","content":{"application/json":{"schema":{"type":"object","properties":{"segment":{"$ref":"#/components/schemas/WheelSegment"},"play":{"$ref":"#/components/schemas/GamificationPlay"}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedToken"},"403":{"description":"Feature disabled, or the customer can't play right now (cooldown / daily limit)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/customer/gamification/scratch/cards":{"get":{"operationId":"listScratchCards","tags":["Gamification"],"summary":"List scratch cards","description":"Active scratch cards for this customer, each with its play\neligibility and (when already played) the last revealed prize.\n","security":[{"ApiKeyAuth":[],"CustomerToken":[]}],"x-codeSamples":[{"lang":"cURL","source":"curl https://api.staging.loyaltriberewards.com/api/public/v1/customer/gamification/scratch/cards \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\" \\\n  -H \"Authorization: Bearer CUSTOMER_TOKEN\"\n"}],"responses":{"200":{"description":"Card list","content":{"application/json":{"schema":{"type":"object","properties":{"cards":{"type":"array","items":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"description":{"type":"string"},"mode":{"type":"string","description":"Prize selection mode (random / fixed)."},"coverImageUrl":{"type":"string","description":"The unscratched foil art."},"revealImageUrl":{"type":"string"},"winMessage":{"type":"string"},"winTone":{"type":"string"},"status":{"$ref":"#/components/schemas/PlayStatus"}}}}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedToken"},"403":{"description":"VOUCHER_SCRATCH feature not enabled for this shop","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/customer/gamification/scratch/cards/{cardId}/play":{"post":{"operationId":"playScratchCard","tags":["Gamification"],"summary":"Play a scratch card","description":"Plays (scratches) the card. The server picks and issues the prize\natomically; reveal it from the returned `play`.\n","security":[{"ApiKeyAuth":[],"CustomerToken":[]}],"parameters":[{"name":"cardId","in":"path","required":true,"schema":{"type":"integer"}}],"x-codeSamples":[{"lang":"cURL","source":"curl -X POST https://api.staging.loyaltriberewards.com/api/public/v1/customer/gamification/scratch/cards/5/play \\\n  -H \"X-API-Key: pk_live_YOUR_KEY\" \\\n  -H \"Authorization: Bearer CUSTOMER_TOKEN\"\n"}],"responses":{"200":{"description":"Play result","content":{"application/json":{"schema":{"type":"object","properties":{"play":{"$ref":"#/components/schemas/GamificationPlay"}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedToken"},"403":{"description":"Feature disabled, or the customer can't play right now","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/customers":{"post":{"operationId":"createCustomer","tags":["Server-to-Server"],"summary":"Enroll a customer","description":"Creates (or updates) a loyalty customer and enrolls them in the\nprogram. Idempotent per `customer_id` — re-posting updates the profile.\nEnrollment triggers the same machinery as every other channel: member\nlimits, welcome email, referral code generation, registration-bonus\nrules and tier evaluation.\n","security":[{"SecretKeyAuth":[]}],"x-codeSamples":[{"lang":"cURL","source":"curl -X POST https://api.staging.loyaltriberewards.com/api/public/v1/customers \\\n  -H \"Authorization: Bearer sk_live_YOUR_SECRET\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"customer_id\": \"cust-1042\",\n    \"email\": \"jane@example.com\",\n    \"first_name\": \"Jane\",\n    \"last_name\": \"Doe\"\n  }'\n"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customer_id"],"properties":{"customer_id":{"type":"string","description":"Your platform's customer identifier. Stable — it keys all loyalty data.","example":"cust-1042"},"email":{"type":"string","format":"email"},"first_name":{"type":"string"},"last_name":{"type":"string"},"phone":{"type":"string"},"enrolled":{"type":"boolean","default":true,"description":"Set false to register without enrolling."},"referral_code":{"type":"string","description":"A referrer's code, if the customer signed up through a referral link."}}}}}},"responses":{"201":{"description":"Customer enrolled","content":{"application/json":{"schema":{"type":"object","properties":{"customer":{"type":"object","properties":{"id":{"type":"string","example":"cust-1042"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"enrolled":{"type":"boolean"},"referralCode":{"type":["string","null"],"example":"JANE-X7K2"}}}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedSecret"},"422":{"description":"Enrollment rejected (e.g. package member limit reached)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/orders":{"post":{"operationId":"ingestOrder","tags":["Server-to-Server"],"summary":"Submit a paid order","description":"Feeds a paid order into the loyalty engine — the same queue and rules\nengine Shopify webhooks use. Points appear in the customer's history\nwithin seconds.\n\n**Idempotent per `order_id`**: re-submissions of the same order are\ndetected and skipped, so retrying on timeouts is safe.\n\nThe customer must already be enrolled (`POST /customers`).\n","security":[{"SecretKeyAuth":[]}],"x-codeSamples":[{"lang":"cURL","source":"curl -X POST https://api.staging.loyaltriberewards.com/api/public/v1/orders \\\n  -H \"Authorization: Bearer sk_live_YOUR_SECRET\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"order_id\": \"INV-1042\",\n    \"order_number\": \"#1042\",\n    \"customer_id\": \"cust-1042\",\n    \"total_amount\": 150000,\n    \"currency\": \"IDR\",\n    \"discount_codes\": [\"750KBB56\"]\n  }'\n"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["order_id","customer_id","total_amount"],"properties":{"order_id":{"type":"string","description":"Your order identifier (numeric, UUID or code — all fine). Dedup key.","example":"INV-1042"},"order_number":{"type":"string","description":"Human-facing order label, shown in point history and notifications.","example":"#1042"},"customer_id":{"type":"string","example":"cust-1042"},"total_amount":{"type":"number","description":"Order grand total in major currency units. Must be > 0.","example":150000},"currency":{"type":"string","example":"IDR"},"financial_status":{"type":"string","default":"paid","description":"Only \"paid\" orders are accepted — submit after payment capture."},"processed_at":{"type":"string","format":"date-time","description":"When the order was paid. Defaults to now."},"line_items":{"type":"array","description":"Cart contents — enables product-specific earning rules.","items":{"type":"object","properties":{"product_id":{"type":"string"},"variant_id":{"type":"string"},"title":{"type":"string"},"quantity":{"type":"integer"},"unit_price":{"type":"number"},"image_url":{"type":"string"}}}},"discount_codes":{"type":"array","items":{"type":"string"},"description":"Coupon codes used on this order — marks matching loyalty coupons as consumed."}}}}}},"responses":{"202":{"description":"Order queued for points processing","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"queued"},"orderId":{"type":"string","example":"INV-1042"},"message":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedSecret"},"404":{"description":"Customer not enrolled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"customer_not_enrolled","message":"No enrolled loyalty customer with this customer_id — enroll them first via POST /customers."}}}}},"422":{"description":"Order not paid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"Queue temporarily unavailable — retry shortly","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/coupons/validate":{"post":{"operationId":"validateCoupon","tags":["Server-to-Server"],"summary":"Validate a coupon code","description":"Checks an internally minted coupon code at your checkout: whose it is,\nwhat discount it grants, and whether it is still unused. Read-only —\ncall `POST /coupons/consume` after the order is placed.\n","security":[{"SecretKeyAuth":[]}],"x-codeSamples":[{"lang":"cURL","source":"curl -X POST https://api.staging.loyaltriberewards.com/api/public/v1/coupons/validate \\\n  -H \"Authorization: Bearer sk_live_YOUR_SECRET\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"code\": \"750KBB56\"}'\n"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string","example":"750KBB56"}}}}}},"responses":{"200":{"description":"Coupon details","content":{"application/json":{"schema":{"type":"object","properties":{"coupon":{"$ref":"#/components/schemas/Coupon"}}},"example":{"coupon":{"code":"750KBB56","valid":true,"used":false,"rewardName":"Rp 20.000 off","rewardType":"discount_coupon","discountType":"amount","discountValue":20000,"discountSubType":"order","customerId":"cust-1042","pointsUsed":100,"redeemedAt":"2026-06-12T15:58:00Z"}}}}},"401":{"$ref":"#/components/responses/UnauthorizedSecret"},"404":{"description":"Unknown code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/coupons/consume":{"post":{"operationId":"consumeCoupon","tags":["Server-to-Server"],"summary":"Consume a coupon code","description":"Atomically marks a coupon as used after the order is placed. A code\ncan never be consumed twice — the second attempt returns `409\ncoupon_already_used`, even across concurrent requests.\n","security":[{"SecretKeyAuth":[]}],"x-codeSamples":[{"lang":"cURL","source":"curl -X POST https://api.staging.loyaltriberewards.com/api/public/v1/coupons/consume \\\n  -H \"Authorization: Bearer sk_live_YOUR_SECRET\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"code\": \"750KBB56\", \"order_id\": \"INV-1042\"}'\n"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string","example":"750KBB56"},"order_id":{"type":"string","description":"Optional — the order this coupon was applied to."}}}}}},"responses":{"200":{"description":"Consumed","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"consumed"},"code":{"type":"string"},"consumedAt":{"type":"string","format":"date-time"}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedSecret"},"404":{"description":"Unknown code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already consumed","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"$ref":"#/components/schemas/Error/properties/error"},"coupon":{"$ref":"#/components/schemas/Coupon"}}}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Your **publishable key** (`pk_live_…`). Identifies your shop. Required\non every request, safe to use in browsers.\n"},"CustomerToken":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Short-lived **customer token** from `POST /auth/customer-token`.\nRequired on all `/customer/*` endpoints.\n"},"SecretKeyAuth":{"type":"http","scheme":"bearer","description":"Your **secret key** (`sk_live_…`) sent as a Bearer token. Server-to-\nserver endpoints only — never call these from a browser.\n"}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Stable machine-readable error code.","example":"invalid_api_key"},"message":{"type":"string","example":"This API key is unknown or has been revoked."}}}}},"WheelSegment":{"type":"object","properties":{"id":{"type":"integer"},"label":{"type":"string","example":"100 Points"},"color":{"type":"string","example":"#f59e0b"},"sortOrder":{"type":"integer"},"winMessage":{"type":"string"},"winTone":{"type":"string"},"rewardName":{"type":"string","description":"Present when the slot carries a prize."},"rewardType":{"type":"string","description":"bonus_points, discount_coupon, store_credit, points_conversion — or no_win for empty slots.","example":"bonus_points"}}},"PlayStatus":{"type":"object","properties":{"canPlay":{"type":"boolean"},"playsRemaining":{"type":"integer","description":"-1 = unlimited."},"nextPlayAt":{"type":["string","null"],"format":"date-time"},"reason":{"type":"string","description":"Why the customer can't play — cooldown, daily_limit, wheel_inactive, needs_purchase, already_played. Empty when canPlay."}}},"GamificationPlay":{"type":"object","description":"The recorded play with a denormalized snapshot of the prize. Inspect\n`rewardType` to render the outcome — bonus points are already\ncredited, coupons appear in the customer's redemptions, store credit\nis already in their wallet.\n","properties":{"id":{"type":"integer"},"mechanic":{"type":"string","example":"wheel"},"playedAt":{"type":"string","format":"date-time"},"rewardType":{"type":"string","example":"bonus_points"},"rewardLabel":{"type":"string","example":"100 bonus points"},"rewardPoints":{"type":["integer","null"],"description":"For bonus_points prizes."},"rewardAmount":{"type":["number","null"],"description":"For store_credit prizes."}}},"Coupon":{"type":"object","properties":{"code":{"type":"string","example":"750KBB56"},"valid":{"type":"boolean","description":"Present on validate — true when unused."},"used":{"type":"boolean"},"usedAt":{"type":"string","format":"date-time"},"rewardName":{"type":"string"},"rewardType":{"type":"string","example":"discount_coupon"},"discountType":{"type":["string","null"],"example":"amount"},"discountValue":{"type":["number","null"],"example":20000},"discountSubType":{"type":"string","example":"order"},"minPurchaseAmount":{"type":["number","null"]},"targetProductIds":{"type":["array","null"],"items":{"type":"string"}},"endsAt":{"type":["string","null"],"format":"date-time"},"customerId":{"type":"string","example":"cust-1042"},"pointsUsed":{"type":"integer","example":100},"redeemedAt":{"type":"string","format":"date-time"}}},"Program":{"type":"object","properties":{"shopDomain":{"type":"string","example":"my-store.myshopify.com"},"currency":{"type":"string","example":"IDR"},"widgetTitle":{"type":["string","null"],"example":"My Store Rewards"},"widgetIntroduction":{"type":["string","null"]},"joinLoyaltyLabel":{"type":["string","null"]},"joinLoyaltyDescription":{"type":["string","null"]},"colorAccent":{"type":["string","null"],"description":"Hex accent color configured by the merchant.","example":"#e91e63"},"faq":{"type":["array","null"],"items":{"type":"object","properties":{"question":{"type":"string"},"answer":{"type":"string"}}}},"autoEnrollLoyalty":{"type":"boolean","description":"Whether customers are auto-enrolled on login/registration."},"storeCreditEnabled":{"type":"boolean"}}},"Tier":{"type":"object","properties":{"id":{"type":"integer","example":2},"name":{"type":"string","example":"Gold"},"minSpend":{"type":"number","description":"Total spend (shop currency) required to reach this tier.","example":5000000},"pointMultiplier":{"type":"number","example":2},"benefits":{"type":"string","example":"Earn 2x points, birthday gift"},"sortOrder":{"type":"integer","example":1}}},"Reward":{"type":"object","properties":{"id":{"type":"integer","example":12},"name":{"type":"string","example":"Rp 50.000 off"},"description":{"type":"string"},"voucherDescription":{"type":"string","description":"Usage terms shown on the coupon after redemption."},"rewardType":{"type":"string","enum":["discount_coupon","store_credit","points_conversion"]},"pointsCost":{"type":"integer","example":500},"tierIds":{"type":["array","null"],"description":"If non-empty, only customers in these tiers may redeem.","items":{"type":"string"}},"discountType":{"type":["string","null"],"enum":["amount","percentage",null]},"discountValue":{"type":["number","null"],"example":50000},"discountSubType":{"type":"string","description":"order, product, collection, bogo or shipping.","example":"order"},"applyMode":{"type":["string","null"],"description":"For product discounts — `each`, `one`, or `customer_picks` (customer chooses a product at redemption)."},"minPurchaseAmount":{"type":["number","null"]},"targetProductIds":{"type":["array","null"],"items":{"type":"string"}},"targetCollectionIds":{"type":["array","null"],"items":{"type":"string"}},"buyQuantity":{"type":["integer","null"],"description":"BOGO buy quantity."},"getQuantity":{"type":["integer","null"],"description":"BOGO get quantity."},"shippingPriceMaximum":{"type":["number","null"]},"storeCreditAmount":{"type":["number","null"]},"pointsPerDollar":{"type":["integer","null"],"description":"For points_conversion — points per 1 unit of currency."},"minRedeemPoints":{"type":["integer","null"]},"maxRedeemPoints":{"type":["integer","null"],"description":"0 = unlimited."},"startsAt":{"type":["string","null"],"format":"date-time"},"endsAt":{"type":["string","null"],"format":"date-time"}}},"Rule":{"type":"object","properties":{"id":{"type":"integer","example":3},"name":{"type":"string","example":"Points per purchase"},"type":{"type":"string","description":"Rule type, e.g. pointsPerSpend, firstPurchaseBonus, birthdayBonus, referralBonus, reviewPoints.","example":"pointsPerSpend"},"rewardType":{"type":["string","null"],"description":"What the rule awards — points, voucher or store_credit.","example":"points"},"pointsPerSpend":{"type":["integer","null"],"example":1},"bonusPoints":{"type":["integer","null"]},"pointMultiplier":{"type":["number","null"]},"minPurchase":{"type":["integer","null"]},"startDate":{"type":["string","null"],"example":null},"endDate":{"type":["string","null"]},"pointLifetimeDays":{"type":["integer","null"],"description":"Days until earned points expire.","example":365}}},"Customer":{"type":"object","properties":{"id":{"type":"string","description":"Platform customer id (e.g. Shopify customer ID).","example":"7234567890123"},"email":{"type":"string","example":"jane@example.com"},"firstName":{"type":"string","example":"Jane"},"lastName":{"type":"string","example":"Doe"},"phone":{"type":"string","example":"+628123456789"},"birthday":{"type":["string","null"],"example":"1995-04-12"},"gender":{"type":["string","null"],"example":"female"},"enrolled":{"type":"boolean","example":true},"points":{"type":"integer","description":"Currently available (unexpired, unredeemed) points.","example":1250},"tier":{"type":["object","null"],"properties":{"id":{"type":"integer"},"name":{"type":"string"},"pointMultiplier":{"type":"number"},"benefits":{"type":"string"}}},"referralCode":{"type":["string","null"],"example":"JANE-X7K2"},"memberSince":{"type":"string","format":"date-time"}}},"HistoryEntry":{"type":"object","properties":{"id":{"type":"integer","example":481},"orderId":{"type":["string","null"],"example":"5839201837465"},"orderName":{"type":["string","null"],"example":"#1042"},"pointsEarned":{"type":"integer","example":150},"pointsRedeemed":{"type":"integer","example":0},"pointsAvailable":{"type":"integer","description":"Points still available from this batch.","example":150},"ruleName":{"type":["string","null"],"example":"Points per purchase"},"orderTotal":{"type":["number","null"],"example":150000},"reason":{"type":["string","null"],"example":null},"earnedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"isExpired":{"type":"boolean","example":false}}},"Redemption":{"type":"object","properties":{"id":{"type":"integer","example":87},"rewardName":{"type":"string","example":"Rp 50.000 off"},"rewardType":{"type":"string","example":"discount_coupon"},"voucherDescription":{"type":"string","example":"Valid for 30 days."},"pointsUsed":{"type":"integer","example":500},"redeemedAt":{"type":"string","format":"date-time"},"discountCode":{"type":["string","null"],"example":"LT-X7K2-9QPM"},"source":{"type":"string","description":"redemption (spent points) or rule_voucher (awarded by an earning rule).","example":"redemption"},"targetProducts":{"type":["array","null"],"description":"Products the discount targets (when product-scoped).","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"handle":{"type":"string"},"imageUrl":{"type":"string"}}}},"targetCollections":{"type":["array","null"],"items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"}}}},"selectedProduct":{"type":["object","null"],"description":"The product the customer picked (customer_picks rewards).","properties":{"id":{"type":"string"},"title":{"type":"string"},"handle":{"type":"string"},"imageUrl":{"type":"string"}}}}}},"responses":{"BadRequest":{"description":"Malformed request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_request","message":"Provide customer_id, email, or both — at least one is required."}}}}},"UnauthorizedKey":{"description":"Missing, unknown or revoked API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_api_key","message":"This API key is unknown or has been revoked."}}}}},"UnauthorizedSecret":{"description":"Missing, invalid or revoked secret key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_secret_key","message":"This endpoint requires the secret key (sk_…), not the publishable key."}}}}},"UnauthorizedToken":{"description":"Missing or expired customer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_token","message":"The customer token is invalid or expired. Exchange a fresh signed identity via POST /auth/customer-token."}}}}},"RateLimited":{"description":"Per-key rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Too many requests for this API key. Slow down and retry shortly."}}}}}}}}