{
    "openapi": "3.1.0",
    "info": {
        "title": "KNISTR Loyalty Storefront API",
        "version": "1.0.0",
        "description": "Read and change the loyalty data of the customer who is logged in to the shop.\n\nUse it to build your own loyalty UI instead of the ready-made theme blocks the app ships with. Those blocks call these same endpoints, so you lose nothing by replacing them.\n\nThe loyalty data that the app syncs into Shopify (customer metafields, shop metaobjects) is an internal detail of those blocks and may change without notice — build against this API instead."
    },
    "servers": [
        {
            "url": "https://{shop_domain}/apps/loyalty-proxy",
            "description": "Shopify App Proxy — same origin as the storefront, signed by Shopify. Use this from a theme.",
            "variables": {
                "shop_domain": {
                    "default": "your-shop.myshopify.com",
                    "description": "The shop the customer is logged in to"
                }
            }
        },
        {
            "url": "https://api.ecommerce.knistrloyalty.cloud/storefront",
            "description": "Direct API host. Use this from a Shopify checkout or customer-account UI extension, with a session token."
        }
    ],
    "security": [
        {
        },
        {
            "ShopifySessionToken": [
            ]
        }
    ],
    "tags": [
        {
            "name": "Storefront",
            "description": "Loyalty endpoints a storefront may call on behalf of the logged-in customer.\n\nTwo supported ways to reach them:\n\n* **Shopify App Proxy** (recommended for themes) — call `/apps/loyalty-proxy/...` on the shop domain. Shopify signs the request and appends `logged_in_customer_id`; no credentials of your own.\n* **Session token** (checkout and customer-account UI extensions) — call the API host directly and send the Shopify session token in the `X-Shopify-Session-Token` header. The customer is taken from the token, never from the query string.\n\nEvery endpoint except `GET /config` acts on the logged-in customer and answers `401` without one."
        }
    ],
    "paths": {
        "/activities": {
            "parameters": [
                {
                    "description": "Appended by the Shopify App Proxy. Ignored when a session token is used.",
                    "name": "logged_in_customer_id",
                    "in": "query",
                    "schema": {
                        "type": "string"
                    }
                }
            ],
            "get": {
                "summary": "List the member's activity history",
                "description": "Points earned, points spent and rewards redeemed, newest first.",
                "tags": [
                    "Storefront"
                ],
                "responses": {
                    "200": {
                        "description": "Activity history",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/MemberActivityResponse"
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "No logged-in customer",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "The customer is not a member",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/birthday": {
            "parameters": [
                {
                    "description": "Appended by the Shopify App Proxy. Ignored when a session token is used.",
                    "name": "logged_in_customer_id",
                    "in": "query",
                    "schema": {
                        "type": "string"
                    }
                }
            ],
            "post": {
                "summary": "Set the member's birthday",
                "description": "Month and day only, as `--MM-DD`. It can be set once; afterwards the stored date is returned with `409`.",
                "tags": [
                    "Storefront"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/StorefrontBirthdayRequest"
                            }
                        }
                    },
                    "required": true
                },
                "responses": {
                    "200": {
                        "description": "Birthday stored",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/StorefrontBirthdayResponse"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "Birthdate is not in `--MM-DD` format",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "No logged-in customer",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "The customer is not a member",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "A birthday is already set; the response repeats the stored `birthdate`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/StorefrontBirthdayConflictResponse"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/config": {
            "parameters": [
                {
                    "description": "Appended by the Shopify App Proxy. Ignored when a session token is used.",
                    "name": "logged_in_customer_id",
                    "in": "query",
                    "schema": {
                        "type": "string"
                    }
                }
            ],
            "get": {
                "summary": "Get the shop's program configuration",
                "description": "Branding, earning rules and the reward options a member can redeem. Shop-scoped: this is the one endpoint that needs no logged-in customer.",
                "tags": [
                    "Storefront"
                ],
                "responses": {
                    "200": {
                        "description": "Program configuration",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/StorefrontConfigResponse"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/leave": {
            "parameters": [
                {
                    "description": "Appended by the Shopify App Proxy. Ignored when a session token is used.",
                    "name": "logged_in_customer_id",
                    "in": "query",
                    "schema": {
                        "type": "string"
                    }
                }
            ],
            "post": {
                "summary": "Leave the loyalty program",
                "description": "Ends the membership. Points and coupons are no longer usable.",
                "tags": [
                    "Storefront"
                ],
                "responses": {
                    "200": {
                        "description": "Membership ended",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/StorefrontLeaveResponse"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "No logged-in customer",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "The customer is not a member",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/member": {
            "parameters": [
                {
                    "description": "Appended by the Shopify App Proxy. Ignored when a session token is used.",
                    "name": "logged_in_customer_id",
                    "in": "query",
                    "schema": {
                        "type": "string"
                    }
                }
            ],
            "get": {
                "summary": "Get the member's balance and coupons",
                "description": "Points balance, pending points and the coupons the customer currently holds.",
                "tags": [
                    "Storefront"
                ],
                "responses": {
                    "200": {
                        "description": "Member details",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/StorefrontMemberResponse"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "No logged-in customer",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "The customer is not a member",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/redeem": {
            "parameters": [
                {
                    "description": "Appended by the Shopify App Proxy. Ignored when a session token is used.",
                    "name": "logged_in_customer_id",
                    "in": "query",
                    "schema": {
                        "type": "string"
                    }
                }
            ],
            "post": {
                "summary": "Redeem points for a reward",
                "description": "Exchanges the member's points for the reward option and returns the coupon to show the customer. Option ids come from `GET /config`.",
                "tags": [
                    "Storefront"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/StorefrontRedeemRequest"
                            }
                        }
                    },
                    "required": true
                },
                "responses": {
                    "200": {
                        "description": "Coupon issued",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/CouponResponse"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "Option id missing or unknown, or the member has too few points",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "No logged-in customer",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "The customer is not a member",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/register": {
            "parameters": [
                {
                    "description": "Appended by the Shopify App Proxy. Ignored when a session token is used.",
                    "name": "logged_in_customer_id",
                    "in": "query",
                    "schema": {
                        "type": "string"
                    }
                }
            ],
            "post": {
                "summary": "Join the loyalty program",
                "description": "Creates a membership for the logged-in customer. Calling it again for a customer who is already a member returns that membership instead of failing.",
                "tags": [
                    "Storefront"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/StorefrontRegisterRequest"
                            }
                        }
                    },
                    "required": true
                },
                "responses": {
                    "200": {
                        "description": "Membership created, or the existing one returned",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/StorefrontRegisterResponse"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "Consent was not given, or the customer has no email address in Shopify",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "No logged-in customer",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "Another member already uses this email address",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "Customer data could not be read from Shopify",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorResponse"
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "ShopifySessionToken": {
                "type": "apiKey",
                "in": "header",
                "name": "X-Shopify-Session-Token",
                "description": "A Shopify session token for the logged-in customer, obtained with `api.sessionToken.get()`. Required when calling the API host directly; not needed through the App Proxy, which Shopify signs.",
                "bearerFormat": "JWT"
            }
        },
        "schemas": {
            "Branding": {
                "type": "object",
                "properties": {
                    "rewardCurrencyName": {
                        "type": "string",
                        "description": "What the shop calls its points, for use next to a number.",
                        "examples": [
                            "Sparks"
                        ]
                    },
                    "rewardSymbol": {
                        "type": "string",
                        "description": "Short symbol or emoji shown beside a balance. May be empty.",
                        "examples": [
                            "✦"
                        ]
                    },
                    "programWelcomeMessage": {
                        "type": "string",
                        "description": "The shop's own text for the join call to action. May be empty.",
                        "examples": [
                            "Join and start collecting Sparks on every order."
                        ]
                    }
                }
            },
            "CouponResponse": {
                "type": "object",
                "properties": {
                    "couponId": {
                        "type": "string",
                        "description": "Id of this coupon. Use it as the list key.",
                        "examples": [
                            "c81d4e2f-6a90-4b13-9f77-2b0c5e6a8d41"
                        ]
                    },
                    "memberId": {
                        "type": "string",
                        "description": "Member the coupon belongs to.",
                        "examples": [
                            "3f1a7c02-95d1-4a2e-8f0b-1c6de2a9b410"
                        ]
                    },
                    "code": {
                        "type": "string",
                        "description": "The discount code the customer enters at checkout.",
                        "examples": [
                            "LOYALTY-5EUR-A7K2"
                        ]
                    },
                    "title": {
                        "type": "string",
                        "description": "Reward name as the merchant wrote it.",
                        "examples": [
                            "€5 off your order"
                        ]
                    },
                    "description": {
                        "type": "string",
                        "description": "Longer description as the merchant wrote it. May be empty.",
                        "examples": [
                            "Applies to any order over €20."
                        ]
                    },
                    "discountType": {
                        "$ref": "#/components/schemas/DiscountType",
                        "description": "What the code does at checkout.",
                        "type": "string"
                    },
                    "discountValue": {
                        "type": "number",
                        "description": "Percent off for `PERCENTAGE`, otherwise an amount in the shop's currency (major units). Ignored for `FREE_SHIPPING` and `FREE_GIFT`.",
                        "examples": [
                            5.0
                        ]
                    },
                    "minOrderValue": {
                        "type": "number",
                        "description": "Order total the code needs to apply, in the shop's currency (major units). Null or 0 means no minimum.",
                        "examples": [
                            20.0
                        ]
                    },
                    "status": {
                        "$ref": "#/components/schemas/CouponStatus",
                        "description": "Lifecycle state. Only `AVAILABLE` can still be used — but check `isValid`, which also accounts for the dates.",
                        "type": "string"
                    },
                    "validFrom": {
                        "$ref": "#/components/schemas/LocalDateTime",
                        "description": "Usable from this moment, in UTC and without an offset.",
                        "type": "string",
                        "examples": [
                            "2026-09-04T04:48:20"
                        ]
                    },
                    "validUntil": {
                        "$ref": "#/components/schemas/LocalDateTime",
                        "description": "Expires at this moment, in UTC and without an offset. Null means it never expires.",
                        "type": "string",
                        "examples": [
                            "2026-12-04T04:48:20"
                        ]
                    },
                    "redeemedAt": {
                        "$ref": "#/components/schemas/LocalDateTime",
                        "description": "When the customer used it, in UTC and without an offset. Null while unused.",
                        "type": "string",
                        "examples": [
                            "2026-10-01T09:12:44"
                        ]
                    },
                    "pointsCost": {
                        "type": "integer",
                        "format": "int32",
                        "description": "Points the member paid for this coupon.",
                        "examples": [
                            500
                        ]
                    },
                    "isValid": {
                        "type": "boolean",
                        "description": "Whether the customer can use it right now — status and dates together. This is the flag to filter a \"your coupons\" list on.",
                        "examples": [
                            true
                        ]
                    }
                }
            },
            "CouponStatus": {
                "type": "string",
                "enum": [
                    "AVAILABLE",
                    "REDEEMED",
                    "EXPIRED",
                    "DELETED"
                ]
            },
            "DiscountType": {
                "type": "string",
                "enum": [
                    "PERCENTAGE",
                    "FIXED_AMOUNT",
                    "FREE_SHIPPING",
                    "FREE_GIFT"
                ]
            },
            "Earning": {
                "description": "Every `...Cents` value is in the minor unit of the shop's currency (for example 100 = 1.00). The currency code is not part of this API — take it from Shopify, e.g. `shop.currency` in Liquid.",
                "type": "object",
                "properties": {
                    "pointsPerCurrencyEnabled": {
                        "type": "boolean",
                        "description": "Whether the shop awards points for money spent. When false, ignore the other `pointsPerCurrency*` fields.",
                        "examples": [
                            true
                        ]
                    },
                    "pointsPerCurrencyAmountCents": {
                        "type": "integer",
                        "format": "int32",
                        "description": "The spend step that earns `pointsPerCurrencyPoints`, in minor units.",
                        "examples": [
                            100
                        ]
                    },
                    "pointsPerCurrencyPoints": {
                        "type": "integer",
                        "format": "int32",
                        "description": "Points earned per spend step.",
                        "examples": [
                            1
                        ]
                    },
                    "pointsPerCurrencyDescription": {
                        "type": "string",
                        "description": "The merchant's own wording for this rule, already translated. Prefer it over a sentence you build yourself.",
                        "examples": [
                            "Earn 1 Spark per €1 spent"
                        ]
                    },
                    "pointsPerCurrencyMinOrderValueCents": {
                        "type": "integer",
                        "format": "int32",
                        "description": "Orders below this total earn nothing from this rule. 0 means no minimum.",
                        "examples": [
                            0
                        ]
                    },
                    "pointsForPurchaseEnabled": {
                        "type": "boolean",
                        "description": "Whether the shop awards a flat bonus per order.",
                        "examples": [
                            false
                        ]
                    },
                    "pointsForPurchase": {
                        "type": "integer",
                        "format": "int32",
                        "description": "Flat points awarded per qualifying order.",
                        "examples": [
                            10
                        ]
                    },
                    "pointsForPurchaseDescription": {
                        "type": "string",
                        "description": "The merchant's own wording for the per-order bonus.",
                        "examples": [
                            "10 Sparks on every order"
                        ]
                    },
                    "pointsForPurchaseMinOrderValueCents": {
                        "type": "integer",
                        "format": "int32",
                        "description": "Orders below this total earn no per-order bonus. 0 means no minimum.",
                        "examples": [
                            5000
                        ]
                    },
                    "pointsForRegistrationEnabled": {
                        "type": "boolean",
                        "description": "Whether joining the program awards points.",
                        "examples": [
                            true
                        ]
                    },
                    "pointsForRegistration": {
                        "type": "integer",
                        "format": "int32",
                        "description": "Points awarded once, when the customer joins.",
                        "examples": [
                            100
                        ]
                    },
                    "pointsForRegistrationDescription": {
                        "type": "string",
                        "description": "The merchant's own wording for the joining reward.",
                        "examples": [
                            "100 Sparks just for joining"
                        ]
                    },
                    "pointsForBirthdayEnabled": {
                        "type": "boolean",
                        "description": "Whether the shop awards points on the member's birthday. When false, do not show the birthday field.",
                        "examples": [
                            true
                        ]
                    },
                    "pointsForBirthday": {
                        "type": "integer",
                        "format": "int32",
                        "description": "Points awarded each year on the member's birthday.",
                        "examples": [
                            50
                        ]
                    },
                    "pointsForBirthdayDescription": {
                        "type": "string",
                        "description": "The merchant's own wording for the birthday reward.",
                        "examples": [
                            "50 Sparks on your birthday"
                        ]
                    },
                    "pointsExpirationMonths": {
                        "type": "integer",
                        "format": "int32",
                        "description": "How many months points stay spendable before they expire. 0 means they never expire.",
                        "examples": [
                            12
                        ]
                    }
                }
            },
            "ErrorResponse": {
                "type": "object",
                "properties": {
                    "error": {
                        "type": "string",
                        "description": "Why the request failed, in English. Meant for your logs — show the customer your own wording.",
                        "examples": [
                            "Not enough points"
                        ]
                    }
                }
            },
            "LocalDateTime": {
                "type": "string",
                "format": "date-time",
                "examples": [
                    "2022-03-10T12:15:50"
                ]
            },
            "MemberActivityResponse": {
                "type": "object",
                "properties": {
                    "activityId": {
                        "type": "string",
                        "description": "Id of this log entry. Use it as the list key.",
                        "examples": [
                            "a2c8f1de-0b44-4c9e-9d21-77f0b3ac1e58"
                        ]
                    },
                    "activityType": {
                        "type": "string",
                        "description": "What happened. New types may be added at any time, so fall back to `description` for anything you do not recognise.",
                        "examples": [
                            "POINTS_EARNED"
                        ],
                        "enum": [
                            "REGISTRATION",
                            "POINTS_EARNED",
                            "POINTS_DEDUCTED",
                            "POINT_ADJUSTMENT",
                            "BIRTHDAY_SET",
                            "CUSTOMER_UPDATED",
                            "MEMBER_RECONNECTED",
                            "MEMBER_DELETED",
                            "MEMBER_DELETED_BY_SHOPIFY"
                        ]
                    },
                    "description": {
                        "type": "string",
                        "description": "Ready-to-display sentence, already in the shop's language. Show this rather than building your own from `activityType`.",
                        "examples": [
                            "You earned 42 Sparks for order #1042"
                        ]
                    },
                    "metadata": {
                        "type": "string",
                        "description": "A JSON object serialised as a string, or null. Keys depend on `activityType`; point movements carry `pointsChange`, order-related ones also `orderNumber`. Treat every key as optional.",
                        "examples": [
                            "{\"pointsChange\":42,\"orderNumber\":\"#1042\"}"
                        ]
                    },
                    "timestamp": {
                        "$ref": "#/components/schemas/LocalDateTime",
                        "description": "When it happened, in UTC and without an offset.",
                        "type": "string",
                        "examples": [
                            "2026-09-04T04:48:20"
                        ]
                    }
                }
            },
            "RewardOption": {
                "type": "object",
                "properties": {
                    "optionId": {
                        "type": "string",
                        "description": "Send this back as `optionId` in `POST /redeem`.",
                        "examples": [
                            "6b1f0f4c-2f3a-4a1e-9a7c-0d2f8e5b1c33"
                        ]
                    },
                    "name": {
                        "type": "string",
                        "description": "Reward name as the merchant wrote it.",
                        "examples": [
                            "€5 off your order"
                        ]
                    },
                    "description": {
                        "type": "string",
                        "description": "Longer description as the merchant wrote it. May be empty.",
                        "examples": [
                            "Applies to any order over €20."
                        ]
                    },
                    "discountType": {
                        "$ref": "#/components/schemas/DiscountType",
                        "description": "What the coupon does once redeemed.",
                        "type": "string"
                    },
                    "discountValue": {
                        "type": "number",
                        "description": "Percent off for `PERCENTAGE`, otherwise an amount in the shop's currency (major units). Ignored for `FREE_SHIPPING` and `FREE_GIFT`.",
                        "examples": [
                            5.0
                        ]
                    },
                    "pointsCost": {
                        "type": "integer",
                        "format": "int32",
                        "description": "Points this reward costs. Compare it against `activePoints`, not `totalPoints`.",
                        "examples": [
                            500
                        ]
                    }
                }
            },
            "StorefrontBirthdayConflictResponse": {
                "type": "object",
                "properties": {
                    "error": {
                        "type": "string",
                        "description": "Why the birthday was not changed.",
                        "examples": [
                            "Birthday is already set"
                        ]
                    },
                    "birthdate": {
                        "type": "string",
                        "description": "The birthday already on file, as the ISO-8601 MonthDay `--MM-DD`. Show this instead of an error.",
                        "examples": [
                            "--08-24"
                        ]
                    }
                }
            },
            "StorefrontBirthdayRequest": {
                "type": "object",
                "required": [
                    "birthdate"
                ],
                "properties": {
                    "birthdate": {
                        "type": "string",
                        "pattern": "^--\\d{2}-\\d{2}$",
                        "description": "Day and month of birth as an ISO-8601 MonthDay, `--MM-DD`. The year is deliberately not collected. Any other format is rejected with 400.",
                        "examples": [
                            "--08-24"
                        ]
                    }
                }
            },
            "StorefrontBirthdayResponse": {
                "type": "object",
                "properties": {
                    "success": {
                        "type": "boolean",
                        "description": "Always `true`; the birthday was stored.",
                        "examples": [
                            true
                        ]
                    },
                    "birthdate": {
                        "type": "string",
                        "description": "The stored birthday, as the ISO-8601 MonthDay `--MM-DD`.",
                        "examples": [
                            "--08-24"
                        ]
                    }
                }
            },
            "StorefrontConfigResponse": {
                "type": "object",
                "properties": {
                    "branding": {
                        "$ref": "#/components/schemas/Branding",
                        "description": "What the shop calls its points and how it greets new members.",
                        "type": "object"
                    },
                    "earning": {
                        "$ref": "#/components/schemas/Earning",
                        "description": "The rules that earn points, so you can explain them on the page.",
                        "type": "object"
                    },
                    "rewardOptions": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/RewardOption"
                        },
                        "description": "The rewards a member can redeem points for, in display order."
                    }
                }
            },
            "StorefrontLeaveResponse": {
                "type": "object",
                "properties": {
                    "success": {
                        "type": "boolean",
                        "description": "Always `true`; the membership ended.",
                        "examples": [
                            true
                        ]
                    },
                    "message": {
                        "type": "string",
                        "description": "Confirmation text you can show the customer.",
                        "examples": [
                            "You have successfully left the loyalty program."
                        ]
                    }
                }
            },
            "StorefrontMemberResponse": {
                "type": "object",
                "properties": {
                    "memberId": {
                        "type": "string",
                        "description": "The member's id in the loyalty program. Stable for the life of the membership.",
                        "examples": [
                            "3f1a7c02-95d1-4a2e-8f0b-1c6de2a9b410"
                        ]
                    },
                    "email": {
                        "type": "string",
                        "description": "Email of the Shopify customer the membership belongs to.",
                        "examples": [
                            "ada@example.com"
                        ]
                    },
                    "firstName": {
                        "type": "string",
                        "description": "First name as it is held in Shopify. Null when the customer has none.",
                        "examples": [
                            "Ada"
                        ]
                    },
                    "lastName": {
                        "type": "string",
                        "description": "Last name as it is held in Shopify. Null when the customer has none.",
                        "examples": [
                            "Lovelace"
                        ]
                    },
                    "birthdate": {
                        "type": "string",
                        "description": "Birthday as the ISO-8601 MonthDay `--MM-DD`, or null when the customer has not set one. Use it to decide whether to show the birthday field.",
                        "examples": [
                            "--08-24"
                        ]
                    },
                    "activePoints": {
                        "type": "integer",
                        "format": "int32",
                        "description": "Points the customer can spend right now.",
                        "examples": [
                            420
                        ]
                    },
                    "pendingPoints": {
                        "type": "integer",
                        "format": "int32",
                        "description": "Points already earned that are still inside the shop's waiting period. They become spendable on their own, with no further action.",
                        "examples": [
                            50
                        ]
                    },
                    "totalPoints": {
                        "type": "integer",
                        "format": "int32",
                        "description": "`activePoints` + `pendingPoints`. Show this only as a headline figure — it is not what the customer can spend.",
                        "examples": [
                            470
                        ]
                    },
                    "coupons": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/CouponResponse"
                        },
                        "description": "Every coupon the member has ever received, newest first — including used and expired ones. Filter on `isValid` before showing them."
                    }
                }
            },
            "StorefrontRedeemRequest": {
                "type": "object",
                "required": [
                    "optionId"
                ],
                "properties": {
                    "optionId": {
                        "type": "string",
                        "description": "`optionId` of the reward to redeem, taken from `rewardOptions` in `GET /config`.",
                        "examples": [
                            "6b1f0f4c-2f3a-4a1e-9a7c-0d2f8e5b1c33"
                        ]
                    }
                }
            },
            "StorefrontRegisterRequest": {
                "type": "object",
                "properties": {
                    "registrationSource": {
                        "type": "string",
                        "description": "Where the customer joined from, stored for reporting. Defaults to `WIDGET` when omitted.",
                        "examples": [
                            "STOREFRONT"
                        ]
                    },
                    "consentGiven": {
                        "type": "boolean",
                        "description": "Whether the customer agreed to the loyalty terms. Must be `true`; the request is rejected with 400 otherwise.",
                        "examples": [
                            true
                        ]
                    }
                },
                "required": [
                    "consentGiven"
                ]
            },
            "StorefrontRegisterResponse": {
                "type": "object",
                "properties": {
                    "success": {
                        "type": "boolean",
                        "description": "Always `true`; the membership was created.",
                        "examples": [
                            true
                        ]
                    },
                    "memberId": {
                        "type": "string",
                        "description": "The new member's id in the loyalty program.",
                        "examples": [
                            "3f1a7c02-95d1-4a2e-8f0b-1c6de2a9b410"
                        ]
                    },
                    "email": {
                        "type": "string",
                        "description": "Email taken from the Shopify customer, not from the request.",
                        "examples": [
                            "ada@example.com"
                        ]
                    },
                    "firstName": {
                        "type": "string",
                        "description": "First name taken from the Shopify customer.",
                        "examples": [
                            "Ada"
                        ]
                    },
                    "lastName": {
                        "type": "string",
                        "description": "Last name taken from the Shopify customer.",
                        "examples": [
                            "Lovelace"
                        ]
                    },
                    "activePoints": {
                        "type": "integer",
                        "format": "int32",
                        "description": "Spendable points right after joining — the registration reward, when the shop grants one.",
                        "examples": [
                            100
                        ]
                    },
                    "pendingPoints": {
                        "type": "integer",
                        "format": "int32",
                        "description": "Points still inside the shop's waiting period right after joining.",
                        "examples": [
                            0
                        ]
                    },
                    "totalRewardPoints": {
                        "type": "integer",
                        "format": "int32",
                        "description": "`activePoints` + `pendingPoints`.",
                        "examples": [
                            100
                        ]
                    }
                }
            }
        }
    }
}
