{
  "openapi": "3.1.0",
  "info": {
    "title": "Matrix Rewards Merchant API",
    "version": "2026-10",
    "summary": "Read a merchant's loyalty programme — stores, members, the ledger, reports, usage and the audit log — and change a member's points or cancel a reward code.",
    "description": "The account-scoped API behind Matrix Rewards. Every call is authenticated by an account API key (`mrk_live_...`), and the key alone decides whose data is served: no path, query or header names an account. The first writes are here — changing a member's points and cancelling a reward code — and so are webhooks: an https endpoint the merchant registers is sent every points movement, signed the Standard Webhooks way. The CLI follows. A points change and a code cancel each take an `Idempotency-Key` and can be previewed with `?dry_run=true`, and every write is written to the account's audit log before it is answered; a key the merchant tagged for an AI assistant must preview a change and send back that preview's confirmation before anything changes.\n\nConventions: `snake_case` fields; lists are `{data, has_more, next_cursor}` with opaque keyset cursors; failures are RFC 9457 `application/problem+json` carrying a machine-readable `code` and the `request_id` also returned in the `Request-Id` header. The programme documents (`config`, `effective`, `appearance`) and the settlement report keep the engine's own field names, because they are the same documents the app writes and the write API will accept back.\n\nEvery read that carries shopper data is written to the account's audit log before it is returned, and the call fails with 503 `access_not_recorded` rather than return data it could not record.\n\nNo answer carries a whole reward code. An unused code is a live discount, and API answers end up in logs, scripts and assistant transcripts, so a code is named by its last four characters (`code_ending`, null for a code too short to show any of) and by its `ref`, which is what a cancel takes. The shopper holds the code itself.",
    "contact": {
      "name": "Matrix Rewards support",
      "email": "support@matrixhealthgroup.co.uk"
    }
  },
  "servers": [
    {
      "url": "https://app.matrixrewards.co.uk/api/2026-10",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "x-matrix-limits": {
    "read_requests_per_minute": 120,
    "read_burst": 120,
    "write_requests_per_minute": 60,
    "write_burst": 20,
    "page_size_default": 25,
    "page_size_max": 100,
    "member_search_max_chars": 120,
    "write_body_max_bytes": 4096,
    "programme_write_body_max_bytes": 32768,
    "adjustment_max_points": 100000,
    "reason_max_chars": 200,
    "api_points_added_per_24h": 100000,
    "api_points_taken_per_24h": 100000,
    "api_points_window_hours": 24,
    "idempotency_window_hours": 24,
    "idempotency_key_max_chars": 100,
    "confirmation_ttl_minutes": 10,
    "confirmation_max_chars": 1024,
    "live_keys_per_account_max": 20,
    "key_name_max_chars": 60,
    "rotation_grace_default_days": 1,
    "rotation_grace_max_days": 7,
    "client_name_max_chars": 80,
    "audit_retention_days": 365,
    "expiry_min_period_months": 12,
    "expiry_max_period_months": 1200,
    "expiry_max_effective_year": 9898,
    "webhook_endpoints_max": 5,
    "webhook_url_max_chars": 2048,
    "webhook_description_max_chars": 120,
    "webhook_payload_max_bytes": 4096,
    "webhook_timeout_seconds": 15,
    "webhook_attempts_max": 10,
    "webhook_retry_delays_seconds": [
      5,
      300,
      1800,
      7200,
      18000,
      36000,
      50400,
      72000,
      86400
    ],
    "webhook_retry_jitter_percent": 10,
    "webhook_retry_span_hours": 75,
    "webhook_disable_after_failures": 10,
    "webhook_disable_after_hours": 24,
    "webhook_message_retention_days": 30,
    "api_programme_changes_per_24h": 100,
    "api_programme_window_hours": 24,
    "offer_product_ids_max": 500,
    "offer_channel_list_max": 50,
    "assistant_step_factor": 2,
    "referral_advocate_cap_window_days": 30,
    "assistant_window_hours": 24,
    "spend_any_amount_points_max": 10000000,
    "ways_to_earn_max": 50,
    "brand_image_max_bytes": 524288,
    "brand_image_min_side_px": 16,
    "brand_image_max_side_px": 4096,
    "brand_images_per_store_max": 20,
    "brand_slogan_max_chars": 80
  },
  "tags": [
    {
      "name": "Account",
      "description": "What this key is and what it reaches."
    },
    {
      "name": "Stores",
      "description": "The stores on the account and their programmes."
    },
    {
      "name": "Members",
      "description": "Shoppers holding points, and the codes they hold."
    },
    {
      "name": "Reports",
      "description": "The ledger, liability and settlement."
    },
    {
      "name": "Billing",
      "description": "Usage against the plan, and the recorded plan."
    },
    {
      "name": "Log",
      "description": "What every key did, and what it was shown."
    },
    {
      "name": "Webhooks",
      "description": "Where every points movement is sent, and a way to check a receiver. Managing webhooks needs `write_webhooks`, and a key for an AI assistant or a test key can never do it: an endpoint is a standing channel out of the account. Each event is POSTed as JSON to every enabled endpoint subscribed to its type, signed the Standard Webhooks way: `webhook-id` (the same on every retry, and the thing to de-duplicate on), `webhook-timestamp` (unix seconds of this attempt; reject one more than 5 minutes from your clock), and `webhook-signature: v1,<base64 HMAC-SHA256>` over `<webhook-id>.<webhook-timestamp>.<body>`, keyed by the base64 after `whsec_` in the endpoint's secret, decoded to bytes. Verify the raw body, before parsing it. Answer 2xx within 15 seconds; anything else, a redirect, or no answer is a failure, and the event is sent again after 5 seconds, 5 minutes, 30 minutes, then 2, 5, 10, 14, 20 and 24 hours (each ±10%) — ten attempts over about 75 hours — with the same id and the same bytes. Events can arrive out of order: `data.ledger_entry_id` and `data.created_at` order them. An endpoint that has failed 10 deliveries in a row AND been failing for at least 24 hours is disabled: nothing more is sent to it, what was waiting is abandoned, `webhook.disabled` is written to the audit log, and GET /webhooks shows `status: disabled`. Delete it and create it again once the receiver works. An endpoint receives the events recorded after it was created."
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "Account"
        ],
        "summary": "The account this key belongs to, its stores, and the key's own scopes",
        "description": "Needs no scope beyond a valid key: a client's first call should not have to guess what it may do.",
        "responses": {
          "200": {
            "description": "The account, its stores and this key.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                },
                "examples": {
                  "example": {
                    "value": {
                      "api_version": "2026-10",
                      "account": {
                        "id": "acc_example",
                        "name": "Example Retail Ltd",
                        "wallet_scope": "site",
                        "created_at": "2026-08-03T09:12:00Z"
                      },
                      "stores": [
                        {
                          "key": "example-outlet",
                          "name": "Example Outlet",
                          "platform": "shopify",
                          "external_ref": "example-outlet.myshopify.com",
                          "created_at": "2026-08-20T14:30:00Z",
                          "display_name": "Example Outlet"
                        },
                        {
                          "key": "example-store",
                          "name": "Example Store",
                          "platform": "shopify",
                          "external_ref": "example-store.myshopify.com",
                          "created_at": "2026-08-03T09:12:00Z",
                          "display_name": "Example Store"
                        }
                      ],
                      "key": {
                        "id": "Kx7Qp2Lm9Vt4Rz8N",
                        "name": "Nightly export",
                        "mode": "live",
                        "scopes": [
                          "manage_keys",
                          "read_audit",
                          "read_members",
                          "read_programme",
                          "read_reports",
                          "read_usage",
                          "write_members",
                          "write_webhooks"
                        ],
                        "agent": false,
                        "limits_lifted": false,
                        "used_previous_secret": false
                      },
                      "rate_limit": {
                        "limit": 120,
                        "remaining": 119,
                        "reset": 1789552801
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stores": {
      "get": {
        "operationId": "listStores",
        "tags": [
          "Stores"
        ],
        "summary": "Every store on this account",
        "security": [
          {
            "apiKey": [
              "read_programme"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The account's stores.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StorePage"
                },
                "examples": {
                  "example": {
                    "value": {
                      "data": [
                        {
                          "key": "example-outlet",
                          "name": "Example Outlet",
                          "platform": "shopify",
                          "external_ref": "example-outlet.myshopify.com",
                          "created_at": "2026-08-20T14:30:00Z",
                          "display_name": "Example Outlet"
                        },
                        {
                          "key": "example-store",
                          "name": "Example Store",
                          "platform": "shopify",
                          "external_ref": "example-store.myshopify.com",
                          "created_at": "2026-08-03T09:12:00Z",
                          "display_name": "Example Store"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stores/{store_key}": {
      "get": {
        "operationId": "getStore",
        "tags": [
          "Stores"
        ],
        "summary": "One store",
        "security": [
          {
            "apiKey": [
              "read_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The store.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Store"
                },
                "examples": {
                  "example": {
                    "value": {
                      "key": "example-store",
                      "name": "Example Store",
                      "platform": "shopify",
                      "external_ref": "example-store.myshopify.com",
                      "created_at": "2026-08-03T09:12:00Z",
                      "display_name": "Example Store"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stores/{store_key}/programme": {
      "get": {
        "operationId": "getProgramme",
        "tags": [
          "Stores"
        ],
        "summary": "A store's programme: what is stored, the settings in force, what shoppers are actually paid, and the expiry rule",
        "security": [
          {
            "apiKey": [
              "read_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The programme.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Programme"
                },
                "examples": {
                  "example": {
                    "value": {
                      "store_key": "example-store",
                      "config": {
                        "rules": [
                          {
                            "id": "base_earn",
                            "award": {
                              "type": "perMajorUnit",
                              "pointsPerUnit": 1
                            },
                            "event": "order_paid",
                            "reason": "Points earned on your order",
                            "condition": {
                              "type": "always"
                            }
                          }
                        ],
                        "rewards": [
                          {
                            "id": "reward_5",
                            "costPoints": 500,
                            "valueLabel": "5.00 GBP off",
                            "minSubtotal": "5.00",
                            "valueAmount": "5.00"
                          }
                        ],
                        "currency": "GBP",
                        "timezone": "Europe/London",
                        "waysToEarn": [
                          {
                            "id": "orders",
                            "label": "Shop online",
                            "detail": "Earn 1 point for every £1 you spend"
                          }
                        ],
                        "appChannelValue": "",
                        "loyaltyStartDate": "2026-08-03",
                        "appOrderMultiplier": 1,
                        "welcomeBonusPoints": 0,
                        "appChannelAttribute": "_channel",
                        "basePointsPerMajorUnit": 1,
                        "redeemPointsPerMajorUnit": 100
                      },
                      "effective": {
                        "currency": "GBP",
                        "timezone": "Europe/London",
                        "basePointsPerMajorUnit": 1,
                        "appOrderMultiplier": 1,
                        "welcomeBonusPoints": 0,
                        "redeemPointsPerMajorUnit": 100,
                        "appChannelAttribute": "_channel",
                        "appChannelValue": "",
                        "loyaltyStartDate": "2026-08-03",
                        "rewards": [
                          {
                            "id": "reward_5",
                            "costPoints": 500,
                            "valueLabel": "5.00 GBP off",
                            "minSubtotal": "5.00",
                            "valueAmount": "5.00"
                          }
                        ],
                        "rules": [
                          {
                            "id": "base_earn",
                            "award": {
                              "type": "perMajorUnit",
                              "pointsPerUnit": 1
                            },
                            "event": "order_paid",
                            "reason": "Points earned on your order",
                            "condition": {
                              "type": "always"
                            }
                          }
                        ],
                        "waysToEarn": [
                          {
                            "id": "orders",
                            "label": "Shop online",
                            "detail": "Earn 1 point for every £1 you spend"
                          }
                        ],
                        "referral": {
                          "enabled": true,
                          "friendPoints": 300,
                          "advocatePoints": 200,
                          "qualifyingMinAmount": "15.00",
                          "claimExpiryDays": 30,
                          "advocateCapFriends": 10
                        },
                        "bonusesInAppOnly": false,
                        "spendAnyAmount": {
                          "enabled": false,
                          "minPoints": 100,
                          "maxPoints": null,
                          "minSubtotal": "0.00"
                        }
                      },
                      "as_paid": {
                        "earn_rate_per_major_unit": 1,
                        "earn_rates_per_major_unit": [
                          1
                        ],
                        "welcome_bonus_points": 0,
                        "ways_to_earn": [
                          {
                            "id": "orders",
                            "label": "Shop online",
                            "detail": "Earn 1 point for every £1 you spend"
                          }
                        ]
                      },
                      "appearance": {
                        "programmeName": "Rewards",
                        "pointsName": "point",
                        "pointsNamePlural": "points",
                        "heading": "",
                        "storeName": "",
                        "signedOutPrompt": "Sign in to see your points.",
                        "rewardsHeading": "What you can get",
                        "earnHeading": "Ways to earn",
                        "activityHeading": "Recent activity",
                        "accent": null,
                        "cornerRadius": null,
                        "density": "comfortable",
                        "textScale": "normal",
                        "fontFamily": "theme",
                        "showRewards": true,
                        "showWaysToEarn": true,
                        "showActivity": true,
                        "showExpiry": true,
                        "showSharedWallet": true,
                        "showProductPoints": true,
                        "showRewardsSite": false,
                        "termsUrl": null,
                        "otherShops": {}
                      },
                      "expiry": null,
                      "version": "3f1c9a2b7d4e5f60718293a4b5c6d7e8"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "updateProgramme",
        "tags": [
          "Stores"
        ],
        "summary": "Change the earn rate, the bonuses, what a point is worth, referrals, spending any amount, the time zone, the ways to earn, or when points expire",
        "description": "Plain fields, not rules: the rules that pay them are built by the same code the Programme screen in your Shopify admin runs, and every other rule (offers, rewards, anything set up by hand) is carried exactly as it is. The change is built over one reading of the programme, so a change made elsewhere in between is refused (409 `settings_changed`), never undone. A preview (`dry_run=true`) makes the change in a transaction, reads the programme back and rolls it back: it shows what three example orders earn before and after, every figure the change moves before and after in this call's own field names (`before_after`) and, for a point-value change, what the points shoppers hold are worth. At most 100 changes per account through the API in any 24 hours. Spending any amount (`spend_any_amount`) lets a shopper choose how many points to spend, for a code worth exactly that at the store's point value; it is off at every store until it is switched on, and the preview shows its terms before and after. The currency cannot be changed here. Written to the audit log in the same transaction as the change: if the entry cannot be written, the change is not made.",
        "security": [
          {
            "apiKey": [
              "write_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/DryRun"
          },
          {
            "$ref": "#/components/parameters/Confirmation"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProgrammeChangeRequest"
              },
              "examples": {
                "change": {
                  "summary": "Make the change",
                  "value": {
                    "points_per_currency_unit": 2
                  }
                },
                "preview": {
                  "summary": "The same body, sent with `?dry_run=true`",
                  "value": {
                    "points_per_currency_unit": 2
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The change, or with `dry_run=true` the preview of it. A request already answered is answered again, byte for byte (`Idempotency-Replayed`).",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ProgrammeChange"
                    },
                    {
                      "$ref": "#/components/schemas/ProgrammeChangePreview"
                    }
                  ]
                },
                "examples": {
                  "change": {
                    "summary": "The change, made",
                    "value": {
                      "object": "programme_change",
                      "store_key": "example-store",
                      "version": "9b2e4c1d7a8f60519283a4b5c6d7e8f0",
                      "changed": [
                        "basePointsPerMajorUnit",
                        "rules"
                      ],
                      "as_paid": {
                        "earn_rate_per_major_unit": 2,
                        "earn_rates_per_major_unit": [
                          2
                        ],
                        "welcome_bonus_points": 0,
                        "ways_to_earn": []
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-00000000000e"
                    }
                  },
                  "preview": {
                    "summary": "The same request with `?dry_run=true`: nothing changes",
                    "value": {
                      "object": "programme_change_preview",
                      "dry_run": true,
                      "outcome": "would_apply",
                      "store_key": "example-store",
                      "changed": [
                        "basePointsPerMajorUnit",
                        "rules"
                      ],
                      "before_after": {
                        "points_per_currency_unit": {
                          "before": 1,
                          "after": 2
                        }
                      },
                      "currency": "GBP",
                      "examples": [
                        {
                          "order_amount": "10.00",
                          "points_before": 10,
                          "points_after": 20
                        },
                        {
                          "order_amount": "50.00",
                          "points_before": 50,
                          "points_after": 100
                        },
                        {
                          "order_amount": "100.00",
                          "points_before": 100,
                          "points_after": 200
                        }
                      ],
                      "point_value": {
                        "points_per_currency_unit_before": 100,
                        "points_per_currency_unit_after": 100
                      },
                      "liability": null,
                      "referral": null,
                      "spend_any_amount": null,
                      "version_after": "9b2e4c1d7a8f60519283a4b5c6d7e8f0",
                      "confirmation": {
                        "token": "mct_EXAMPLE.EXAMPLE",
                        "expires_at": "2026-09-16T10:10:00.000Z"
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-00000000000f"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ProgrammeForbidden"
          },
          "404": {
            "$ref": "#/components/responses/WriteNotFound"
          },
          "409": {
            "$ref": "#/components/responses/ProgrammeConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/ProgrammeUnprocessable"
          },
          "428": {
            "$ref": "#/components/responses/ConfirmationNeeded"
          },
          "429": {
            "$ref": "#/components/responses/ProgrammeLimited"
          },
          "503": {
            "$ref": "#/components/responses/ChangeNotRecorded"
          }
        }
      }
    },
    "/stores/{store_key}/programme/rewards": {
      "get": {
        "operationId": "listRewards",
        "tags": [
          "Stores"
        ],
        "summary": "The rewards a shopper can spend points on at this store",
        "security": [
          {
            "apiKey": [
              "read_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The effective rewards list.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RewardDocumentPage"
                },
                "examples": {
                  "example": {
                    "value": {
                      "data": [
                        {
                          "id": "reward_5",
                          "costPoints": 500,
                          "valueLabel": "5.00 GBP off",
                          "minSubtotal": "5.00",
                          "valueAmount": "5.00"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null,
                      "version": "3f1c9a2b7d4e5f60718293a4b5c6d7e8"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "addReward",
        "tags": [
          "Stores"
        ],
        "summary": "Add a reward shoppers can spend points on",
        "description": "The platform names the reward `reward_` and the next whole number, whatever its label says, never a number a reward or a code has already had. Every other reward, and every earn rule, is carried exactly as it is. The preview warns, and never refuses, when the reward is worth more than 10 times what its points are worth at the store's point value, or more than the smallest order it can be used on. Counts toward the 100 programme changes a day through the API.",
        "security": [
          {
            "apiKey": [
              "write_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/DryRun"
          },
          {
            "$ref": "#/components/parameters/Confirmation"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RewardRequest"
              },
              "examples": {
                "change": {
                  "summary": "Make the change",
                  "value": {
                    "cost_points": 1000,
                    "value_amount": "10.00"
                  }
                },
                "preview": {
                  "summary": "The same body, sent with `?dry_run=true`",
                  "value": {
                    "cost_points": 1000,
                    "value_amount": "10.00"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The change, or with `dry_run=true` the preview of it. A request already answered is answered again, byte for byte (`Idempotency-Replayed`).",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/RewardResult"
                    },
                    {
                      "$ref": "#/components/schemas/RewardPreview"
                    }
                  ]
                },
                "examples": {
                  "change": {
                    "summary": "The reward, added",
                    "value": {
                      "object": "reward",
                      "reward": {
                        "id": "reward_6",
                        "kind": "amount",
                        "cost_points": 1000,
                        "value_amount": "10.00",
                        "percent_off": null,
                        "min_subtotal": "0.00",
                        "label": "£10.00 off"
                      },
                      "warnings": [],
                      "store_key": "example-store",
                      "version": "9b2e4c1d7a8f60519283a4b5c6d7e8f1",
                      "changed": [
                        "rewards"
                      ],
                      "as_paid": {
                        "earn_rate_per_major_unit": 1,
                        "earn_rates_per_major_unit": [
                          1
                        ],
                        "welcome_bonus_points": 0,
                        "ways_to_earn": []
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000010"
                    }
                  },
                  "preview": {
                    "summary": "The same request with `?dry_run=true`: nothing changes",
                    "value": {
                      "object": "reward_preview",
                      "reward": {
                        "id": "reward_6",
                        "kind": "amount",
                        "cost_points": 1000,
                        "value_amount": "10.00",
                        "percent_off": null,
                        "min_subtotal": "0.00",
                        "label": "£10.00 off"
                      },
                      "warnings": [],
                      "dry_run": true,
                      "outcome": "would_apply",
                      "store_key": "example-store",
                      "changed": [
                        "rewards"
                      ],
                      "before_after": {
                        "reward": {
                          "before": null,
                          "after": {
                            "id": "reward_6",
                            "kind": "amount",
                            "cost_points": 1000,
                            "value_amount": "10.00",
                            "percent_off": null,
                            "min_subtotal": "0.00",
                            "label": "£10.00 off"
                          }
                        }
                      },
                      "currency": "GBP",
                      "examples": [
                        {
                          "order_amount": "10.00",
                          "points_before": 10,
                          "points_after": 10
                        },
                        {
                          "order_amount": "50.00",
                          "points_before": 50,
                          "points_after": 50
                        },
                        {
                          "order_amount": "100.00",
                          "points_before": 100,
                          "points_after": 100
                        }
                      ],
                      "point_value": {
                        "points_per_currency_unit_before": 100,
                        "points_per_currency_unit_after": 100
                      },
                      "liability": null,
                      "version_after": "9b2e4c1d7a8f60519283a4b5c6d7e8f1",
                      "confirmation": {
                        "token": "mct_EXAMPLE.EXAMPLE",
                        "expires_at": "2026-09-16T10:10:00.000Z"
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000011"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ProgrammeForbidden"
          },
          "404": {
            "$ref": "#/components/responses/WriteNotFound"
          },
          "409": {
            "$ref": "#/components/responses/ProgrammeConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "428": {
            "$ref": "#/components/responses/ConfirmationNeeded"
          },
          "429": {
            "$ref": "#/components/responses/ProgrammeLimited"
          },
          "503": {
            "$ref": "#/components/responses/ChangeNotRecorded"
          }
        }
      }
    },
    "/stores/{store_key}/programme/rewards/{reward_id}": {
      "post": {
        "operationId": "changeReward",
        "tags": [
          "Stores"
        ],
        "summary": "Change a reward's cost, value, minimum order or label",
        "description": "Only the fields sent change. Codes already issued for the reward keep the value they were issued with. The same warnings as an add.",
        "security": [
          {
            "apiKey": [
              "write_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          },
          {
            "$ref": "#/components/parameters/RewardId"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/DryRun"
          },
          {
            "$ref": "#/components/parameters/Confirmation"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RewardChangeRequest"
              },
              "examples": {
                "change": {
                  "summary": "Make the change",
                  "value": {
                    "cost_points": 800
                  }
                },
                "preview": {
                  "summary": "The same body, sent with `?dry_run=true`",
                  "value": {
                    "cost_points": 800
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The change, or with `dry_run=true` the preview of it. A request already answered is answered again, byte for byte (`Idempotency-Replayed`).",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/RewardResult"
                    },
                    {
                      "$ref": "#/components/schemas/RewardPreview"
                    }
                  ]
                },
                "examples": {
                  "change": {
                    "summary": "The reward, changed",
                    "value": {
                      "object": "reward",
                      "reward": {
                        "id": "reward_6",
                        "kind": "amount",
                        "cost_points": 800,
                        "value_amount": "10.00",
                        "percent_off": null,
                        "min_subtotal": "0.00",
                        "label": "£10.00 off"
                      },
                      "warnings": [],
                      "store_key": "example-store",
                      "version": "9b2e4c1d7a8f60519283a4b5c6d7e8f2",
                      "changed": [
                        "rewards"
                      ],
                      "as_paid": {
                        "earn_rate_per_major_unit": 1,
                        "earn_rates_per_major_unit": [
                          1
                        ],
                        "welcome_bonus_points": 0,
                        "ways_to_earn": []
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000012"
                    }
                  },
                  "preview": {
                    "summary": "The same request with `?dry_run=true`: nothing changes",
                    "value": {
                      "object": "reward_preview",
                      "reward": {
                        "id": "reward_6",
                        "kind": "amount",
                        "cost_points": 800,
                        "value_amount": "10.00",
                        "percent_off": null,
                        "min_subtotal": "0.00",
                        "label": "£10.00 off"
                      },
                      "warnings": [],
                      "dry_run": true,
                      "outcome": "would_apply",
                      "store_key": "example-store",
                      "changed": [
                        "rewards"
                      ],
                      "before_after": {
                        "reward": {
                          "before": {
                            "id": "reward_6",
                            "kind": "amount",
                            "cost_points": 1000,
                            "value_amount": "10.00",
                            "percent_off": null,
                            "min_subtotal": "0.00",
                            "label": "£10.00 off"
                          },
                          "after": {
                            "id": "reward_6",
                            "kind": "amount",
                            "cost_points": 800,
                            "value_amount": "10.00",
                            "percent_off": null,
                            "min_subtotal": "0.00",
                            "label": "£10.00 off"
                          }
                        }
                      },
                      "currency": "GBP",
                      "examples": [
                        {
                          "order_amount": "10.00",
                          "points_before": 10,
                          "points_after": 10
                        },
                        {
                          "order_amount": "50.00",
                          "points_before": 50,
                          "points_after": 50
                        },
                        {
                          "order_amount": "100.00",
                          "points_before": 100,
                          "points_after": 100
                        }
                      ],
                      "point_value": {
                        "points_per_currency_unit_before": 100,
                        "points_per_currency_unit_after": 100
                      },
                      "liability": null,
                      "version_after": "9b2e4c1d7a8f60519283a4b5c6d7e8f2",
                      "confirmation": {
                        "token": "mct_EXAMPLE.EXAMPLE",
                        "expires_at": "2026-09-16T10:10:00.000Z"
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000013"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ProgrammeForbidden"
          },
          "404": {
            "$ref": "#/components/responses/WriteNotFound"
          },
          "409": {
            "$ref": "#/components/responses/ProgrammeConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "428": {
            "$ref": "#/components/responses/ConfirmationNeeded"
          },
          "429": {
            "$ref": "#/components/responses/ProgrammeLimited"
          },
          "503": {
            "$ref": "#/components/responses/ChangeNotRecorded"
          }
        }
      }
    },
    "/stores/{store_key}/programme/rewards/{reward_id}/remove": {
      "post": {
        "operationId": "removeReward",
        "tags": [
          "Stores"
        ],
        "summary": "Remove a reward",
        "description": "Codes already issued for it stay valid. The store's only reward cannot be removed (409 `last_reward`): a programme with nothing to spend points on is a trap.",
        "security": [
          {
            "apiKey": [
              "write_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          },
          {
            "$ref": "#/components/parameters/RewardId"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/DryRun"
          },
          {
            "$ref": "#/components/parameters/Confirmation"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RewardRemoveRequest"
              },
              "examples": {
                "change": {
                  "summary": "Make the change",
                  "value": {}
                },
                "preview": {
                  "summary": "The same body, sent with `?dry_run=true`",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The change, or with `dry_run=true` the preview of it. A request already answered is answered again, byte for byte (`Idempotency-Replayed`).",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/RewardRemoved"
                    },
                    {
                      "$ref": "#/components/schemas/RewardRemovedPreview"
                    }
                  ]
                },
                "examples": {
                  "change": {
                    "summary": "The reward, removed",
                    "value": {
                      "object": "reward_removed",
                      "reward_id": "reward_6",
                      "store_key": "example-store",
                      "version": "9b2e4c1d7a8f60519283a4b5c6d7e8f3",
                      "changed": [
                        "rewards"
                      ],
                      "as_paid": {
                        "earn_rate_per_major_unit": 1,
                        "earn_rates_per_major_unit": [
                          1
                        ],
                        "welcome_bonus_points": 0,
                        "ways_to_earn": []
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000014"
                    }
                  },
                  "preview": {
                    "summary": "The same request with `?dry_run=true`: nothing changes",
                    "value": {
                      "object": "reward_removed_preview",
                      "reward_id": "reward_6",
                      "dry_run": true,
                      "outcome": "would_apply",
                      "store_key": "example-store",
                      "changed": [
                        "rewards"
                      ],
                      "before_after": {
                        "reward": {
                          "before": {
                            "id": "reward_6",
                            "kind": "amount",
                            "cost_points": 800,
                            "value_amount": "10.00",
                            "percent_off": null,
                            "min_subtotal": "0.00",
                            "label": "£10.00 off"
                          },
                          "after": null
                        }
                      },
                      "currency": "GBP",
                      "examples": [
                        {
                          "order_amount": "10.00",
                          "points_before": 10,
                          "points_after": 10
                        },
                        {
                          "order_amount": "50.00",
                          "points_before": 50,
                          "points_after": 50
                        },
                        {
                          "order_amount": "100.00",
                          "points_before": 100,
                          "points_after": 100
                        }
                      ],
                      "point_value": {
                        "points_per_currency_unit_before": 100,
                        "points_per_currency_unit_after": 100
                      },
                      "liability": null,
                      "version_after": "9b2e4c1d7a8f60519283a4b5c6d7e8f3",
                      "confirmation": {
                        "token": "mct_EXAMPLE.EXAMPLE",
                        "expires_at": "2026-09-16T10:10:00.000Z"
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000015"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ProgrammeForbidden"
          },
          "404": {
            "$ref": "#/components/responses/WriteNotFound"
          },
          "409": {
            "$ref": "#/components/responses/RewardConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "428": {
            "$ref": "#/components/responses/ConfirmationNeeded"
          },
          "429": {
            "$ref": "#/components/responses/ProgrammeLimited"
          },
          "503": {
            "$ref": "#/components/responses/ChangeNotRecorded"
          }
        }
      }
    },
    "/stores/{store_key}/programme/offers": {
      "get": {
        "operationId": "listOffers",
        "tags": [
          "Stores"
        ],
        "summary": "A store's offers",
        "security": [
          {
            "apiKey": [
              "read_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The store's offers.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfferPage"
                },
                "examples": {
                  "example": {
                    "value": {
                      "data": [
                        {
                          "id": "campaign_mfk2q",
                          "kind": "campaign",
                          "reason": "Double points week",
                          "read_only": false,
                          "ended": false,
                          "points_per_currency_unit": 1,
                          "starts_at": "2026-10-05T23:00:00.000Z",
                          "ends_at": "2026-10-12T23:00:00.000Z",
                          "starts_on": "2026-10-06",
                          "ends_on": "2026-10-12"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null,
                      "version": "3f1c9a2b7d4e5f60718293a4b5c6d7e8"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Campaigns, product offers and channel offers in the words the offer calls take, and every other rule as `other`, read-only. The everyday rate and the two bonuses are not offers: they are in `getProgramme`."
      },
      "post": {
        "operationId": "addOffer",
        "tags": [
          "Stores"
        ],
        "summary": "Add an offer: a campaign, a product offer or a channel offer",
        "description": "The platform makes the offer's id. Every other rule is carried exactly as it is. A campaign cannot start before today or end on a day already past, in the store's time zone; one starting today starts when it is made. The preview prices three orders under the offers running now, so a campaign starting later changes nothing in it yet.",
        "security": [
          {
            "apiKey": [
              "write_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/DryRun"
          },
          {
            "$ref": "#/components/parameters/Confirmation"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferRequest"
              },
              "examples": {
                "change": {
                  "summary": "Make the change",
                  "value": {
                    "kind": "campaign",
                    "reason": "Double points week",
                    "points_per_currency_unit": 1,
                    "starts_on": "2026-10-06",
                    "ends_on": "2026-10-12"
                  }
                },
                "preview": {
                  "summary": "The same body, sent with `?dry_run=true`",
                  "value": {
                    "kind": "campaign",
                    "reason": "Double points week",
                    "points_per_currency_unit": 1,
                    "starts_on": "2026-10-06",
                    "ends_on": "2026-10-12"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The change, or with `dry_run=true` the preview of it. A request already answered is answered again, byte for byte (`Idempotency-Replayed`).",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/OfferResult"
                    },
                    {
                      "$ref": "#/components/schemas/OfferPreview"
                    }
                  ]
                },
                "examples": {
                  "change": {
                    "summary": "The offer, added",
                    "value": {
                      "object": "offer",
                      "offer": {
                        "id": "campaign_mfk2q",
                        "kind": "campaign",
                        "reason": "Double points week",
                        "read_only": false,
                        "ended": false,
                        "points_per_currency_unit": 1,
                        "starts_at": "2026-10-05T23:00:00.000Z",
                        "ends_at": "2026-10-12T23:00:00.000Z",
                        "starts_on": "2026-10-06",
                        "ends_on": "2026-10-12"
                      },
                      "store_key": "example-store",
                      "version": "9b2e4c1d7a8f60519283a4b5c6d7e8f4",
                      "changed": [
                        "rules"
                      ],
                      "as_paid": {
                        "earn_rate_per_major_unit": 1,
                        "earn_rates_per_major_unit": [
                          1
                        ],
                        "welcome_bonus_points": 0,
                        "ways_to_earn": []
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000016"
                    }
                  },
                  "preview": {
                    "summary": "The same request with `?dry_run=true`: nothing changes",
                    "value": {
                      "object": "offer_preview",
                      "offer": {
                        "id": "campaign_mfk2q",
                        "kind": "campaign",
                        "reason": "Double points week",
                        "read_only": false,
                        "ended": false,
                        "points_per_currency_unit": 1,
                        "starts_at": "2026-10-05T23:00:00.000Z",
                        "ends_at": "2026-10-12T23:00:00.000Z",
                        "starts_on": "2026-10-06",
                        "ends_on": "2026-10-12"
                      },
                      "dry_run": true,
                      "outcome": "would_apply",
                      "store_key": "example-store",
                      "changed": [
                        "rules"
                      ],
                      "before_after": {},
                      "currency": "GBP",
                      "examples": [
                        {
                          "order_amount": "10.00",
                          "points_before": 10,
                          "points_after": 10
                        },
                        {
                          "order_amount": "50.00",
                          "points_before": 50,
                          "points_after": 50
                        },
                        {
                          "order_amount": "100.00",
                          "points_before": 100,
                          "points_after": 100
                        }
                      ],
                      "point_value": {
                        "points_per_currency_unit_before": 100,
                        "points_per_currency_unit_after": 100
                      },
                      "liability": null,
                      "version_after": "9b2e4c1d7a8f60519283a4b5c6d7e8f4",
                      "confirmation": {
                        "token": "mct_EXAMPLE.EXAMPLE",
                        "expires_at": "2026-09-16T10:10:00.000Z"
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000017"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ProgrammeForbidden"
          },
          "404": {
            "$ref": "#/components/responses/WriteNotFound"
          },
          "409": {
            "$ref": "#/components/responses/OfferConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "428": {
            "$ref": "#/components/responses/ConfirmationNeeded"
          },
          "429": {
            "$ref": "#/components/responses/ProgrammeLimited"
          },
          "503": {
            "$ref": "#/components/responses/ChangeNotRecorded"
          }
        }
      }
    },
    "/stores/{store_key}/programme/offers/{offer_id}": {
      "post": {
        "operationId": "changeOffer",
        "tags": [
          "Stores"
        ],
        "summary": "Change an offer",
        "description": "Send only what changes; `kind` cannot change. A campaign's start or end is rebuilt only from a day sent, so a window set up to start at a time of day keeps that time until its day is changed. An ended campaign cannot be changed (409 `offer_ended`), and a rule set up another way is read-only (409 `offer_read_only`).",
        "security": [
          {
            "apiKey": [
              "write_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          },
          {
            "$ref": "#/components/parameters/OfferId"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/DryRun"
          },
          {
            "$ref": "#/components/parameters/Confirmation"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferRequest"
              },
              "examples": {
                "change": {
                  "summary": "Make the change",
                  "value": {
                    "ends_on": "2026-10-19"
                  }
                },
                "preview": {
                  "summary": "The same body, sent with `?dry_run=true`",
                  "value": {
                    "ends_on": "2026-10-19"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The change, or with `dry_run=true` the preview of it. A request already answered is answered again, byte for byte (`Idempotency-Replayed`).",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/OfferResult"
                    },
                    {
                      "$ref": "#/components/schemas/OfferPreview"
                    }
                  ]
                },
                "examples": {
                  "change": {
                    "summary": "The offer, changed",
                    "value": {
                      "object": "offer",
                      "offer": {
                        "id": "campaign_mfk2q",
                        "kind": "campaign",
                        "reason": "Double points week",
                        "read_only": false,
                        "ended": false,
                        "points_per_currency_unit": 1,
                        "starts_at": "2026-10-05T23:00:00.000Z",
                        "ends_at": "2026-10-19T23:00:00.000Z",
                        "starts_on": "2026-10-06",
                        "ends_on": "2026-10-19"
                      },
                      "store_key": "example-store",
                      "version": "9b2e4c1d7a8f60519283a4b5c6d7e8f5",
                      "changed": [
                        "rules"
                      ],
                      "as_paid": {
                        "earn_rate_per_major_unit": 1,
                        "earn_rates_per_major_unit": [
                          1
                        ],
                        "welcome_bonus_points": 0,
                        "ways_to_earn": []
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000018"
                    }
                  },
                  "preview": {
                    "summary": "The same request with `?dry_run=true`: nothing changes",
                    "value": {
                      "object": "offer_preview",
                      "offer": {
                        "id": "campaign_mfk2q",
                        "kind": "campaign",
                        "reason": "Double points week",
                        "read_only": false,
                        "ended": false,
                        "points_per_currency_unit": 1,
                        "starts_at": "2026-10-05T23:00:00.000Z",
                        "ends_at": "2026-10-19T23:00:00.000Z",
                        "starts_on": "2026-10-06",
                        "ends_on": "2026-10-19"
                      },
                      "dry_run": true,
                      "outcome": "would_apply",
                      "store_key": "example-store",
                      "changed": [
                        "rules"
                      ],
                      "before_after": {},
                      "currency": "GBP",
                      "examples": [
                        {
                          "order_amount": "10.00",
                          "points_before": 10,
                          "points_after": 10
                        },
                        {
                          "order_amount": "50.00",
                          "points_before": 50,
                          "points_after": 50
                        },
                        {
                          "order_amount": "100.00",
                          "points_before": 100,
                          "points_after": 100
                        }
                      ],
                      "point_value": {
                        "points_per_currency_unit_before": 100,
                        "points_per_currency_unit_after": 100
                      },
                      "liability": null,
                      "version_after": "9b2e4c1d7a8f60519283a4b5c6d7e8f5",
                      "confirmation": {
                        "token": "mct_EXAMPLE.EXAMPLE",
                        "expires_at": "2026-09-16T10:10:00.000Z"
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000019"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ProgrammeForbidden"
          },
          "404": {
            "$ref": "#/components/responses/WriteNotFound"
          },
          "409": {
            "$ref": "#/components/responses/OfferConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "428": {
            "$ref": "#/components/responses/ConfirmationNeeded"
          },
          "429": {
            "$ref": "#/components/responses/ProgrammeLimited"
          },
          "503": {
            "$ref": "#/components/responses/ChangeNotRecorded"
          }
        }
      }
    },
    "/stores/{store_key}/programme/offers/{offer_id}/remove": {
      "post": {
        "operationId": "removeOffer",
        "tags": [
          "Stores"
        ],
        "summary": "Remove an offer",
        "description": "Any campaign, product or channel offer, ended or not. Orders it already paid keep their points. A rule set up another way is read-only (409 `offer_read_only`).",
        "security": [
          {
            "apiKey": [
              "write_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          },
          {
            "$ref": "#/components/parameters/OfferId"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/DryRun"
          },
          {
            "$ref": "#/components/parameters/Confirmation"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferRemoveRequest"
              },
              "examples": {
                "change": {
                  "summary": "Make the change",
                  "value": {}
                },
                "preview": {
                  "summary": "The same body, sent with `?dry_run=true`",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The change, or with `dry_run=true` the preview of it. A request already answered is answered again, byte for byte (`Idempotency-Replayed`).",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/OfferRemoved"
                    },
                    {
                      "$ref": "#/components/schemas/OfferRemovedPreview"
                    }
                  ]
                },
                "examples": {
                  "change": {
                    "summary": "The offer, removed",
                    "value": {
                      "object": "offer_removed",
                      "offer_id": "campaign_mfk2q",
                      "store_key": "example-store",
                      "version": "9b2e4c1d7a8f60519283a4b5c6d7e8f6",
                      "changed": [
                        "rules"
                      ],
                      "as_paid": {
                        "earn_rate_per_major_unit": 1,
                        "earn_rates_per_major_unit": [
                          1
                        ],
                        "welcome_bonus_points": 0,
                        "ways_to_earn": []
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-00000000001a"
                    }
                  },
                  "preview": {
                    "summary": "The same request with `?dry_run=true`: nothing changes",
                    "value": {
                      "object": "offer_removed_preview",
                      "offer_id": "campaign_mfk2q",
                      "dry_run": true,
                      "outcome": "would_apply",
                      "store_key": "example-store",
                      "changed": [
                        "rules"
                      ],
                      "before_after": {},
                      "currency": "GBP",
                      "examples": [
                        {
                          "order_amount": "10.00",
                          "points_before": 10,
                          "points_after": 10
                        },
                        {
                          "order_amount": "50.00",
                          "points_before": 50,
                          "points_after": 50
                        },
                        {
                          "order_amount": "100.00",
                          "points_before": 100,
                          "points_after": 100
                        }
                      ],
                      "point_value": {
                        "points_per_currency_unit_before": 100,
                        "points_per_currency_unit_after": 100
                      },
                      "liability": null,
                      "version_after": "9b2e4c1d7a8f60519283a4b5c6d7e8f6",
                      "confirmation": {
                        "token": "mct_EXAMPLE.EXAMPLE",
                        "expires_at": "2026-09-16T10:10:00.000Z"
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-00000000001b"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ProgrammeForbidden"
          },
          "404": {
            "$ref": "#/components/responses/WriteNotFound"
          },
          "409": {
            "$ref": "#/components/responses/OfferConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "428": {
            "$ref": "#/components/responses/ConfirmationNeeded"
          },
          "429": {
            "$ref": "#/components/responses/ProgrammeLimited"
          },
          "503": {
            "$ref": "#/components/responses/ChangeNotRecorded"
          }
        }
      }
    },
    "/stores/{store_key}/programme/rules": {
      "get": {
        "operationId": "listRules",
        "tags": [
          "Stores"
        ],
        "summary": "The earn rules in force at this store",
        "security": [
          {
            "apiKey": [
              "read_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The effective rules list.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentPage"
                },
                "examples": {
                  "example": {
                    "value": {
                      "data": [
                        {
                          "id": "base_earn",
                          "award": {
                            "type": "perMajorUnit",
                            "pointsPerUnit": 1
                          },
                          "event": "order_paid",
                          "reason": "Points earned on your order",
                          "condition": {
                            "type": "always"
                          }
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null,
                      "version": "3f1c9a2b7d4e5f60718293a4b5c6d7e8"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stores/{store_key}/design": {
      "get": {
        "operationId": "getDesign",
        "tags": [
          "Stores"
        ],
        "summary": "A store's design: the colours, words and look of the rewards panel, the product-page line and the account page",
        "description": "The settings the Design screen in your Shopify admin saves, every one present (anything unusable in what is stored reads as its default, as the shopper surfaces read it), and the design's `version` for a change to send back as `if_version`.",
        "security": [
          {
            "apiKey": [
              "read_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The design.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DesignDocument"
                },
                "examples": {
                  "example": {
                    "value": {
                      "object": "design",
                      "store_key": "example-store",
                      "version": "7c1e2d3f4a5b60718293a4b5c6d7e8f0",
                      "design": {
                        "programmeName": "Rewards",
                        "pointsName": "point",
                        "pointsNamePlural": "points",
                        "heading": "",
                        "storeName": "",
                        "signedOutPrompt": "Sign in to see your points.",
                        "rewardsHeading": "What you can get",
                        "earnHeading": "Ways to earn",
                        "activityHeading": "Recent activity",
                        "accent": null,
                        "cornerRadius": null,
                        "density": "comfortable",
                        "textScale": "normal",
                        "fontFamily": "theme",
                        "showRewards": true,
                        "showWaysToEarn": true,
                        "showActivity": true,
                        "showExpiry": true,
                        "showSharedWallet": true,
                        "showProductPoints": true,
                        "showRewardsSite": false,
                        "termsUrl": null,
                        "otherShops": {},
                        "productWidget": {
                          "variant": "boxed",
                          "icon": "none",
                          "lines": {
                            "earn": true,
                            "redeem": true,
                            "signin": true
                          },
                          "words": {
                            "earn": "",
                            "rate": "",
                            "ready": "",
                            "gap": "",
                            "signin": ""
                          }
                        },
                        "accountPage": {
                          "width": "base",
                          "cards": "outlined",
                          "corners": "base",
                          "button": "primary",
                          "progress": true,
                          "order": [
                            "codes",
                            "rewards",
                            "ways",
                            "activity",
                            "birthday",
                            "referral"
                          ]
                        },
                        "panel": {
                          "variant": "card",
                          "surface": null,
                          "layout": "grid",
                          "words": {
                            "signIn": "",
                            "use": ""
                          }
                        },
                        "brand": {
                          "logo": null,
                          "squareLogo": null,
                          "coverImage": null,
                          "colour": null,
                          "textColour": null,
                          "slogan": null,
                          "font": null
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "updateDesign",
        "tags": [
          "Stores"
        ],
        "summary": "Change the design: colours, words, the panel and product-line look, the account page layout, the terms link",
        "description": "The Design screen's save, through a key: each setting is checked exactly as that screen's save checks it, and a refusal names the setting (`param`) in the screen's own words. Only the settings sent change. The change is laid over one reading of the design, so a change made elsewhere in between is refused (409 `settings_changed`), never undone; a change to the programme's rates in between is not. The design it replaces is kept as the Design screen's one step of undo. A preview (`dry_run=true`) shows each setting that would change, before and after. Asking for the design already set changes nothing. Counts toward the 100 programme changes per account through the API in any 24 hours, and is written to the audit log in the same transaction as the change.",
        "security": [
          {
            "apiKey": [
              "write_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/DryRun"
          },
          {
            "$ref": "#/components/parameters/Confirmation"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DesignChangeRequest"
              },
              "examples": {
                "change": {
                  "summary": "Make the change",
                  "value": {
                    "accent": "#4f46e5",
                    "panel": {
                      "variant": "bold"
                    }
                  }
                },
                "preview": {
                  "summary": "The same body, sent with `?dry_run=true`",
                  "value": {
                    "accent": "#4f46e5",
                    "panel": {
                      "variant": "bold"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The change, or with `dry_run=true` the preview of it. A request already answered is answered again, byte for byte (`Idempotency-Replayed`).",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/DesignChange"
                    },
                    {
                      "$ref": "#/components/schemas/DesignChangePreview"
                    }
                  ]
                },
                "examples": {
                  "change": {
                    "summary": "The change, made",
                    "value": {
                      "object": "design_change",
                      "changed": [
                        "accent",
                        "panel"
                      ],
                      "design": {
                        "programmeName": "Rewards",
                        "pointsName": "point",
                        "pointsNamePlural": "points",
                        "heading": "",
                        "storeName": "",
                        "signedOutPrompt": "Sign in to see your points.",
                        "rewardsHeading": "What you can get",
                        "earnHeading": "Ways to earn",
                        "activityHeading": "Recent activity",
                        "accent": "#4f46e5",
                        "cornerRadius": null,
                        "density": "comfortable",
                        "textScale": "normal",
                        "fontFamily": "theme",
                        "showRewards": true,
                        "showWaysToEarn": true,
                        "showActivity": true,
                        "showExpiry": true,
                        "showSharedWallet": true,
                        "showProductPoints": true,
                        "showRewardsSite": false,
                        "termsUrl": null,
                        "otherShops": {},
                        "productWidget": {
                          "variant": "boxed",
                          "icon": "none",
                          "lines": {
                            "earn": true,
                            "redeem": true,
                            "signin": true
                          },
                          "words": {
                            "earn": "",
                            "rate": "",
                            "ready": "",
                            "gap": "",
                            "signin": ""
                          }
                        },
                        "accountPage": {
                          "width": "base",
                          "cards": "outlined",
                          "corners": "base",
                          "button": "primary",
                          "progress": true,
                          "order": [
                            "codes",
                            "rewards",
                            "ways",
                            "activity",
                            "birthday",
                            "referral"
                          ]
                        },
                        "panel": {
                          "variant": "bold",
                          "surface": null,
                          "layout": "grid",
                          "words": {
                            "signIn": "",
                            "use": ""
                          }
                        },
                        "brand": {
                          "logo": null,
                          "squareLogo": null,
                          "coverImage": null,
                          "colour": null,
                          "textColour": null,
                          "slogan": null,
                          "font": null
                        }
                      },
                      "store_key": "example-store",
                      "version": "7c1e2d3f4a5b60718293a4b5c6d7e8f1",
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000020"
                    }
                  },
                  "preview": {
                    "summary": "The same request with `?dry_run=true`: nothing changes",
                    "value": {
                      "object": "design_change_preview",
                      "dry_run": true,
                      "outcome": "would_apply",
                      "store_key": "example-store",
                      "changed": [
                        "accent",
                        "panel"
                      ],
                      "before_after": {
                        "accent": {
                          "before": null,
                          "after": "#4f46e5"
                        },
                        "panel": {
                          "before": {
                            "variant": "card",
                            "surface": null,
                            "layout": "grid",
                            "words": {
                              "signIn": "",
                              "use": ""
                            }
                          },
                          "after": {
                            "variant": "bold",
                            "surface": null,
                            "layout": "grid",
                            "words": {
                              "signIn": "",
                              "use": ""
                            }
                          }
                        }
                      },
                      "version_after": "7c1e2d3f4a5b60718293a4b5c6d7e8f1",
                      "confirmation": {
                        "token": "mct_EXAMPLE.EXAMPLE",
                        "expires_at": "2026-09-16T10:10:00.000Z"
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000021"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/DesignForbidden"
          },
          "404": {
            "$ref": "#/components/responses/DesignNotFound"
          },
          "409": {
            "$ref": "#/components/responses/DesignConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "428": {
            "$ref": "#/components/responses/ConfirmationNeeded"
          },
          "429": {
            "$ref": "#/components/responses/ProgrammeLimited"
          },
          "503": {
            "$ref": "#/components/responses/ChangeNotRecorded"
          }
        }
      }
    },
    "/members": {
      "get": {
        "operationId": "listMembers",
        "tags": [
          "Members"
        ],
        "summary": "Shoppers on this account",
        "description": "Recorded in the audit log as a disclosure, counted in shoppers.",
        "security": [
          {
            "apiKey": [
              "read_members"
            ]
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Email, member key or store customer reference. At most 120 characters.",
            "schema": {
              "type": "string",
              "maxLength": 120
            }
          },
          {
            "name": "store",
            "in": "query",
            "description": "Only members with a customer record at this store.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "member_key",
                "balance",
                "enrolled_at",
                "last_activity_at"
              ],
              "default": "member_key"
            }
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of members.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MemberPage"
                },
                "examples": {
                  "example": {
                    "value": {
                      "data": [
                        {
                          "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                          "email": "alex@example.com",
                          "balance_points": 640,
                          "enrolled_at": "2026-08-04T11:02:00Z",
                          "last_activity_at": "2026-08-04T11:02:00Z"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidListRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/AccessNotRecorded"
          }
        }
      }
    },
    "/members/{member_key}": {
      "get": {
        "operationId": "getMember",
        "tags": [
          "Members"
        ],
        "summary": "One shopper's record",
        "description": "Email is the only contact detail returned. The birthday a merchant's own staff screen can show is never returned here.",
        "security": [
          {
            "apiKey": [
              "read_members"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MemberKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The member.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                },
                "examples": {
                  "example": {
                    "value": {
                      "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                      "email": "alex@example.com",
                      "balance_points": 640,
                      "enrolled_at": "2026-08-04T11:02:00Z",
                      "last_activity_at": "2026-08-04T11:02:00Z",
                      "points_expire_at": null,
                      "stores": [
                        {
                          "store_key": "example-store",
                          "customer_ref": "gid://shopify/Customer/7001",
                          "linked_at": "2026-08-04T11:02:00Z"
                        }
                      ],
                      "points_by_store": [
                        {
                          "store_key": "example-store",
                          "store_name": "Example Store",
                          "earned_points": 640,
                          "spent_points": 0,
                          "other_points": 0
                        }
                      ],
                      "outstanding_codes": 1,
                      "history_url": "/api/2026-10/ledger?member_key=mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/AccessNotRecorded"
          }
        }
      }
    },
    "/members/{member_key}/codes": {
      "get": {
        "operationId": "listMemberCodes",
        "tags": [
          "Members"
        ],
        "summary": "The reward codes this shopper holds",
        "security": [
          {
            "apiKey": [
              "read_members"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MemberKey"
          },
          {
            "name": "status",
            "in": "query",
            "description": "'minted' is issued and unspent.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "The codes.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CodePage"
                },
                "examples": {
                  "example": {
                    "value": {
                      "data": [
                        {
                          "code_ending": "4TPB",
                          "ref": "5f0c2d8e91a47b36c0de",
                          "store_key": "example-store",
                          "reward_id": "reward_5",
                          "cost_points": 500,
                          "status": "minted",
                          "issued_at": "2026-09-10T16:40:00Z"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidCursor"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/AccessNotRecorded"
          }
        },
        "description": "Paged: a shopper who has redeemed for two years has a history, so this list is keyset-paged like the others. Each code is named by its last four characters and its `ref`, never whole — and so is the cursor, which is opaque but not secret."
      }
    },
    "/members/{member_key}/adjustments": {
      "post": {
        "operationId": "adjustMemberPoints",
        "tags": [
          "Members"
        ],
        "summary": "Add points to a member's balance, or take them away",
        "description": "Booked to the member's earliest-linked store on this account, with the reason on the shopper's statement. At most 100,000 points either way per change, and per account at most 100,000 points added and 100,000 taken away by points changes through the API in any rolling 24 hours, across all its keys (a cancelled code's points are not counted). A test key can preview and is refused the change (403 `test_mode_key`). Written to the audit log in the same transaction as the change: if the entry cannot be written, the change is not made.",
        "security": [
          {
            "apiKey": [
              "write_members"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MemberKey"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/DryRun"
          },
          {
            "$ref": "#/components/parameters/Confirmation"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdjustmentRequest"
              },
              "examples": {
                "change": {
                  "summary": "Make the change",
                  "value": {
                    "delta_points": 50,
                    "reason": "Sorry about the late parcel"
                  }
                },
                "preview": {
                  "summary": "The same body, sent with `?dry_run=true`",
                  "value": {
                    "delta_points": 50,
                    "reason": "Sorry about the late parcel"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The change, or with `dry_run=true` the preview of it.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/AdjustmentResult"
                    },
                    {
                      "$ref": "#/components/schemas/AdjustmentPreview"
                    }
                  ]
                },
                "examples": {
                  "change": {
                    "summary": "The change, made",
                    "value": {
                      "object": "adjustment",
                      "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                      "delta_points": 50,
                      "balance_points": 690,
                      "store_key": "example-store",
                      "replayed": false,
                      "idempotency_key": "goodwill-2026-09-16-0001",
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-00000000000d"
                    }
                  },
                  "preview": {
                    "summary": "The same request with `?dry_run=true`: nothing changes",
                    "value": {
                      "object": "adjustment_preview",
                      "dry_run": true,
                      "outcome": "would_apply",
                      "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                      "delta_points": 50,
                      "balance_points_before": 640,
                      "balance_points_after": 690,
                      "store_key": "example-store",
                      "limit": {
                        "direction": "credit",
                        "limit_points": 100000,
                        "used_points": 0,
                        "remaining_points": 100000
                      },
                      "confirmation": {
                        "token": "mct_EXAMPLE.EXAMPLE",
                        "expires_at": "2026-09-16T10:10:00.000Z"
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-00000000000b"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/WriteForbidden"
          },
          "404": {
            "$ref": "#/components/responses/WriteNotFound"
          },
          "409": {
            "$ref": "#/components/responses/WriteConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "428": {
            "$ref": "#/components/responses/ConfirmationNeeded"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/ChangeNotRecorded"
          }
        }
      }
    },
    "/members/{member_key}/codes/void": {
      "post": {
        "operationId": "cancelMemberCode",
        "tags": [
          "Members"
        ],
        "summary": "Cancel a reward code and give the member back its points",
        "description": "Switches the code off at the store it belongs to and credits its cost back — never one without the other. Name the code by its `ref` from `listMemberCodes`; the code itself is never sent or returned. The attempt is written to the audit log before Shopify is asked. A preview (`dry_run=true`) never contacts Shopify, so a store problem shows only on the real call. A cancel is not counted against the daily points limits on `adjustMemberPoints`: it gives back points the shopper already spent on the code.",
        "security": [
          {
            "apiKey": [
              "write_members"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MemberKey"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/DryRun"
          },
          {
            "$ref": "#/components/parameters/Confirmation"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VoidRequest"
              },
              "examples": {
                "change": {
                  "summary": "Make the change",
                  "value": {
                    "ref": "5f0c2d8e91a47b36c0de"
                  }
                },
                "preview": {
                  "summary": "The same body, sent with `?dry_run=true`",
                  "value": {
                    "ref": "5f0c2d8e91a47b36c0de"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The change, or with `dry_run=true` the preview of it.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Idempotency-Replayed": {
                "$ref": "#/components/headers/IdempotencyReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/VoidResult"
                    },
                    {
                      "$ref": "#/components/schemas/VoidPreview"
                    }
                  ]
                },
                "examples": {
                  "change": {
                    "summary": "The change, made",
                    "value": {
                      "object": "code_void",
                      "result": "voided",
                      "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                      "code_ending": "4TPB",
                      "store_key": "example-store",
                      "balance_points": 1190,
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000011"
                    }
                  },
                  "preview": {
                    "summary": "The same request with `?dry_run=true`: nothing changes",
                    "value": {
                      "object": "code_void_preview",
                      "dry_run": true,
                      "outcome": "would_void",
                      "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                      "code_ending": "4TPB",
                      "store_key": "example-store",
                      "cost_points": 500,
                      "balance_points_before": 690,
                      "balance_points_after": 1190,
                      "confirmation": {
                        "token": "mct_EXAMPLE.EXAMPLE",
                        "expires_at": "2026-09-16T10:10:00.000Z"
                      },
                      "audit_id": "0d1e2f30-4a5b-4c6d-8e7f-00000000000f"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/WriteForbidden"
          },
          "404": {
            "$ref": "#/components/responses/WriteNotFound"
          },
          "409": {
            "$ref": "#/components/responses/VoidConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "428": {
            "$ref": "#/components/responses/ConfirmationNeeded"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamFailed"
          },
          "503": {
            "$ref": "#/components/responses/VoidUnavailable"
          }
        }
      }
    },
    "/ledger": {
      "get": {
        "operationId": "listLedger",
        "tags": [
          "Reports"
        ],
        "summary": "Every points movement on this account, newest first",
        "description": "The ledger is append-only: a correction is a later row, never an edit to an earlier one.",
        "security": [
          {
            "apiKey": [
              "read_reports"
            ]
          }
        ],
        "parameters": [
          {
            "name": "store",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "member_key",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of ledger entries.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LedgerPage"
                },
                "examples": {
                  "example": {
                    "value": {
                      "data": [
                        {
                          "id": 3,
                          "at": "2026-09-16T10:00:00.000Z",
                          "store_key": "example-store",
                          "store_name": "Example Store",
                          "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                          "delta_points": 500,
                          "reason": "Reward removed — points returned",
                          "rule_id": "redeem_void",
                          "source_type": "redeem_void",
                          "source_ref": null,
                          "order_ref": null,
                          "code_ending": "4TPB",
                          "code_ref": "5f0c2d8e91a47b36c0de"
                        },
                        {
                          "id": 2,
                          "at": "2026-09-16T10:00:00.000Z",
                          "store_key": "example-store",
                          "store_name": "Example Store",
                          "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                          "delta_points": 50,
                          "reason": "Sorry about the late parcel",
                          "rule_id": "admin_adjustment",
                          "source_type": "admin_adjustment",
                          "source_ref": "admin|api.Kx7Qp2Lm9Vt4Rz8N|goodwill-2026-09-16-0001|mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18|50",
                          "order_ref": null,
                          "code_ending": null,
                          "code_ref": null
                        },
                        {
                          "id": 1,
                          "at": "2026-08-04T11:02:00Z",
                          "store_key": "example-store",
                          "store_name": "Example Store",
                          "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                          "delta_points": 640,
                          "reason": "Points earned on your order",
                          "rule_id": "base_earn",
                          "source_type": "order",
                          "source_ref": "o-1001",
                          "order_ref": null,
                          "code_ending": null,
                          "code_ref": null
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidListRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/AccessNotRecorded"
          }
        }
      }
    },
    "/reports/liability": {
      "get": {
        "operationId": "getLiabilityReport",
        "tags": [
          "Reports"
        ],
        "summary": "What this account owes its shoppers",
        "description": "Summed from the ledger, not from the balance cache. The money value is null with a reason rather than a guess when the stores redeem at different rates or none is configured.",
        "security": [
          {
            "apiKey": [
              "read_reports"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The liability report.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LiabilityReport"
                },
                "examples": {
                  "example": {
                    "value": {
                      "as_at": "2026-09-16T10:00:00Z",
                      "redeemable_points": 1190,
                      "net_points": 1190,
                      "members_in_credit": 1,
                      "total_members": 1,
                      "points_expiring_within_days": 0,
                      "expiring_within_days": 90,
                      "outstanding_codes": [],
                      "valuation": {
                        "currency": "GBP",
                        "minor_units": 1190,
                        "major_units": "11.90"
                      },
                      "valuation_unavailable": null,
                      "redeem_rate_by_store": [
                        {
                          "store_key": "example-outlet",
                          "currency": "GBP",
                          "redeem_points_per_major_unit": 100
                        },
                        {
                          "store_key": "example-store",
                          "currency": "GBP",
                          "redeem_points_per_major_unit": 100
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/reports/settlement": {
      "get": {
        "operationId": "getSettlementReport",
        "tags": [
          "Reports"
        ],
        "summary": "Points earned at one store and spent at another, inside a window",
        "description": "The window is two absolute UTC instants. The report keeps the engine's own field names.",
        "security": [
          {
            "apiKey": [
              "read_reports"
            ]
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "description": "The start of the window: an instant in UTC, written with a `Z` and to at most the millisecond, such as 2026-09-16T10:00:00Z. A date alone, or a finer fraction of a second, is refused with 400 `invalid_request`.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-01T00:00:00Z"
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "description": "The end of the window, written the same way and later than `from`. The report stops just before it: anything recorded at exactly that instant is left out. A date alone, or a finer fraction of a second, is refused with 400 `invalid_request`.",
            "schema": {
              "type": "string"
            },
            "example": "2026-10-01T00:00:00Z"
          }
        ],
        "responses": {
          "200": {
            "description": "The settlement report.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettlementEnvelope"
                },
                "examples": {
                  "example": {
                    "value": {
                      "report": {
                        "accountId": "acc_example",
                        "window": {
                          "from": "2026-09-01T00:00:00Z",
                          "to": "2026-09-16T00:00:00Z"
                        },
                        "lines": [],
                        "totals": {
                          "redeemedPoints": 0,
                          "consumedPoints": 0,
                          "attributedPoints": 0,
                          "unattributedPoints": 0,
                          "valueByCurrency": {}
                        },
                        "attributionComplete": true,
                        "anomalies": [],
                        "unpricedSpendSites": [],
                        "unknownSpendSites": [],
                        "clampedFutureRows": 0,
                        "ledgerDigest": "17b799bbaf2815941767da47eebbc881fac4e92211c327b92723cad37f5003b7"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/AccessNotRecorded"
          }
        }
      }
    },
    "/usage": {
      "get": {
        "operationId": "getUsage",
        "tags": [
          "Billing"
        ],
        "summary": "Orders that earned points this billing month, against the plan's cap",
        "description": "The plan comes from the subscription this platform has recorded from Shopify's billing webhooks, not from a live call to Shopify.",
        "security": [
          {
            "apiKey": [
              "read_usage"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Usage this month.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                },
                "examples": {
                  "example": {
                    "value": {
                      "used": 0,
                      "cap": null,
                      "percent": null,
                      "level": "ok",
                      "counts": "orders that earned points",
                      "month": {
                        "label": "2026-09",
                        "from": "2026-09-01T00:00:00.000Z",
                        "to": "2026-10-01T00:00:00.000Z"
                      },
                      "plan_source": "recorded_subscription",
                      "plan_name": "Multi-store wallet"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/plan": {
      "get": {
        "operationId": "getPlan",
        "tags": [
          "Billing"
        ],
        "summary": "The subscriptions recorded for this account",
        "description": "A read, and only a read. A plan is changed on Shopify's own page.",
        "security": [
          {
            "apiKey": [
              "read_usage"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The recorded subscriptions.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanPage"
                },
                "examples": {
                  "example": {
                    "value": {
                      "data": [
                        {
                          "store_key": "example-store",
                          "plan_name": "Multi-store wallet",
                          "status": "active",
                          "price": {
                            "amount": "79.00",
                            "currency": "USD"
                          },
                          "billing_interval": "EVERY_30_DAYS",
                          "trial_days": 14,
                          "is_test": false,
                          "capabilities": [
                            "shared_wallet"
                          ],
                          "activated_at": "2026-08-03T09:20:00Z"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null,
                      "change_plan_at": "the app in your Shopify admin"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/audit-log": {
      "get": {
        "operationId": "listAuditLog",
        "tags": [
          "Log"
        ],
        "summary": "What every key changed, and what it was shown",
        "description": "Append-only. A disclosure row says how many shoppers a read carried; a change row says what changed. `object_ref` names a shopper, so it is returned only to a key that also holds `read_members` — and that read is itself recorded.",
        "security": [
          {
            "apiKey": [
              "read_audit"
            ]
          }
        ],
        "parameters": [
          {
            "name": "actor",
            "in": "query",
            "description": "One key id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of log entries.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuditPage"
                },
                "examples": {
                  "example": {
                    "value": {
                      "data": [
                        {
                          "id": "0d1e2f30-4a5b-4c6d-8e7f-00000000001b",
                          "at": "2026-09-16T10:00:00Z",
                          "actor_type": "api_key",
                          "actor_id": "Kx7Qp2Lm9Vt4Rz8N",
                          "actor_name": "Nightly export",
                          "client": "example-script/1.0",
                          "request_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000008",
                          "action": "member.read",
                          "store_key": null,
                          "object_ref": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                          "detail": {
                            "subject_count": 1
                          },
                          "result": "disclosed",
                          "reason": null
                        },
                        {
                          "id": "0d1e2f30-4a5b-4c6d-8e7f-00000000001c",
                          "at": "2026-09-16T10:00:00Z",
                          "actor_type": "api_key",
                          "actor_id": "Kx7Qp2Lm9Vt4Rz8N",
                          "actor_name": "Nightly export",
                          "client": "example-script/1.0",
                          "request_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000013",
                          "action": "ledger.read",
                          "store_key": null,
                          "object_ref": null,
                          "detail": {
                            "subject_count": 1
                          },
                          "result": "disclosed",
                          "reason": null
                        },
                        {
                          "id": "0d1e2f30-4a5b-4c6d-8e7f-00000000001d",
                          "at": "2026-09-16T10:00:00Z",
                          "actor_type": "api_key",
                          "actor_id": "Kx7Qp2Lm9Vt4Rz8N",
                          "actor_name": "Nightly export",
                          "client": "example-script/1.0",
                          "request_id": "0d1e2f30-4a5b-4c6d-8e7f-000000000007",
                          "action": "members.list",
                          "store_key": null,
                          "object_ref": null,
                          "detail": {
                            "subject_count": 1
                          },
                          "result": "disclosed",
                          "reason": null
                        }
                      ],
                      "has_more": true,
                      "next_cursor": "eyJhdCI6IjIwMjYtMDktMTZUMTA6MDA6MDBaIiwiaWQiOiIwZDFlMmYzMCJ9"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidListRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/keys": {
      "get": {
        "operationId": "listKeys",
        "tags": [
          "Account"
        ],
        "summary": "The API keys on this account",
        "description": "Never the secret half: the row keeps a keyed hash of it. A key whose plaintext was lost is rotated, not recovered. Keys are created, rotated and revoked in the app's API keys screen, behind a Shopify staff session — no scope mints a key, because a key that could mint keys could widen its own reach.",
        "security": [
          {
            "apiKey": [
              "manage_keys"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The keys.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyPage"
                },
                "examples": {
                  "example": {
                    "value": {
                      "data": [
                        {
                          "id": "Kx7Qp2Lm9Vt4Rz8N",
                          "name": "Nightly export",
                          "mode": "live",
                          "scopes": [
                            "manage_keys",
                            "read_audit",
                            "read_members",
                            "read_programme",
                            "read_reports",
                            "read_usage",
                            "write_members",
                            "write_webhooks"
                          ],
                          "agent": false,
                          "limits_lifted": false,
                          "created_at": "2026-09-01T08:00:00Z",
                          "last_used_at": "2026-09-16T10:00:00Z",
                          "revoked_at": null,
                          "rotation_grace_until": null
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/keys/self/revoke": {
      "post": {
        "operationId": "revokeThisKey",
        "tags": [
          "Account"
        ],
        "summary": "Stop the key making this request, immediately",
        "description": "Needs no scope: presenting the key is the authorisation. It takes no identifier, so it cannot revoke any key but the one signing the request — which is what makes it safe to leave unscoped. Use it the moment a key is believed to have leaked; a merchant can also revoke any key from the app's API keys screen. The revoke and its audit entry are one transaction, and so is deleting every webhook endpoint the key created (`webhook.delete` for each, with the reason and the key's id).",
        "responses": {
          "200": {
            "description": "The key is revoked. It will not authenticate again.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SelfRevoked"
                },
                "examples": {
                  "example": {
                    "value": {
                      "ok": true,
                      "key_id": "3D1wN9KfD8595Al5",
                      "revoked_at": "2026-09-16T10:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "404": {
            "description": "`not_found`: this key was revoked a moment ago, by another request.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "operationId": "listWebhookEndpoints",
        "tags": [
          "Webhooks"
        ],
        "summary": "The webhook endpoints on this account, and why any stopped",
        "description": "Deleted endpoints are not listed. Needs `write_webhooks`, because an endpoint's URL can carry the merchant's own token. Written to the audit log (`webhooks.list`) before it answers.",
        "security": [
          {
            "apiKey": [
              "write_webhooks"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "The endpoints.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointPage"
                },
                "examples": {
                  "example": {
                    "value": {
                      "data": [
                        {
                          "object": "webhook_endpoint",
                          "id": "we_Q3mN8xR2vT6pL9wK4sJ7hF1d",
                          "url": "https://hooks.example.com/matrix-rewards",
                          "event_types": [
                            "points.earned",
                            "points.adjusted"
                          ],
                          "description": "Our CRM",
                          "status": "enabled",
                          "created_at": "2026-09-16T10:00:00.000Z",
                          "disabled_at": null,
                          "disabled_reason": null,
                          "consecutive_failures": 0,
                          "failing_since": null,
                          "last_success_at": null,
                          "last_failure_at": null
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/WebhookForbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/AccessNotRecorded"
          }
        }
      },
      "post": {
        "operationId": "createWebhookEndpoint",
        "tags": [
          "Webhooks"
        ],
        "summary": "Send points movements to an https endpoint",
        "description": "Returns the endpoint and its signing `secret`, once. At most 5 enabled endpoints per account. The endpoint receives the events it subscribes to from the moment it is created — not before. Takes no Idempotency-Key: a stored answer would have to keep the secret, so a retry after a lost answer makes a second endpoint, which shows in the list and can be deleted. The endpoint and its audit entry (`webhook.create`, recording the URL's host only) are one transaction. The endpoint lasts as long as the key that created it: revoking that key, or ending it with the website seat that made it, deletes the endpoint too (`webhook.delete`, with the reason and the key's id); so does the signing out everywhere, or removal, of the website seat whose secret created it, whatever has become of the key since. A key's previous secret, still working while a rotation's grace lasts, cannot create one.",
        "security": [
          {
            "apiKey": [
              "write_webhooks"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEndpointRequest"
              },
              "examples": {
                "example": {
                  "value": {
                    "url": "https://hooks.example.com/matrix-rewards",
                    "event_types": [
                      "points.earned",
                      "points.adjusted"
                    ],
                    "description": "Our CRM"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The endpoint, with its secret.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointCreated"
                },
                "examples": {
                  "example": {
                    "value": {
                      "object": "webhook_endpoint",
                      "id": "we_Q3mN8xR2vT6pL9wK4sJ7hF1d",
                      "url": "https://hooks.example.com/matrix-rewards",
                      "event_types": [
                        "points.earned",
                        "points.adjusted"
                      ],
                      "description": "Our CRM",
                      "status": "enabled",
                      "created_at": "2026-09-16T10:00:00.000Z",
                      "disabled_at": null,
                      "disabled_reason": null,
                      "consecutive_failures": 0,
                      "failing_since": null,
                      "last_success_at": null,
                      "last_failure_at": null,
                      "secret": "whsec_EXAMPLE"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/WebhookForbidden"
          },
          "409": {
            "$ref": "#/components/responses/WebhookLimit"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/WebhookNotRecorded"
          }
        }
      }
    },
    "/webhooks/{endpoint_id}": {
      "delete": {
        "operationId": "deleteWebhookEndpoint",
        "tags": [
          "Webhooks"
        ],
        "summary": "Stop sending to an endpoint",
        "description": "Nothing more is sent to it, and events waiting to be sent are abandoned. The endpoint and its audit entry (`webhook.delete`) change together.",
        "security": [
          {
            "apiKey": [
              "write_webhooks"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/EndpointId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointDeleted"
                },
                "examples": {
                  "example": {
                    "value": {
                      "object": "webhook_endpoint_deleted",
                      "id": "we_Q3mN8xR2vT6pL9wK4sJ7hF1d",
                      "deleted": true,
                      "abandoned_messages": 0
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/WebhookForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/WebhookNotRecorded"
          }
        }
      }
    },
    "/webhooks/{endpoint_id}/test": {
      "post": {
        "operationId": "testWebhookEndpoint",
        "tags": [
          "Webhooks"
        ],
        "summary": "Send one signed test event now",
        "description": "A `webhook.test` event, signed with the endpoint's secret and sent through the same checks as every delivery, answered with what happened. It is not stored or retried, and it never counts towards disabling the endpoint. Recorded in the audit log (`webhook.test`) before it is sent. Works on a disabled endpoint too, so a fixed receiver can be checked.",
        "security": [
          {
            "apiKey": [
              "write_webhooks"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/EndpointId"
          }
        ],
        "responses": {
          "200": {
            "description": "What the receiver did. `delivered` is false for every failure; the request itself succeeded.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookTestResult"
                },
                "examples": {
                  "example": {
                    "value": {
                      "object": "webhook_test",
                      "id": "evt_test_4c0a9e7d21b3f58e6a0d9c12",
                      "delivered": true,
                      "status": 200,
                      "error": null,
                      "duration_ms": 212
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/WebhookForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/WebhookNotRecorded"
          }
        }
      }
    },
    "/stores/{store_key}/brand": {
      "get": {
        "operationId": "getStoreBranding",
        "tags": [
          "Stores"
        ],
        "summary": "A store's name and branding as the Matrix Rewards website shows them, and what Shopify has",
        "description": "The finished name, logos, cover, colours and slogan (each the design's setting, else Shopify's), what Shopify has for the shop, and the design's branding settings. Change them with `POST /stores/{store_key}/design` (`storeName`, `brand`).",
        "security": [
          {
            "apiKey": [
              "read_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The branding.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StoreBranding"
                },
                "examples": {
                  "example": {
                    "value": {
                      "object": "store_branding",
                      "store_key": "example-store",
                      "name": "Example Store",
                      "slogan": "Small batch, made well",
                      "logo_url": "https://app.matrixrewards.co.uk/brand-images/img_3f9a1c0e5b7d42a8916c0d2e4f6a8b1c",
                      "square_logo_url": "https://cdn.shopify.com/s/files/1/0000/0001/files/square.png",
                      "cover_image_url": null,
                      "colour": "#1a5c5c",
                      "text_colour": "#ffffff",
                      "font": "playfair-display",
                      "from_shopify": {
                        "name": "Example Store",
                        "slogan": "Small batch, made well",
                        "shortDescription": null,
                        "logo": null,
                        "squareLogo": "https://cdn.shopify.com/s/files/1/0000/0001/files/square.png",
                        "coverImage": null,
                        "colour": "#1a5c5c",
                        "textColour": "#ffffff"
                      },
                      "shopify_read_at": "2026-09-16T10:00:00Z",
                      "design": {
                        "logo": "img_3f9a1c0e5b7d42a8916c0d2e4f6a8b1c",
                        "squareLogo": null,
                        "coverImage": null,
                        "colour": null,
                        "textColour": null,
                        "slogan": null,
                        "font": null
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/stores/{store_key}/images": {
      "get": {
        "operationId": "listBrandImages",
        "tags": [
          "Stores"
        ],
        "summary": "The pictures uploaded to a store, newest first",
        "description": "Every picture kept for the store (at most 20), for the design's `brand` settings to name.",
        "security": [
          {
            "apiKey": [
              "read_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The pictures.",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandImageList"
                },
                "examples": {
                  "example": {
                    "value": {
                      "data": [
                        {
                          "object": "brand_image",
                          "id": "img_3f9a1c0e5b7d42a8916c0d2e4f6a8b1c",
                          "store_key": "example-store",
                          "content_type": "image/png",
                          "width": 512,
                          "height": 512,
                          "bytes": 24816,
                          "source": "upload",
                          "url": "https://app.matrixrewards.co.uk/brand-images/img_3f9a1c0e5b7d42a8916c0d2e4f6a8b1c",
                          "created_at": "2026-09-16T10:00:00Z"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "uploadBrandImage",
        "tags": [
          "Stores"
        ],
        "summary": "Upload a picture for the store's branding (a logo, a square logo, a cover)",
        "description": "Send the file itself (`Content-Type: image/png`, `image/jpeg` or `image/webp`, up to 524,288 bytes, 16 to 4096 pixels a side), or JSON naming a picture in your Shopify Files (`source_url`, https://cdn.shopify.com/ only). The type is read from the file's own bytes; SVG and anything else are refused. The same picture twice is the same picture (`created: false`). Then name its `id` in the design: `POST /stores/{store_key}/design` with `{ \"brand\": { \"logo\": \"img_…\" } }`. A store keeps at most 20; the oldest the design does not use makes room.",
        "security": [
          {
            "apiKey": [
              "write_programme"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/StoreKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "image/png": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/jpeg": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/webp": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BrandImageFromShopifyFiles"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kept (`created: true`), or the same picture already kept (`created: false`).",
            "headers": {
              "Request-Id": {
                "$ref": "#/components/headers/RequestId"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandImageUploaded"
                },
                "examples": {
                  "example": {
                    "value": {
                      "object": "brand_image",
                      "id": "img_3f9a1c0e5b7d42a8916c0d2e4f6a8b1c",
                      "store_key": "example-store",
                      "content_type": "image/png",
                      "width": 512,
                      "height": 512,
                      "bytes": 24816,
                      "source": "upload",
                      "url": "https://app.matrixrewards.co.uk/brand-images/img_3f9a1c0e5b7d42a8916c0d2e4f6a8b1c",
                      "created_at": "2026-09-16T10:00:00Z",
                      "created": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/BrandImageForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/BrandImageConflict"
          },
          "413": {
            "$ref": "#/components/responses/BrandImageTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/BrandImageUnprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/BrandImageUpstream"
          },
          "503": {
            "$ref": "#/components/responses/BrandImageNotRecorded"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "mrk_live_<key id>.<secret><checksum>",
        "description": "An account API key, created in the app. Scopes are held by the key, and a key can hold only the scopes listed in `Scope`: the ones some operation here checks, each named in that operation's `security`. `write_members` also covers `read_members`: a key that may change members' points may read them. A scope a key was given that is not on that list grants nothing, not even a read it might seem to cover, and is left out of what `/me` and `/keys` report. A key made for an AI assistant can never hold `write_webhooks`."
      }
    },
    "parameters": {
      "StoreKey": {
        "name": "store_key",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "example": "example-store"
      },
      "MemberKey": {
        "name": "member_key",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "example": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18"
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "description": "1 to 100. Larger values are capped.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 25
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "description": "Opaque. Pass back `next_cursor` from the previous page, with the same filters, and on `GET /members` the same `sort` and `order`.",
        "schema": {
          "type": "string"
        }
      },
      "From": {
        "name": "from",
        "in": "query",
        "description": "2026-09-16 or 2026-09-16T10:00:00Z. A date means the whole of that day (a `to` of 2026-09-16 includes 23:59 on the 16th).",
        "schema": {
          "type": "string"
        }
      },
      "To": {
        "name": "to",
        "in": "query",
        "description": "2026-09-16 or 2026-09-16T10:00:00Z. A date means the whole of that day (a `to` of 2026-09-16 includes 23:59 on the 16th).",
        "schema": {
          "type": "string"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "Required on both writes, a points change and a cancel. Pick a new one for each new change and send the same one to retry it. For 24 hours from its first use, a key has three answers: the same request again gets the first answer back, byte for byte, with `Idempotency-Replayed: true`; a different request under the key is 422 `idempotency_key_reused`; and while the first request is still running, 409 `idempotency_key_in_use` with `Retry-After`. A 5xx does not use the key up: retry with the same one. For a points change the key is also the adjustment's id in the ledger, so the same change is never booked twice, whichever of the account's keys sends it, and even after the 24 hours.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9._:-]{1,100}$"
        },
        "examples": {
          "adjustMemberPoints": {
            "summary": "A points change",
            "value": "goodwill-2026-09-16-0001"
          },
          "cancelMemberCode": {
            "summary": "A cancel",
            "value": "cancel-2026-09-16-0001"
          },
          "updateProgramme": {
            "summary": "A programme change",
            "value": "programme-2026-09-16-0001"
          },
          "addReward": {
            "summary": "A new reward",
            "value": "reward-add-2026-09-16-0001"
          },
          "changeReward": {
            "summary": "A reward change",
            "value": "reward-change-2026-09-16-0001"
          },
          "removeReward": {
            "summary": "A reward removed",
            "value": "reward-remove-2026-09-16-0001"
          },
          "addOffer": {
            "summary": "A new offer",
            "value": "offer-add-2026-09-16-0001"
          },
          "changeOffer": {
            "summary": "An offer change",
            "value": "offer-change-2026-09-16-0001"
          },
          "removeOffer": {
            "summary": "An offer removed",
            "value": "offer-remove-2026-09-16-0001"
          },
          "updateDesign": {
            "summary": "A design change",
            "value": "design-2026-09-16-0001"
          }
        }
      },
      "DryRun": {
        "name": "dry_run",
        "in": "query",
        "required": false,
        "description": "`true` previews the change: every check the real call makes runs, the answer has the status the real call would have, and nothing changes — except one `dry_run` entry in the audit log, because a preview shows the member's balance. When the change would happen, the preview carries a `confirmation` for exactly this request. It is the only query parameter a write takes: any other, a misspelling such as `dryRun` included, is refused with 400 `invalid_request` naming it in `param`, and nothing changes.",
        "schema": {
          "type": "boolean"
        }
      },
      "Confirmation": {
        "name": "Matrix-Confirmation",
        "in": "header",
        "required": false,
        "description": "The `confirmation.token` a dry run of this exact request returned. Required for keys tagged for an assistant, except on a replay: a request whose first answer is still kept, or a points change that is already made; checked whenever it is sent. It covers one request — the same key, member, body and Idempotency-Key — for 10 minutes. It proves the change was previewed first; it cannot prove a person saw the preview.",
        "schema": {
          "type": "string",
          "maxLength": 1024
        }
      },
      "EndpointId": {
        "name": "endpoint_id",
        "in": "path",
        "required": true,
        "description": "A webhook endpoint on this account (`we_…`). Another account's id answers 404 `not_found`, exactly as one that never existed.",
        "schema": {
          "type": "string",
          "pattern": "^we_[A-Za-z0-9_-]{16,32}$"
        },
        "example": "we_Q3mN8xR2vT6pL9wK4sJ7hF1d"
      },
      "RewardId": {
        "name": "reward_id",
        "in": "path",
        "required": true,
        "description": "A reward's `id`, as `listRewards` gives it.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9._-]{1,64}$"
        }
      },
      "OfferId": {
        "name": "offer_id",
        "in": "path",
        "required": true,
        "description": "An offer's `id`, as `listOffers` gives it.",
        "schema": {
          "type": "string",
          "pattern": "^[a-z0-9_]{1,64}$"
        }
      }
    },
    "headers": {
      "RequestId": {
        "description": "This request's id, the same as a problem's `request_id`. Quote it if you contact support.",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "RateLimitLimit": {
        "description": "The bucket's size: how many calls of this kind, reads or writes, this key may make at once.",
        "required": true,
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Whole calls of this kind left in the bucket after this one.",
        "required": true,
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitReset": {
        "description": "Unix seconds when the bucket is full again.",
        "required": true,
        "schema": {
          "type": "integer"
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before sending the request again.",
        "required": true,
        "schema": {
          "type": "integer"
        }
      },
      "RetryAfterWhenInUse": {
        "description": "Only with `idempotency_key_in_use`: seconds to wait before sending the same request again for its answer.",
        "required": false,
        "schema": {
          "type": "integer"
        }
      },
      "WwwAuthenticate": {
        "description": "Names the scheme a key is sent with: `Bearer`.",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "IdempotencyReplayed": {
        "description": "Present, `true`, when this is the stored answer to an earlier request with the same Idempotency-Key.",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      }
    },
    "responses": {
      "Unauthenticated": {
        "description": "`unauthenticated`: no usable API key was presented. Malformed, unknown, revoked and wrong-secret are one answer.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "WWW-Authenticate": {
            "$ref": "#/components/headers/WwwAuthenticate"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ForbiddenScope": {
        "description": "`forbidden_scope`: the key does not carry the scope this call needs; `required_scope` says which.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "NotFound": {
        "description": "`not_found`: no such resource on this account — indistinguishable from one that does not exist at all.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "WriteNotFound": {
        "description": "`not_found`: no such member on this account, or (cancelling a code) the member holds no code with that `ref`.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Idempotency-Replayed": {
            "$ref": "#/components/headers/IdempotencyReplayed"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "InvalidRequest": {
        "description": "`invalid_request`: a parameter or the body could not be read; `param` names it.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "InvalidListRequest": {
        "description": "`invalid_request`: a parameter could not be read; `param` names it. `invalid_cursor`: the `cursor` is not one this list issued.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "InvalidCursor": {
        "description": "`invalid_cursor`: the `cursor` is not one this list issued.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "RateLimited": {
        "description": "`rate_limited`: too many requests for this key; `bucket` says which allowance. `Retry-After` says how long to wait.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "AccessNotRecorded": {
        "description": "`access_not_recorded`: the read could not be written to the account's audit log, so no data was returned.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "WriteForbidden": {
        "description": "`forbidden_scope`: the key does not hold `write_members` (`required_scope` says so). `test_mode_key`: a test key can preview a change and never make one.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ProgrammeForbidden": {
        "description": "`forbidden_scope`: the key does not hold `write_programme` (`required_scope` says so). `test_mode_key`: a test key can preview a change and never make one. `assistant_limit`: the key is an assistant's (every Claude connection, and a key made for an assistant), the merchant has not lifted its fixed limits (`/me` `key.limits_lifted`; only a person can lift them, on the app's API keys screen), and the change moves the store's money further than one step. It is measured from the programme as it stood before the assistant's changes began, so steps add up; that starting point holds until `assistant_window_hours` pass with no assistant change, and a change by the owner to what the programme pays starts afresh (a new label or reason does not; nor does a change by an assistant key whose limits are lifted). What a point is worth, the currency and when points expire never change. What an order earns at most doubles: a new offer counts as paying every order, against what every order earned; and the everyday rate, which still prices some refunds (an API refund that names no order, and an order paid before amounts were recorded), is not lowered to fund an offer (then the figures are the everyday rate's own, `least_allowed` is the least it may be for every order to earn what it did, up to the field's two decimal places — null when no rate within its own step would do — and on an offer's route no `param` is named). Every other figure that pays shoppers at most doubles, one at zero stays there, and referrals that are off stay off. Spending any amount that is off stays off; while it is on, the fewest points one code may take and the smallest order it can be used on at most halve, and the most points at most double (a limit cannot be removed); switching it off is never limited. An offer may only narrow who and when it pays. A reward at most doubles what it gives for each point, keeps its kind, and its smallest order at most halves; a new reward stays within one step of a reward of its kind. The preview is refused the same way. `problem` says which limit; `before`, `asked` and `most_allowed` or `least_allowed` give the figure in the unit of `param`, and sending that limit back is accepted.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "`payload_too_large`: the body is over 4,096 bytes.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "WriteConflict": {
        "description": "`idempotency_key_in_use`: a request with this key is still running (`Retry-After`). `credit_limit_reached`: the account has moved 100,000 points through the API in this direction in the last 24 hours (`direction`, `limit_points`, `used_points`). `member_has_no_store`: the member is linked to no store to book the change to.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfterWhenInUse"
          },
          "Idempotency-Replayed": {
            "$ref": "#/components/headers/IdempotencyReplayed"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "VoidConflict": {
        "description": "`idempotency_key_in_use`: a request with this key is still running (`Retry-After`). `code_already_used`: the code was spent at checkout.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfterWhenInUse"
          },
          "Idempotency-Replayed": {
            "$ref": "#/components/headers/IdempotencyReplayed"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "IdempotencyKeyReused": {
        "description": "`idempotency_key_reused`: this Idempotency-Key was used for a different request in the last 24 hours.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Idempotency-Replayed": {
            "$ref": "#/components/headers/IdempotencyReplayed"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ConfirmationNeeded": {
        "description": "`confirmation_required`: this key is tagged for an assistant; preview with `dry_run=true` and send the returned confirmation. `confirmation_invalid`: the confirmation is not for this request, has expired, or is not one we issued (`reason`).",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ChangeNotRecorded": {
        "description": "`change_not_recorded` (or `access_not_recorded` for a preview or a replay): the audit log could not be written, so nothing was changed or returned. Retry with the same Idempotency-Key.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "VoidUnavailable": {
        "description": "`change_not_recorded` / `access_not_recorded`: as for a points change. `store_not_connected`: the code's store is not connected, so it cannot be switched off; nothing was changed.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "UpstreamFailed": {
        "description": "`upstream_failed`: Shopify did not confirm the code was switched off. Nothing was credited and the code is still live; retry with the same Idempotency-Key.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "WebhookForbidden": {
        "description": "`forbidden_scope`: the key does not hold `write_webhooks` (`required_scope` says so). `forbidden_for_assistant`: a key made for an AI assistant can never manage webhooks. `test_mode_key`: nor can a test key — an endpoint sends real data out. `previous_secret` (creating an endpoint only): the request was signed with the key's previous secret, which still works while a rotation's grace lasts; create endpoints with the current secret.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "WebhookLimit": {
        "description": "`webhook_limit_reached`: the account already has 5 enabled endpoints (`max_endpoints`). Delete one first.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "WebhookNotRecorded": {
        "description": "`change_not_recorded`: the audit log could not be written, so nothing was done — no endpoint made or deleted, no test sent. Try again.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ProgrammeConflict": {
        "description": "`idempotency_key_in_use`: a request with this key is still running (`Retry-After`). `settings_changed`: the programme changed after it was read — by `if_version`, or by the reading this change was built over — so nothing was changed; read it again and resend.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfterWhenInUse"
          },
          "Idempotency-Replayed": {
            "$ref": "#/components/headers/IdempotencyReplayed"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ProgrammeUnprocessable": {
        "description": "`idempotency_key_reused`: this Idempotency-Key was used for a different request in the last 24 hours. `retroactive_change`: the expiry change would expire points sooner than the rule shoppers have already been told about, or switch expiry on from a date already past (`problem`, and the current rule when there is one).",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Idempotency-Replayed": {
            "$ref": "#/components/headers/IdempotencyReplayed"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ProgrammeLimited": {
        "description": "`rate_limited`: too many requests for this key (`Retry-After`). `programme_write_limit`: this account has made 100 programme changes through the API in the last 24 hours (`limit`, `used`); nothing was changed.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "RewardConflict": {
        "description": "`idempotency_key_in_use`: a request with this key is still running (`Retry-After`). `settings_changed`: the programme changed after it was read, so nothing was changed. `last_reward`: this is the store's only reward, and a programme with nothing to spend points on is not allowed; add another first.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfterWhenInUse"
          },
          "Idempotency-Replayed": {
            "$ref": "#/components/headers/IdempotencyReplayed"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "OfferConflict": {
        "description": "`idempotency_key_in_use`: a request with this key is still running (`Retry-After`). `settings_changed`: the programme changed after it was read, so nothing was changed. `offer_read_only`: the rule was set up in a way the API does not change; change it in the app. `offer_ended`: the campaign has ended, so it can only be removed.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfterWhenInUse"
          },
          "Idempotency-Replayed": {
            "$ref": "#/components/headers/IdempotencyReplayed"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "DesignForbidden": {
        "description": "`forbidden_scope`: the key does not hold `write_programme` (`required_scope` says so). `test_mode_key`: a test key can preview a change and never make one.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "DesignNotFound": {
        "description": "`not_found`: no store with that key on this account.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Idempotency-Replayed": {
            "$ref": "#/components/headers/IdempotencyReplayed"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "DesignConflict": {
        "description": "`idempotency_key_in_use`: a request with this key is still running (`Retry-After`). `settings_changed`: the design changed after it was read — by `if_version`, or by the reading this change was built over — so nothing was changed; read it again and resend.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfterWhenInUse"
          },
          "Idempotency-Replayed": {
            "$ref": "#/components/headers/IdempotencyReplayed"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "BrandImageForbidden": {
        "description": "`forbidden_scope`: the key does not hold `write_programme` (`required_scope` says so). `test_mode_key`: a test key keeps nothing.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "BrandImageConflict": {
        "description": "`too_many_images`: the store keeps at most 20 pictures and the design uses every one.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "BrandImageTooLarge": {
        "description": "`payload_too_large`: the picture is over 524,288 bytes, or the JSON body over 4,096 bytes.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "BrandImageUnprocessable": {
        "description": "`invalid_image`: not a PNG, JPEG or WebP by its own bytes, or under 16 or over 4096 pixels on a side; or sent with another Content-Type.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "BrandImageUpstream": {
        "description": "`shopify_file_unavailable`: Shopify did not send the picture at `source_url`. Nothing was kept; try again, or upload the file.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "BrandImageNotRecorded": {
        "description": "`change_not_recorded`: the picture was kept but the audit log could not be written. Send it again: the same picture is not kept twice.",
        "headers": {
          "Request-Id": {
            "$ref": "#/components/headers/RequestId"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "required": [
          "type",
          "title",
          "status",
          "code",
          "request_id",
          "doc_url"
        ],
        "properties": {
          "type": {
            "type": "string",
            "description": "A page on the documentation site for this code: `https://matrixrewards.co.uk/docs/errors/<code>`."
          },
          "title": {
            "type": "string",
            "description": "A short summary of the code, for people. Branch on `code`, not on this."
          },
          "status": {
            "type": "integer",
            "description": "The HTTP status of the answer, repeated in the body."
          },
          "code": {
            "type": "string",
            "enum": [
              "invalid_request",
              "invalid_cursor",
              "unauthenticated",
              "forbidden_scope",
              "not_found",
              "unsupported_version",
              "rate_limited",
              "internal",
              "access_not_recorded",
              "payload_too_large",
              "idempotency_key_reused",
              "idempotency_key_in_use",
              "confirmation_required",
              "confirmation_invalid",
              "test_mode_key",
              "credit_limit_reached",
              "member_has_no_store",
              "code_already_used",
              "store_not_connected",
              "upstream_failed",
              "change_not_recorded",
              "forbidden_for_assistant",
              "webhook_limit_reached",
              "settings_changed",
              "retroactive_change",
              "programme_write_limit",
              "last_reward",
              "offer_read_only",
              "offer_ended",
              "programme_not_set_up",
              "assistant_limit",
              "invalid_image",
              "too_many_images",
              "shopify_file_unavailable",
              "previous_secret"
            ],
            "description": "What went wrong, as a word a program can branch on."
          },
          "detail": {
            "type": "string",
            "description": "What happened, in a sentence for people."
          },
          "instance": {
            "type": "string",
            "description": "The path that was called."
          },
          "param": {
            "type": "string",
            "description": "The field, header or query parameter at fault, when there is exactly one."
          },
          "request_id": {
            "type": "string",
            "description": "This request's id, also sent as the `Request-Id` header. Quote it if you contact support."
          },
          "doc_url": {
            "type": "string",
            "description": "The same page as `type`: this code's page on the documentation site."
          },
          "bucket": {
            "type": "string",
            "enum": [
              "read",
              "write"
            ],
            "description": "With `rate_limited`: which allowance ran out, reads or writes."
          },
          "required_scope": {
            "$ref": "#/components/schemas/Scope",
            "description": "With `forbidden_scope`: the scope this call needs, which the key does not hold."
          },
          "key_scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Scope"
            },
            "description": "With `forbidden_scope`: every scope this key holds."
          },
          "supported_versions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "With `unsupported_version`: the versions this platform serves."
          },
          "max_bytes": {
            "type": "integer",
            "description": "With `payload_too_large`: the largest body accepted, in bytes."
          },
          "max_points": {
            "type": "integer",
            "description": "With `invalid_request` on `delta_points`: the largest change allowed, either way."
          },
          "max_chars": {
            "type": "integer",
            "description": "With `invalid_request` on `reason`: the longest reason allowed."
          },
          "max_endpoints": {
            "type": "integer",
            "description": "With `webhook_limit_reached`: how many enabled endpoints an account may have."
          },
          "reason": {
            "type": "string",
            "enum": [
              "malformed",
              "expired",
              "wrong_request",
              "not_a_url",
              "too_long",
              "not_https",
              "port_not_allowed",
              "has_credentials",
              "has_fragment",
              "blocked_name",
              "own_host",
              "blocked_address",
              "does_not_resolve"
            ],
            "description": "With `confirmation_invalid`: `expired` (preview again), `wrong_request` (the key, member, body or Idempotency-Key differ from the preview) or `malformed` (not a confirmation this API issued). With `invalid_request` on a webhook `url`: what is wrong with the address, as listed under `WebhookEndpointRequest`."
          },
          "direction": {
            "type": "string",
            "enum": [
              "credit",
              "debit"
            ],
            "description": "With `credit_limit_reached`: `credit` for points added, `debit` for points taken away."
          },
          "limit_points": {
            "type": "integer",
            "description": "With `credit_limit_reached`: the account's limit in this direction."
          },
          "used_points": {
            "type": "integer",
            "description": "With `credit_limit_reached`: what the account has already moved in this direction in the window."
          },
          "upstream_reason": {
            "type": "string",
            "description": "With `upstream_failed`: why Shopify's answer did not confirm the code was switched off."
          },
          "problem": {
            "type": "string",
            "description": "With `invalid_request` or `retroactive_change` from a programme change: the platform's own reason, like `campaign_starts_in_past` or `retroactive_start`. With `assistant_limit`, which limit: `point_value_locked`, `currency_locked`, `expiry_locked`, `more_than_double`, `raised_from_zero`, `earning_more_than_double`, `everyday_rate_moved`, `changes_how_it_pays`, `changes_who_it_pays`, `changes_what_it_gives`, `minimum_more_than_halved`, `new_offer_kind`, `new_reward_too_generous` or `new_reward_kind`."
          },
          "current_effective_from": {
            "type": [
              "string",
              "null"
            ],
            "description": "With `retroactive_change`: when the expiry rule shoppers were told about took effect, or null when there is none."
          },
          "current_period_months": {
            "type": [
              "integer",
              "null"
            ],
            "description": "With `retroactive_change`: that rule's period in months, or null when there is none."
          },
          "before": {
            "type": [
              "number",
              "string",
              "boolean",
              "null"
            ],
            "description": "With `assistant_limit`: the figure at the assistant's starting point, in the unit of `param` (a rate, points, an amount, a percentage, a kind; true or false for whether referrals run)."
          },
          "asked": {
            "type": [
              "number",
              "string",
              "boolean",
              "null"
            ],
            "description": "With `assistant_limit`: what the change asked for, in the same unit."
          },
          "most_allowed": {
            "type": [
              "number",
              "string",
              "boolean",
              "null"
            ],
            "description": "With `assistant_limit`, when the figure was raised: the most it may rise to. Null where no value of it would do."
          },
          "least_allowed": {
            "type": [
              "number",
              "string",
              "boolean",
              "null"
            ],
            "description": "With `assistant_limit`, when a cost or a smallest order was lowered, or the everyday rate lowered while an offer rose: the least it may fall to. Null where no value of it would do."
          },
          "limit": {
            "type": "integer",
            "description": "With `programme_write_limit`: how many programme changes the account may make through the API in the window."
          },
          "used": {
            "type": "integer",
            "description": "With `programme_write_limit`: how many it has made in the window."
          }
        },
        "additionalProperties": false
      },
      "Scope": {
        "type": "string",
        "description": "A scope a key can hold. This list is exactly the scopes some operation checks. `write_members` covers the two writes (changing a member's points, cancelling a reward code) and also covers `read_members`. The self-revoke needs no scope. `write_webhooks` covers the webhook endpoints — listing, creating, deleting and testing them — and a key for an AI assistant can never hold it. `write_programme` covers changing the programme (`POST /stores/{store_key}/programme`); a key for an assistant may hold it, and must preview each change and send back its confirmation.",
        "enum": [
          "read_programme",
          "read_members",
          "read_reports",
          "read_usage",
          "read_audit",
          "manage_keys",
          "write_members",
          "write_webhooks",
          "write_programme"
        ],
        "x-enum-descriptions": {
          "read_programme": "Your programme settings — rates, rewards, rules",
          "read_members": "Members, their points, their email addresses and the last four characters of their reward codes",
          "read_reports": "The ledger, what you owe, and settlement between stores",
          "read_usage": "Orders used this month and which plan you are on",
          "read_audit": "The activity log — what each key did and saw",
          "manage_keys": "The list of your keys (not their secrets)",
          "write_members": "Change members’ points and cancel reward codes. This also reads members, their email addresses and the last four characters of their reward codes. Every change is logged",
          "write_webhooks": "Send every change to members’ points, by member key, to a web address the key chooses (webhooks). Not for an assistant key",
          "write_programme": "Change your programme: the earn rate, the joining and birthday bonuses, what a point is worth, when points expire, the rewards and the offers. Every change is previewable and logged"
        }
      },
      "Store": {
        "type": "object",
        "required": [
          "key",
          "name",
          "platform",
          "external_ref",
          "created_at",
          "display_name"
        ],
        "additionalProperties": false,
        "properties": {
          "key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "platform": {
            "type": "string",
            "description": "'shopify' today; the engine ingests events, so another platform is another producer."
          },
          "external_ref": {
            "type": [
              "string",
              "null"
            ],
            "description": "That platform's own identifier — a myshopify domain, for instance."
          },
          "created_at": {
            "type": "string"
          },
          "display_name": {
            "type": "string",
            "description": "The store's name as every page shows it: the design's `storeName`, else the name Shopify has for the shop now, else the name it was installed with. `name` stays the name it was installed with."
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "api_version",
          "account",
          "stores",
          "key",
          "rate_limit"
        ],
        "additionalProperties": false,
        "properties": {
          "api_version": {
            "type": "string"
          },
          "account": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "id",
              "name",
              "wallet_scope",
              "created_at"
            ],
            "additionalProperties": false,
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "wallet_scope": {
                "type": "string",
                "enum": [
                  "site",
                  "account"
                ],
                "description": "'account' means one balance shared across the account's stores."
              },
              "created_at": {
                "type": "string"
              }
            }
          },
          "stores": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Store"
            }
          },
          "key": {
            "type": "object",
            "required": [
              "id",
              "name",
              "mode",
              "scopes",
              "agent",
              "limits_lifted",
              "used_previous_secret"
            ],
            "additionalProperties": false,
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "mode": {
                "type": "string",
                "enum": [
                  "live",
                  "test"
                ]
              },
              "scopes": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Scope"
                }
              },
              "agent": {
                "type": "boolean",
                "description": "Tagged by the merchant as being for an AI assistant."
              },
              "limits_lifted": {
                "type": "boolean",
                "description": "True when the merchant has lifted this assistant key's fixed limits on the API keys screen of the Matrix Rewards app: its programme changes are then not held to the assistant limits (what a point is worth, expiry, one step a day). It still needs a preview's confirmation for every change, and still cannot manage webhooks. Only a person can lift or restore the limits, in the app; no API call can. Always false for a key that is not for an assistant."
              },
              "used_previous_secret": {
                "type": "boolean",
                "description": "True when this call used the previous secret during a rotation grace window."
              }
            }
          },
          "rate_limit": {
            "type": "object",
            "required": [
              "limit",
              "remaining",
              "reset"
            ],
            "additionalProperties": false,
            "properties": {
              "limit": {
                "type": "integer"
              },
              "remaining": {
                "type": "integer"
              },
              "reset": {
                "type": "integer",
                "description": "Unix seconds at which the bucket is full again."
              }
            }
          }
        }
      },
      "StorePage": {
        "type": "object",
        "required": [
          "data",
          "has_more",
          "next_cursor"
        ],
        "additionalProperties": false,
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Store"
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "DocumentPage": {
        "type": "object",
        "description": "A list of programme documents in the engine's own field names.",
        "required": [
          "data",
          "has_more",
          "next_cursor",
          "version"
        ],
        "additionalProperties": false,
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "Which stored programme this is: it changes whenever the store's programme settings change (not its look or expiry), so two reads with the same version saw the same programme. Send it as `if_version` with a programme change (`POST /stores/{store_key}/programme`) and the change is refused, 409 `settings_changed`, if the programme has changed since you read it. The embedded admin's save checks the same version."
          }
        }
      },
      "Programme": {
        "type": "object",
        "required": [
          "store_key",
          "config",
          "effective",
          "as_paid",
          "appearance",
          "expiry",
          "version"
        ],
        "additionalProperties": false,
        "properties": {
          "store_key": {
            "type": "string"
          },
          "config": {
            "type": "object",
            "additionalProperties": true,
            "description": "What the merchant has stored. Empty until they configure it."
          },
          "effective": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "required": [
              "spendAnyAmount"
            ],
            "properties": {
              "spendAnyAmount": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "enabled",
                  "minPoints",
                  "maxPoints",
                  "minSubtotal"
                ],
                "description": "Spend any amount, in the engine's own names. Off (`enabled` false) at every store until it is switched on.",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "description": "Whether a shopper may choose how many points to spend."
                  },
                  "minPoints": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "The fewest points one code may take, before it is rounded up to a whole step."
                  },
                  "maxPoints": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "description": "The most points one code may take, before it is rounded down to a whole step; null for no limit of the store's own (one spend never takes more than 10,000,000 points)."
                  },
                  "minSubtotal": {
                    "type": "string",
                    "description": "The smallest order a code can be used on, in the store's currency; \"0.00\" for any order."
                  }
                }
              }
            },
            "description": "The settings in force: what is stored, over built-in defaults, in the engine's own field names. These are SETTINGS, not what shoppers are paid, and three of them can disagree with what they are paid. `basePointsPerMajorUnit` is a stored setting nothing earns by: orders are paid by the always-on `perMajorUnit` rules in `rules`. `welcomeBonusPoints` is a stored setting nothing pays: the joining bonus is the `welcome_bonus` rule, and a store with no such rule pays 0. `waysToEarn` is as stored, and a sentence in it that states the rate, an app-order bonus or the joining bonus may have been written for rules the store no longer has. Do not quote any of the three to shoppers: quote `as_paid`. `bonusesInAppOnly` is always present, and false unless set: true means the store's welcome and birthday bonuses are paid only on its own app sign-in, never on a web page. Only Matrix Rewards sets it. `spendAnyAmount` is always present: whether a shopper may choose how many points to spend, at `redeemPointsPerMajorUnit`, and the limits on one code. Change it with `spend_any_amount` (POST /stores/{store_key}/programme). While it is off, the limits shown are the ones kept for when it is switched on again; a `minPoints` never set is the points worth one unit of the store's currency at `redeemPointsPerMajorUnit`."
          },
          "as_paid": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "earn_rate_per_major_unit",
              "earn_rates_per_major_unit",
              "welcome_bonus_points",
              "ways_to_earn"
            ],
            "additionalProperties": false,
            "description": "What a shopper is actually paid, worked out from the rules the same way the storefront and the account page work it out. These are the figures to show a shopper. Null when the store has no programme, which cannot price an order.",
            "properties": {
              "earn_rate_per_major_unit": {
                "type": "number",
                "minimum": 0,
                "description": "Points an order pays per whole unit of the store's currency: the sum of the rules that apply to every order. Conditional bonuses (an app-order bonus, a campaign window, a product rule) are left out, because they hold only for some orders. 0 means nothing pays per unit; say nothing about a rate rather than \"earn 0\"."
              },
              "earn_rates_per_major_unit": {
                "type": "array",
                "items": {
                  "type": "number",
                  "minimum": 0
                },
                "description": "The same rules one by one. To price an amount, round each rate times the amount DOWN on its own and add the results: that is how an order is paid, and one rounding over the sum can promise a point more than the order pays."
              },
              "welcome_bonus_points": {
                "type": "integer",
                "minimum": 0,
                "description": "The joining bonus a new shopper is paid, once. 0 when the store pays none, whatever `effective.welcomeBonusPoints` says. At a store whose welcome and birthday bonuses are paid only in its own app (the programme's `effective.bonusesInAppOnly`), it is paid only on the store's own app sign-in, never on a web page: do not offer it there as a web sign-up bonus. The platform template's own welcome sentence in `ways_to_earn` says so."
              },
              "ways_to_earn": {
                "type": "array",
                "description": "`effective.waysToEarn` as a shopper is shown it. The base-rate sentence the app wrote is rebuilt from `earn_rate_per_major_unit`, and left out when that is 0. On a store built from one of the platform's templates, the template's own sentences about the rate, the app-order bonus and the joining bonus are served as written while the rules still pay what they say, corrected when a figure has moved, and left out when nothing pays them. Entries the merchant wrote are unchanged.",
                "items": {
                  "type": "object",
                  "required": [
                    "id",
                    "label",
                    "detail"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "appearance": {
            "type": "object",
            "additionalProperties": true,
            "description": "Resolved colours, wording and toggles for the shopper-facing surfaces."
          },
          "expiry": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "period_months",
              "effective_from"
            ],
            "additionalProperties": false,
            "properties": {
              "period_months": {
                "type": "integer",
                "description": "At least 12 — the platform's floor."
              },
              "effective_from": {
                "type": "string"
              }
            }
          },
          "version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "Which stored programme this is: it changes whenever the store's programme settings change (not its look or expiry), so two reads with the same version saw the same programme. Send it as `if_version` with a programme change (`POST /stores/{store_key}/programme`) and the change is refused, 409 `settings_changed`, if the programme has changed since you read it. The embedded admin's save checks the same version."
          }
        }
      },
      "MemberSummary": {
        "type": "object",
        "required": [
          "member_key",
          "email",
          "balance_points",
          "enrolled_at",
          "last_activity_at"
        ],
        "additionalProperties": false,
        "properties": {
          "member_key": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "balance_points": {
            "type": "integer",
            "description": "The maintained balance, which the engine recomputes from the ledger under a lock on every write. `GET /members/{member_key}` sums the ledger rows there and then; the two agree unless something is wrong, and that is the only difference between them."
          },
          "enrolled_at": {
            "type": "string"
          },
          "last_activity_at": {
            "type": "string"
          }
        }
      },
      "MemberPage": {
        "type": "object",
        "required": [
          "data",
          "has_more",
          "next_cursor"
        ],
        "additionalProperties": false,
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MemberSummary"
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Member": {
        "type": "object",
        "required": [
          "member_key",
          "email",
          "balance_points",
          "enrolled_at",
          "last_activity_at",
          "points_expire_at",
          "stores",
          "points_by_store",
          "outstanding_codes",
          "history_url"
        ],
        "additionalProperties": false,
        "properties": {
          "member_key": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "description": "The only contact detail this API returns."
          },
          "balance_points": {
            "type": "integer",
            "description": "Summed from the ledger, not read from the balance cache."
          },
          "enrolled_at": {
            "type": "string"
          },
          "last_activity_at": {
            "type": "string"
          },
          "points_expire_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "Null when expiry is off, which is the default."
          },
          "stores": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "store_key",
                "customer_ref",
                "linked_at"
              ],
              "additionalProperties": false,
              "properties": {
                "store_key": {
                  "type": "string"
                },
                "customer_ref": {
                  "type": "string",
                  "description": "The merchant's own customer identifier at that store."
                },
                "linked_at": {
                  "type": "string"
                }
              }
            }
          },
          "points_by_store": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "store_key",
                "store_name",
                "earned_points",
                "spent_points",
                "other_points"
              ],
              "additionalProperties": false,
              "properties": {
                "store_key": {
                  "type": "string"
                },
                "store_name": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "earned_points": {
                  "type": "integer",
                  "description": "Points the shopper got at this store: orders, bonuses and credits from staff."
                },
                "spent_points": {
                  "type": "integer",
                  "description": "Points exchanged for a reward at this store. Only redemptions: refunds, expiry and staff removals are in other_points."
                },
                "other_points": {
                  "type": "integer",
                  "description": "Signed. Every other movement at this store: points taken back on a refund, expired, removed by staff, returned when a reward was cancelled, or carried over when another of the shopper's store records was deleted. earned_points - spent_points + other_points is this store's net."
                }
              }
            }
          },
          "outstanding_codes": {
            "type": "integer"
          },
          "history_url": {
            "type": "string"
          }
        }
      },
      "CodePage": {
        "type": "object",
        "required": [
          "data",
          "has_more",
          "next_cursor"
        ],
        "additionalProperties": false,
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "code_ending",
                "ref",
                "store_key",
                "reward_id",
                "cost_points",
                "status",
                "issued_at"
              ],
              "additionalProperties": false,
              "properties": {
                "code_ending": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The last four characters of the code: enough to match what the shopper reads out. Null when the code's final run of letters and digits is shorter than twelve, as a code your own connector recorded in a short or prefixed format can be, because its last four would be all of its random part; name it by `ref`. The whole code is never returned by this API — an unused code is a live discount, and API answers end up in logs and assistant transcripts."
                },
                "ref": {
                  "type": "string",
                  "pattern": "^[0-9a-f]{20}$",
                  "description": "The code's reference: what `cancelMemberCode` takes. It is derived from the redemption, never from the code, so it says nothing about the code."
                },
                "store_key": {
                  "type": "string"
                },
                "reward_id": {
                  "type": "string",
                  "description": "The reward the code was bought with, as `listRewards` gives its id; `@any-amount` for a code the shopper bought by choosing how many points to spend (`spend_any_amount`). A merchant's reward id never starts with `@`."
                },
                "cost_points": {
                  "type": "integer"
                },
                "status": {
                  "type": "string"
                },
                "issued_at": {
                  "type": "string"
                }
              }
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "LedgerPage": {
        "type": "object",
        "required": [
          "data",
          "has_more",
          "next_cursor"
        ],
        "additionalProperties": false,
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "at",
                "store_key",
                "store_name",
                "member_key",
                "delta_points",
                "reason",
                "rule_id",
                "source_type",
                "source_ref",
                "order_ref",
                "code_ending",
                "code_ref"
              ],
              "additionalProperties": false,
              "properties": {
                "id": {
                  "type": "integer"
                },
                "at": {
                  "type": "string"
                },
                "store_key": {
                  "type": "string"
                },
                "store_name": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Null when the row's store is not one of this account's."
                },
                "member_key": {
                  "type": "string"
                },
                "delta_points": {
                  "type": "integer",
                  "description": "Signed: earning is positive, spending and clawbacks negative."
                },
                "reason": {
                  "type": "string"
                },
                "rule_id": {
                  "type": "string"
                },
                "source_type": {
                  "type": "string"
                },
                "source_ref": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The platform's own key for what caused the row: an order, a refund, a points change. Null when that key is a reward code — a cancelled code's points coming back (`redeem_void`) or being taken again because it was spent after all (`void_clawback`); `code_ending` and `code_ref` name the code instead."
                },
                "order_ref": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "code_ending": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The last four characters of the reward code this row moved points for; null on a row that is not about a code, and for a code too short to show any of (see CodePage). The whole code is never returned by this API."
                },
                "code_ref": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "pattern": "^[0-9a-f]{20}$",
                  "description": "That code's `ref`, as `listMemberCodes` gives it and `cancelMemberCode` takes it. Null on a row with no code, or when the platform holds no record of the code to give a reference for."
                }
              }
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "LiabilityReport": {
        "type": "object",
        "required": [
          "as_at",
          "redeemable_points",
          "net_points",
          "members_in_credit",
          "total_members",
          "points_expiring_within_days",
          "expiring_within_days",
          "outstanding_codes",
          "valuation",
          "valuation_unavailable",
          "redeem_rate_by_store"
        ],
        "additionalProperties": false,
        "properties": {
          "as_at": {
            "type": "string"
          },
          "redeemable_points": {
            "type": "integer",
            "description": "Held by members in credit — what could be spent tomorrow."
          },
          "net_points": {
            "type": "integer",
            "description": "The algebraic sum, including negative balances."
          },
          "members_in_credit": {
            "type": "integer"
          },
          "total_members": {
            "type": "integer"
          },
          "points_expiring_within_days": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Points due to expire within expiring_within_days that can still be taken: the shopper holds them, was shown every warning whose window has already passed, and can still be shown the rest because a store they shop at shows the expiry countdown. It is the most that can go, not a forecast: a shopper who misses a later warning keeps their points. Null means we could not tell, which is not the same as none."
          },
          "expiring_within_days": {
            "type": "integer"
          },
          "outstanding_codes": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "store_key",
                "count",
                "cost_points"
              ],
              "additionalProperties": false,
              "properties": {
                "store_key": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                },
                "cost_points": {
                  "type": "integer"
                }
              }
            }
          },
          "valuation": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "currency",
              "minor_units",
              "major_units"
            ],
            "additionalProperties": false,
            "properties": {
              "currency": {
                "type": "string"
              },
              "minor_units": {
                "type": "integer"
              },
              "major_units": {
                "type": "string"
              }
            }
          },
          "valuation_unavailable": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "mixed_rates",
              "no_rate_configured",
              null
            ]
          },
          "redeem_rate_by_store": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "store_key",
                "currency",
                "redeem_points_per_major_unit"
              ],
              "additionalProperties": false,
              "properties": {
                "store_key": {
                  "type": "string"
                },
                "currency": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "redeem_points_per_major_unit": {
                  "type": [
                    "integer",
                    "null"
                  ]
                }
              }
            }
          }
        }
      },
      "SettlementEnvelope": {
        "type": "object",
        "required": [
          "report"
        ],
        "additionalProperties": false,
        "properties": {
          "report": {
            "type": "object",
            "additionalProperties": true,
            "description": "The settlement report in the engine's own field names."
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": [
          "used",
          "cap",
          "percent",
          "level",
          "counts",
          "month",
          "plan_source",
          "plan_name"
        ],
        "additionalProperties": false,
        "properties": {
          "used": {
            "type": "integer"
          },
          "cap": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Null when no cap applies, or when the level says we could not tell."
          },
          "percent": {
            "type": [
              "integer",
              "null"
            ]
          },
          "level": {
            "type": "string",
            "enum": [
              "ok",
              "approaching",
              "over",
              "unknown"
            ]
          },
          "counts": {
            "type": "string",
            "description": "What `used` counts, said in the response and not only in the docs."
          },
          "month": {
            "type": "object",
            "required": [
              "label",
              "from",
              "to"
            ],
            "additionalProperties": false,
            "properties": {
              "label": {
                "type": "string"
              },
              "from": {
                "type": "string"
              },
              "to": {
                "type": "string"
              }
            }
          },
          "plan_source": {
            "type": "string",
            "enum": [
              "recorded_subscription"
            ]
          },
          "plan_name": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "PlanPage": {
        "type": "object",
        "required": [
          "data",
          "has_more",
          "next_cursor",
          "change_plan_at"
        ],
        "additionalProperties": false,
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "store_key",
                "plan_name",
                "status",
                "price",
                "billing_interval",
                "trial_days",
                "is_test",
                "capabilities",
                "activated_at"
              ],
              "additionalProperties": false,
              "properties": {
                "store_key": {
                  "type": "string"
                },
                "plan_name": {
                  "type": "string"
                },
                "status": {
                  "type": "string"
                },
                "price": {
                  "type": "object",
                  "required": [
                    "amount",
                    "currency"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "amount": {
                      "type": "string",
                      "description": "Decimal, as text: a price that has been through binary floating point is not a price."
                    },
                    "currency": {
                      "type": "string"
                    }
                  }
                },
                "billing_interval": {
                  "type": "string"
                },
                "trial_days": {
                  "type": "integer"
                },
                "is_test": {
                  "type": "boolean"
                },
                "capabilities": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "activated_at": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "change_plan_at": {
            "type": "string"
          }
        }
      },
      "AuditPage": {
        "type": "object",
        "required": [
          "data",
          "has_more",
          "next_cursor"
        ],
        "additionalProperties": false,
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "at",
                "actor_type",
                "actor_id",
                "actor_name",
                "client",
                "request_id",
                "action",
                "store_key",
                "object_ref",
                "detail",
                "result",
                "reason"
              ],
              "additionalProperties": false,
              "properties": {
                "id": {
                  "type": "string"
                },
                "at": {
                  "type": "string"
                },
                "actor_type": {
                  "type": "string",
                  "enum": [
                    "api_key",
                    "oauth_token",
                    "staff",
                    "platform"
                  ],
                  "description": "Who acted. `platform` is Matrix Rewards itself acting on this account — our staff from the platform console, or a migration — written into this account's own log so it can be seen."
                },
                "actor_id": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "actor_name": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "client": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "From the caller's `Matrix-Client` header."
                },
                "request_id": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "action": {
                  "type": "string"
                },
                "store_key": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "object_ref": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The member key, store key or reward the action touched. Null when the key does not hold `read_members`, even if the entry names one."
                },
                "detail": {
                  "type": "object",
                  "additionalProperties": true
                },
                "result": {
                  "type": "string",
                  "enum": [
                    "applied",
                    "dry_run",
                    "refused",
                    "disclosed"
                  ]
                },
                "reason": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "KeyPage": {
        "type": "object",
        "required": [
          "data",
          "has_more",
          "next_cursor"
        ],
        "additionalProperties": false,
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name",
                "mode",
                "scopes",
                "agent",
                "limits_lifted",
                "created_at",
                "last_used_at",
                "revoked_at",
                "rotation_grace_until"
              ],
              "additionalProperties": false,
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "mode": {
                  "type": "string",
                  "enum": [
                    "live",
                    "test"
                  ]
                },
                "scopes": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Scope"
                  }
                },
                "agent": {
                  "type": "boolean"
                },
                "limits_lifted": {
                  "type": "boolean",
                  "description": "True when the merchant has lifted this assistant key's fixed limits in the app (see `Me.key.limits_lifted`)."
                },
                "created_at": {
                  "type": "string"
                },
                "last_used_at": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "revoked_at": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "rotation_grace_until": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "SelfRevoked": {
        "type": "object",
        "required": [
          "ok",
          "key_id",
          "revoked_at"
        ],
        "additionalProperties": false,
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "key_id": {
            "type": "string"
          },
          "revoked_at": {
            "type": "string"
          }
        }
      },
      "Confirmation": {
        "type": [
          "object",
          "null"
        ],
        "description": "Send `token` back in `Matrix-Confirmation` with the same request (without dry_run). Null when nothing would change, or when the change would be a replay: a replay needs no confirmation, so send the request without one.",
        "required": [
          "token",
          "expires_at"
        ],
        "additionalProperties": false,
        "properties": {
          "token": {
            "type": "string"
          },
          "expires_at": {
            "type": "string"
          }
        }
      },
      "AdjustmentRequest": {
        "type": "object",
        "required": [
          "delta_points",
          "reason"
        ],
        "additionalProperties": false,
        "properties": {
          "delta_points": {
            "type": "integer",
            "not": {
              "const": 0
            },
            "minimum": -100000,
            "maximum": 100000,
            "description": "Points to add (positive) or take away (negative). Not zero; at most 100,000 either way."
          },
          "reason": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Why. Shown to the shopper on their statement, and kept in the audit log."
          }
        }
      },
      "AdjustmentResult": {
        "type": "object",
        "required": [
          "object",
          "member_key",
          "delta_points",
          "balance_points",
          "store_key",
          "replayed",
          "idempotency_key",
          "audit_id"
        ],
        "additionalProperties": false,
        "properties": {
          "object": {
            "const": "adjustment"
          },
          "member_key": {
            "type": "string"
          },
          "delta_points": {
            "type": "integer"
          },
          "balance_points": {
            "type": "integer",
            "description": "The member's balance afterwards."
          },
          "store_key": {
            "type": "string",
            "description": "The store the change is booked to: the member's earliest-linked store on this account."
          },
          "replayed": {
            "type": "boolean",
            "description": "True when the ledger already held this change (the same Idempotency-Key, member and amount) and nothing new was booked."
          },
          "idempotency_key": {
            "type": "string"
          },
          "audit_id": {
            "type": "string",
            "description": "The audit-log entry this answer rests on."
          }
        }
      },
      "AdjustmentPreview": {
        "type": "object",
        "required": [
          "object",
          "dry_run",
          "outcome",
          "member_key",
          "delta_points",
          "balance_points_before",
          "balance_points_after",
          "store_key",
          "limit",
          "confirmation",
          "audit_id"
        ],
        "additionalProperties": false,
        "properties": {
          "object": {
            "const": "adjustment_preview"
          },
          "dry_run": {
            "const": true
          },
          "outcome": {
            "enum": [
              "would_apply",
              "would_replay"
            ]
          },
          "member_key": {
            "type": "string"
          },
          "delta_points": {
            "type": "integer"
          },
          "balance_points_before": {
            "type": "integer"
          },
          "balance_points_after": {
            "type": "integer",
            "description": "What the real call would report. Equal to before when it would replay."
          },
          "store_key": {
            "type": "string"
          },
          "limit": {
            "type": [
              "object",
              "null"
            ],
            "description": "The account's rolling 24-hour limit in this change's direction, before it. Null when the change would be a replay (a replay spends nothing).",
            "required": [
              "direction",
              "limit_points",
              "used_points",
              "remaining_points"
            ],
            "additionalProperties": false,
            "properties": {
              "direction": {
                "enum": [
                  "credit",
                  "debit"
                ]
              },
              "limit_points": {
                "type": "integer"
              },
              "used_points": {
                "type": "integer"
              },
              "remaining_points": {
                "type": "integer"
              }
            }
          },
          "confirmation": {
            "$ref": "#/components/schemas/Confirmation"
          },
          "audit_id": {
            "type": "string",
            "description": "The preview's own audit-log entry."
          }
        }
      },
      "VoidRequest": {
        "type": "object",
        "required": [
          "ref"
        ],
        "additionalProperties": false,
        "properties": {
          "ref": {
            "type": "string",
            "pattern": "^[0-9a-f]{20}$",
            "description": "The code's `ref`, from `listMemberCodes`. The code itself is never sent: a write request's body passes through logs and assistant transcripts, and a live code is spendable."
          }
        }
      },
      "VoidResult": {
        "type": "object",
        "required": [
          "object",
          "result",
          "member_key",
          "code_ending",
          "store_key",
          "balance_points",
          "audit_id"
        ],
        "additionalProperties": false,
        "properties": {
          "object": {
            "const": "code_void"
          },
          "result": {
            "enum": [
              "voided",
              "already_voided",
              "nothing_to_void"
            ],
            "description": "`voided`: switched off at Shopify and the points given back. `already_voided`: done before, not again. `nothing_to_void`: the code never held points to give back."
          },
          "member_key": {
            "type": "string"
          },
          "code_ending": {
            "type": [
              "string",
              "null"
            ],
            "description": "The last four characters of the code, or null for a code too short to show any of (see CodePage). The code itself is never returned."
          },
          "store_key": {
            "type": [
              "string",
              "null"
            ]
          },
          "balance_points": {
            "type": "integer"
          },
          "audit_id": {
            "type": "string",
            "description": "The audit-log entry written before Shopify was asked."
          }
        }
      },
      "VoidPreview": {
        "type": "object",
        "required": [
          "object",
          "dry_run",
          "outcome",
          "member_key",
          "code_ending",
          "store_key",
          "cost_points",
          "balance_points_before",
          "balance_points_after",
          "confirmation",
          "audit_id"
        ],
        "additionalProperties": false,
        "properties": {
          "object": {
            "const": "code_void_preview"
          },
          "dry_run": {
            "const": true
          },
          "outcome": {
            "enum": [
              "would_void",
              "already_voided",
              "nothing_to_void",
              "would_replay"
            ]
          },
          "member_key": {
            "type": "string"
          },
          "code_ending": {
            "type": [
              "string",
              "null"
            ]
          },
          "store_key": {
            "type": [
              "string",
              "null"
            ]
          },
          "cost_points": {
            "type": [
              "integer",
              "null"
            ],
            "description": "What the code cost. Null when the answer would be a replay of a stored one."
          },
          "balance_points_before": {
            "type": "integer"
          },
          "balance_points_after": {
            "type": "integer"
          },
          "confirmation": {
            "$ref": "#/components/schemas/Confirmation"
          },
          "audit_id": {
            "type": "string"
          }
        }
      },
      "WebhookEventType": {
        "type": "string",
        "description": "What an endpoint can subscribe to. Seven kinds of points movement: `points.earned`: an order, a welcome or birthday bonus, a referral. `points.adjusted`: a change made by the merchant (the app, the web page or this API). `points.redeemed`: points spent on a reward, or taken back when a cancelled code turned out to have been spent. `points.returned`: points given back when a reward was cancelled or could not be issued. `points.clawed_back`: points taken back after a refund. `points.expired`: points that expired. `points.changed`: any other movement — today, points carried over when a shopper's data was erased at another of the account's stores. And `programme.updated`: the store's programme changed — its rates, bonuses, rewards, offers, referral terms, expiry or look — whether in the app or through this API.",
        "enum": [
          "points.earned",
          "points.adjusted",
          "points.redeemed",
          "points.returned",
          "points.clawed_back",
          "points.expired",
          "points.changed",
          "programme.updated"
        ]
      },
      "WebhookEndpoint": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "id",
          "url",
          "event_types",
          "description",
          "status",
          "created_at",
          "disabled_at",
          "disabled_reason",
          "consecutive_failures",
          "failing_since",
          "last_success_at",
          "last_failure_at"
        ],
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "webhook_endpoint"
            ]
          },
          "id": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "event_types": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            }
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "enabled",
              "disabled"
            ],
            "description": "`disabled`: it failed too often (`disabled_reason`), and nothing more is sent to it. Delete it and create it again to start over."
          },
          "created_at": {
            "type": "string"
          },
          "disabled_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "disabled_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "failing",
              null
            ]
          },
          "consecutive_failures": {
            "type": "integer",
            "minimum": 0,
            "description": "Failed deliveries in a row since the last success."
          },
          "failing_since": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the current run of failures began; null after a success."
          },
          "last_success_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_failure_at": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "WebhookEndpointPage": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "data",
          "has_more",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEndpoint"
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "WebhookEndpointCreated": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "id",
          "url",
          "event_types",
          "description",
          "status",
          "created_at",
          "disabled_at",
          "disabled_reason",
          "consecutive_failures",
          "failing_since",
          "last_success_at",
          "last_failure_at",
          "secret"
        ],
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "webhook_endpoint"
            ]
          },
          "id": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "event_types": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            }
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "enabled",
              "disabled"
            ],
            "description": "`disabled`: it failed too often (`disabled_reason`), and nothing more is sent to it. Delete it and create it again to start over."
          },
          "created_at": {
            "type": "string"
          },
          "disabled_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "disabled_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "failing",
              null
            ]
          },
          "consecutive_failures": {
            "type": "integer",
            "minimum": 0,
            "description": "Failed deliveries in a row since the last success."
          },
          "failing_since": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the current run of failures began; null after a success."
          },
          "last_success_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_failure_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "secret": {
            "type": "string",
            "pattern": "^whsec_[A-Za-z0-9+/]+={0,2}$",
            "description": "The signing secret, shown ONCE, here. The HMAC key is the base64 part after `whsec_`, decoded to bytes. It cannot be shown again; a lost secret means deleting the endpoint and creating it again."
          }
        }
      },
      "WebhookEndpointRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "url",
          "event_types"
        ],
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 2048,
            "description": "https on the standard port, no user:password, no #fragment, and a public address: a name that resolves only to public addresses, or a public IP. Refused with `param: url` and a `reason`: `not_a_url`, `too_long`, `not_https`, `port_not_allowed`, `has_credentials`, `has_fragment`, `blocked_name` (localhost, *.local, *.internal, *.arpa, a name with no dot), `own_host`, `blocked_address` (private, loopback, link-local, metadata or other reserved — including when any one of a name's addresses is), `does_not_resolve`. The same checks run again before every delivery, and a redirect is never followed."
          },
          "event_types": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            }
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          }
        }
      },
      "WebhookEndpointDeleted": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "id",
          "deleted",
          "abandoned_messages"
        ],
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "webhook_endpoint_deleted"
            ]
          },
          "id": {
            "type": "string"
          },
          "deleted": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "abandoned_messages": {
            "type": "integer",
            "minimum": 0,
            "description": "Events that were waiting to be sent to it, and now never will be."
          }
        }
      },
      "WebhookTestResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "id",
          "delivered",
          "status",
          "error",
          "duration_ms"
        ],
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "webhook_test"
            ]
          },
          "id": {
            "type": "string",
            "description": "The `webhook-id` the test was sent with."
          },
          "delivered": {
            "type": "boolean",
            "description": "True when the receiver answered 2xx."
          },
          "status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The receiver's HTTP status, when it answered."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "unsafe_url",
              "blocked_address",
              "dns_failed",
              "connection_failed",
              "tls_failed",
              "timeout",
              "redirect",
              "http_status",
              "secret_unavailable",
              null
            ],
            "description": "Why it was not delivered. `redirect`: a 3xx, which is never followed. `timeout`: no answer within 15 seconds."
          },
          "duration_ms": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "timestamp",
          "data"
        ],
        "description": "One points movement: one ledger entry. Deliberately minimal — no email, no Shopify customer id, no reason text, never a reward code.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "points.earned",
              "points.adjusted",
              "points.redeemed",
              "points.returned",
              "points.clawed_back",
              "points.expired",
              "points.changed"
            ],
            "description": "What kind of points movement (`WebhookEventType`)."
          },
          "timestamp": {
            "type": "string",
            "description": "When the platform recorded the movement."
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "ledger_entry_id",
              "store_key",
              "member_key",
              "delta_points",
              "rule_id",
              "source_type",
              "created_at"
            ],
            "properties": {
              "ledger_entry_id": {
                "type": "integer",
                "description": "The `id` of the entry in GET /ledger."
              },
              "store_key": {
                "type": "string"
              },
              "member_key": {
                "type": "string"
              },
              "delta_points": {
                "type": "integer"
              },
              "rule_id": {
                "type": "string"
              },
              "source_type": {
                "type": "string",
                "description": "The ledger's own kind of movement, as GET /ledger gives it."
              },
              "created_at": {
                "type": "string",
                "description": "The movement's own time — for an order, the order's."
              },
              "adjustment_id": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "`points.adjusted` only: the adjustment's id — for a change made through this API, its Idempotency-Key."
              }
            }
          }
        }
      },
      "ProgrammeUpdatedEvent": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "timestamp",
          "data"
        ],
        "description": "One change to a store's programme. It says what changed, never the settings themselves: read GET /stores/{store_key}/programme for them.",
        "properties": {
          "type": {
            "type": "string",
            "const": "programme.updated"
          },
          "timestamp": {
            "type": "string",
            "description": "When the change was made."
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "store_key",
              "version",
              "changed",
              "changed_by",
              "audit_id"
            ],
            "properties": {
              "store_key": {
                "type": "string"
              },
              "version": {
                "type": [
                  "string",
                  "null"
                ],
                "pattern": "^[0-9a-f]{32}$",
                "description": "The programme's settings version after the change: what a programme read answers, and what a change sends as `if_version`. A change to expiry or the look alone leaves it as it was."
              },
              "changed": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The settings that changed, in the engine's names (`rules`, `rewards`, `redeemPointsPerMajorUnit`, `referral`, `spendAnyAmount`, `expiry`, `appearance`, …)."
              },
              "changed_by": {
                "type": "string",
                "enum": [
                  "shopify_admin",
                  "api_key"
                ],
                "description": "Where the change was made: a screen of the app in the Shopify admin, or an API key (a Claude connection included)."
              },
              "audit_id": {
                "type": "string",
                "description": "The change's entry in GET /audit-log."
              }
            }
          }
        }
      },
      "WebhookTestEvent": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "timestamp",
          "data"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "webhook.test"
            ]
          },
          "timestamp": {
            "type": "string"
          },
          "data": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "endpoint_id"
            ],
            "properties": {
              "endpoint_id": {
                "type": "string"
              }
            }
          }
        }
      },
      "ProgrammeChangeRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Any of the fields; each one sent is changed, the rest are left as they are. At least one of the first nine. Offers, rewards and any rule set up by hand are carried exactly as they are.",
        "properties": {
          "points_per_currency_unit": {
            "type": "number",
            "minimum": 0,
            "maximum": 1000,
            "description": "The everyday earn rate: points for every 1 of the store's currency an order pays, with at most 2 decimal places. 0 stops it."
          },
          "welcome_bonus_points": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000,
            "description": "Points a shopper gets for joining. 0 stops the bonus."
          },
          "birthday_bonus_points": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100000,
            "description": "Points a shopper gets on their birthday. 0 stops the bonus."
          },
          "point_value": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "points",
              "amount"
            ],
            "description": "What a point is worth: `points` points are worth `amount` of the store's currency. `points` divided by `amount` must be a whole number.",
            "properties": {
              "points": {
                "type": "integer",
                "minimum": 1,
                "maximum": 100000
              },
              "amount": {
                "type": "string",
                "pattern": "^\\d{1,7}(?:\\.\\d{1,2})?$"
              }
            }
          },
          "referral": {
            "type": "object",
            "additionalProperties": false,
            "description": "What a referral pays at this store, and whether it runs. Only the terms sent change: `{ \"enabled\": false }` stops referrals and keeps the numbers. A friend who has already accepted an invitation is paid on the terms they were shown.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "true runs referrals at this store; false stops them."
              },
              "friend_points": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1000000,
                "description": "What the invited friend is paid, in points."
              },
              "advocate_points": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1000000,
                "description": "What the shopper who invited them is paid, in points."
              },
              "qualifying_min_amount": {
                "type": "string",
                "pattern": "^\\d{1,9}(?:\\.\\d{1,3})?$",
                "description": "The smallest first order that pays the referral, in the store's currency; more than zero."
              },
              "claim_expiry_days": {
                "type": "integer",
                "minimum": 14,
                "maximum": 365,
                "description": "Days the friend has to place that order."
              },
              "advocate_cap_friends": {
                "type": "integer",
                "minimum": 1,
                "maximum": 100,
                "description": "The most friends one shopper is paid for in any rolling window (`referral_advocate_cap_window_days`)."
              }
            }
          },
          "spend_any_amount": {
            "type": "object",
            "additionalProperties": false,
            "description": "Whether a shopper may choose how many points to spend, and get a discount code worth exactly that at the store's point value (`point_value`: there is no second rate), and the limits on one code. Off at every store until it is switched on, here or in the app. Only the terms sent change: `{ \"enabled\": false }` switches it off and keeps the limits for when it is switched on again. A shopper can spend only whole steps, a number of points worth a whole number of the currency's smallest unit, so the fewest points are rounded up to a whole step and the most down, and there must be something between them: otherwise the change is refused, `invalid_request` with `problem` `below_minimum` on `spend_any_amount.max_points`, and so is a `point_value` change that would leave nothing between them. A code comes off the items of an order, never delivery, and anything over their total is not kept. Codes already issued keep what they were issued for, and the store's rewards are unchanged beside it. An assistant's key may switch it off or narrow it, never switch it on.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "true lets shoppers choose how many points to spend; false stops it. Off until it is switched on."
              },
              "min_points": {
                "type": "integer",
                "minimum": 1,
                "maximum": 10000000,
                "description": "The fewest points one code may take, rounded up to a whole step. Until it is set, the points worth one whole unit of the store's currency."
              },
              "max_points": {
                "type": [
                  "integer",
                  "null"
                ],
                "minimum": 1,
                "maximum": 10000000,
                "description": "The most points one code may take, rounded down to a whole step; null (the default) for no limit of the store's own (one spend never takes more than 10,000,000 points)."
              },
              "min_subtotal": {
                "type": "string",
                "pattern": "^\\d{1,9}(?:\\.\\d{1,3})?$",
                "description": "The smallest order a code can be used on, in the store's currency and no finer than its smallest unit (two decimal places for pounds); \"0.00\" (the default) for any order."
              }
            }
          },
          "timezone": {
            "type": "string",
            "description": "The store's time zone, by its IANA name, like \"Europe/London\" or \"America/New_York\": the one its days are reckoned in (a shopper's birthday, a campaign's first and last day). Set from the shop when the app is installed. An unknown name is refused (`invalid_request` on `timezone`); the zone already set changes nothing. Campaigns already set keep their exact start and end times, and their days are then read in the new zone."
          },
          "ways_to_earn": {
            "type": "array",
            "description": "The lines a shopper reads under \"Ways to earn\", in order, at most 50 (`ways_to_earn_max`), replacing the store's list whole; `[]` for none of its own. Each `id` once. The platform still adds a \"Join\" line for a joining bonus and a \"Birthday\" line for a birthday bonus when the list has no entry with the id `join` or `birthday`; a list with an id starting `earn_` is read as one of the platform's own templates, and gets neither. A line with the id `orders` saying what every order earns in the platform's own words is rewritten whenever the rate moves. The preview shows the list before and after as a shopper will read it (`before_after.ways_to_earn`). The same list as now changes nothing.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "id",
                "label",
                "detail"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 64
                },
                "label": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 120,
                  "description": "The line's heading, like \"Shop online\"."
                },
                "detail": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 500,
                  "description": "What it pays, in words a shopper reads. Say only what the programme pays."
                }
              }
            }
          },
          "expiry": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "period_months",
              "effective_from"
            ],
            "description": "Points expire this many months after a shopper's last activity, from this instant. It can never reach back before what shoppers were told (`retroactive_change`).",
            "properties": {
              "period_months": {
                "type": "integer",
                "minimum": 12,
                "maximum": 1200
              },
              "effective_from": {
                "type": "string",
                "format": "date-time",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,6})?Z$",
                "description": "A UTC instant on a date that exists, in the year 9898 or earlier (`expiry_max_effective_year`): a later start would put shoppers' expiry dates past four-digit years."
              }
            }
          },
          "if_version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "The `version` from a programme read. If the programme has changed since, nothing is changed (409 `settings_changed`)."
          }
        }
      },
      "ProgrammeChange": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "store_key",
          "version",
          "changed",
          "as_paid",
          "audit_id"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "programme_change"
          },
          "store_key": {
            "type": "string"
          },
          "version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "The programme's version after the change."
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The settings that changed, in the engine's names. Empty when the change asked for what was already set."
          },
          "as_paid": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "earn_rate_per_major_unit",
              "earn_rates_per_major_unit",
              "welcome_bonus_points",
              "ways_to_earn"
            ],
            "additionalProperties": false,
            "description": "What a shopper is actually paid, worked out from the rules the same way the storefront and the account page work it out. These are the figures to show a shopper. Null when the store has no programme, which cannot price an order.",
            "properties": {
              "earn_rate_per_major_unit": {
                "type": "number",
                "minimum": 0,
                "description": "Points an order pays per whole unit of the store's currency: the sum of the rules that apply to every order. Conditional bonuses (an app-order bonus, a campaign window, a product rule) are left out, because they hold only for some orders. 0 means nothing pays per unit; say nothing about a rate rather than \"earn 0\"."
              },
              "earn_rates_per_major_unit": {
                "type": "array",
                "items": {
                  "type": "number",
                  "minimum": 0
                },
                "description": "The same rules one by one. To price an amount, round each rate times the amount DOWN on its own and add the results: that is how an order is paid, and one rounding over the sum can promise a point more than the order pays."
              },
              "welcome_bonus_points": {
                "type": "integer",
                "minimum": 0,
                "description": "The joining bonus a new shopper is paid, once. 0 when the store pays none, whatever `effective.welcomeBonusPoints` says. At a store whose welcome and birthday bonuses are paid only in its own app (the programme's `effective.bonusesInAppOnly`), it is paid only on the store's own app sign-in, never on a web page: do not offer it there as a web sign-up bonus. The platform template's own welcome sentence in `ways_to_earn` says so."
              },
              "ways_to_earn": {
                "type": "array",
                "description": "`effective.waysToEarn` as a shopper is shown it. The base-rate sentence the app wrote is rebuilt from `earn_rate_per_major_unit`, and left out when that is 0. On a store built from one of the platform's templates, the template's own sentences about the rate, the app-order bonus and the joining bonus are served as written while the rules still pay what they say, corrected when a figure has moved, and left out when nothing pays them. Entries the merchant wrote are unchanged.",
                "items": {
                  "type": "object",
                  "required": [
                    "id",
                    "label",
                    "detail"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "audit_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ProgrammeChangePreview": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "dry_run",
          "outcome",
          "store_key",
          "changed",
          "before_after",
          "currency",
          "examples",
          "point_value",
          "liability",
          "version_after",
          "confirmation",
          "audit_id",
          "referral",
          "spend_any_amount"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "programme_change_preview"
          },
          "dry_run": {
            "type": "boolean",
            "const": true
          },
          "outcome": {
            "type": "string",
            "enum": [
              "would_apply",
              "no_change"
            ]
          },
          "store_key": {
            "type": "string"
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The settings that would change, in the engine's names. What each moves from and to is `before_after`."
          },
          "before_after": {
            "$ref": "#/components/schemas/PreviewBeforeAfter"
          },
          "currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "examples": {
            "type": "array",
            "description": "What a plain order of each size earns now, before and after the change, under every offer running now.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "order_amount",
                "points_before",
                "points_after"
              ],
              "properties": {
                "order_amount": {
                  "type": "string"
                },
                "points_before": {
                  "type": "integer"
                },
                "points_after": {
                  "type": "integer"
                }
              }
            }
          },
          "point_value": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "points_per_currency_unit_before",
              "points_per_currency_unit_after"
            ],
            "properties": {
              "points_per_currency_unit_before": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "points_per_currency_unit_after": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          "liability": {
            "description": "When the point value changes: what the points shoppers hold are worth before and after. Null when it does not change; `unavailable` when the account has more than one store.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "redeemable_points",
                  "currency",
                  "money_before",
                  "money_after"
                ],
                "properties": {
                  "redeemable_points": {
                    "type": "integer"
                  },
                  "currency": {
                    "type": "string"
                  },
                  "money_before": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "money_after": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "unavailable"
                ],
                "properties": {
                  "unavailable": {
                    "type": "string",
                    "enum": [
                      "more_than_one_store"
                    ]
                  }
                }
              }
            ]
          },
          "version_after": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{32}$",
            "description": "The version the programme would have."
          },
          "confirmation": {
            "description": "For a key tagged for an assistant: send `token` back as the `Matrix-Confirmation` header with the same request. Null when nothing would change.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "token",
                  "expires_at"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "expires_at": {
                    "type": "string"
                  }
                }
              }
            ]
          },
          "audit_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "referral": {
            "description": "When the change sends `referral`: the store's referral terms before and after. Null otherwise.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "oneOf": [
                      {
                        "type": "null"
                      },
                      {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "enabled",
                          "friend_points",
                          "advocate_points",
                          "qualifying_min_amount",
                          "claim_expiry_days",
                          "advocate_cap_friends"
                        ],
                        "properties": {
                          "enabled": {
                            "type": "boolean"
                          },
                          "friend_points": {
                            "type": "integer"
                          },
                          "advocate_points": {
                            "type": "integer"
                          },
                          "qualifying_min_amount": {
                            "type": "string"
                          },
                          "claim_expiry_days": {
                            "type": "integer"
                          },
                          "advocate_cap_friends": {
                            "type": "integer"
                          }
                        }
                      }
                    ]
                  },
                  "after": {
                    "oneOf": [
                      {
                        "type": "null"
                      },
                      {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "enabled",
                          "friend_points",
                          "advocate_points",
                          "qualifying_min_amount",
                          "claim_expiry_days",
                          "advocate_cap_friends"
                        ],
                        "properties": {
                          "enabled": {
                            "type": "boolean"
                          },
                          "friend_points": {
                            "type": "integer"
                          },
                          "advocate_points": {
                            "type": "integer"
                          },
                          "qualifying_min_amount": {
                            "type": "string"
                          },
                          "claim_expiry_days": {
                            "type": "integer"
                          },
                          "advocate_cap_friends": {
                            "type": "integer"
                          }
                        }
                      }
                    ]
                  }
                }
              }
            ]
          },
          "spend_any_amount": {
            "description": "When the change sends `spend_any_amount`: its terms before and after, as stored, so the limits kept while it is off are shown. Null otherwise.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "oneOf": [
                      {
                        "type": "null"
                      },
                      {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "enabled",
                          "min_points",
                          "max_points",
                          "min_subtotal"
                        ],
                        "properties": {
                          "enabled": {
                            "type": "boolean"
                          },
                          "min_points": {
                            "type": "integer"
                          },
                          "max_points": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "min_subtotal": {
                            "type": "string"
                          }
                        }
                      }
                    ]
                  },
                  "after": {
                    "oneOf": [
                      {
                        "type": "null"
                      },
                      {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "enabled",
                          "min_points",
                          "max_points",
                          "min_subtotal"
                        ],
                        "properties": {
                          "enabled": {
                            "type": "boolean"
                          },
                          "min_points": {
                            "type": "integer"
                          },
                          "max_points": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "min_subtotal": {
                            "type": "string"
                          }
                        }
                      }
                    ]
                  }
                }
              }
            ]
          }
        }
      },
      "RewardRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "cost_points"
        ],
        "description": "A new reward. The platform names it `reward_` and the next whole number, whatever its label says; left out, the label is the amount in the store's currency (\"£5.00 off\"), the percentage (\"10% off\") or \"Free shipping\". An amount reward needs value_amount, a percentage reward percent_off.",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "amount",
              "percentage",
              "free_shipping"
            ],
            "description": "What the reward gives: an amount off the order (the default), a percentage off its goods, or free shipping."
          },
          "cost_points": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10000000,
            "description": "What the reward costs, in points."
          },
          "value_amount": {
            "type": "string",
            "pattern": "^\\d{1,7}(?:\\.\\d{1,2})?$",
            "description": "For an amount reward: how much it takes off an order, in the store's currency, more than zero. Not sent for the other kinds."
          },
          "percent_off": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "For a percentage reward: the percentage off, a whole number."
          },
          "min_subtotal": {
            "type": "string",
            "pattern": "^\\d{1,7}(?:\\.\\d{1,2})?$",
            "description": "The smallest order it can be used on; \"0.00\" (the default) for any order."
          },
          "label": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "What the shopper sees. Left out on an add, it is the value in the store's currency, like \"£5.00 off\"."
          },
          "if_version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "The `version` from a programme read. If the programme has changed since, nothing is changed (409 `settings_changed`)."
          }
        }
      },
      "RewardChangeRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Any of the fields; the rest are left as they are. Changing kind needs that kind's own field, and without a label sent the label becomes the new kind's (\"10% off\"). Codes already issued for the reward keep what they were issued with.",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "amount",
              "percentage",
              "free_shipping"
            ],
            "description": "What the reward gives: an amount off the order (the default), a percentage off its goods, or free shipping."
          },
          "cost_points": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10000000,
            "description": "What the reward costs, in points."
          },
          "value_amount": {
            "type": "string",
            "pattern": "^\\d{1,7}(?:\\.\\d{1,2})?$",
            "description": "For an amount reward: how much it takes off an order, in the store's currency, more than zero. Not sent for the other kinds."
          },
          "percent_off": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "For a percentage reward: the percentage off, a whole number."
          },
          "min_subtotal": {
            "type": "string",
            "pattern": "^\\d{1,7}(?:\\.\\d{1,2})?$",
            "description": "The smallest order it can be used on; \"0.00\" (the default) for any order."
          },
          "label": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "What the shopper sees. Left out on an add, it is the value in the store's currency, like \"£5.00 off\"."
          },
          "if_version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "The `version` from a programme read. If the programme has changed since, nothing is changed (409 `settings_changed`)."
          }
        }
      },
      "RewardRemoveRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Codes already issued for the reward stay valid.",
        "properties": {
          "if_version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "The `version` from a programme read. If the programme has changed since, nothing is changed (409 `settings_changed`)."
          }
        }
      },
      "ApiReward": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "kind",
          "cost_points",
          "value_amount",
          "percent_off",
          "min_subtotal",
          "label"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "amount",
              "percentage",
              "free_shipping"
            ],
            "description": "What the reward gives: an amount off the order (the default), a percentage off its goods, or free shipping."
          },
          "cost_points": {
            "type": "integer"
          },
          "value_amount": {
            "type": "string",
            "description": "For an amount reward, the amount off; \"0.00\" for the other kinds."
          },
          "percent_off": {
            "type": [
              "integer",
              "null"
            ],
            "description": "For a percentage reward, the percentage off; otherwise null."
          },
          "min_subtotal": {
            "type": "string"
          },
          "label": {
            "type": "string"
          }
        }
      },
      "RewardResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "reward",
          "warnings",
          "store_key",
          "version",
          "changed",
          "as_paid",
          "audit_id"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "reward"
          },
          "reward": {
            "$ref": "#/components/schemas/ApiReward"
          },
          "warnings": {
            "type": "array",
            "description": "What may be a mistake, never a refusal: `reward_worth_more_than_points` (worth more than 10 times what its points are worth at the store's point value) and `reward_worth_more_than_minimum_order` (worth more than the smallest order it can be used on; a reward with no minimum can be used on any order, so it always is). Only an amount reward is weighed: a percentage or free shipping is worth what the order makes it.",
            "items": {
              "type": "string",
              "enum": [
                "reward_worth_more_than_points",
                "reward_worth_more_than_minimum_order"
              ]
            }
          },
          "store_key": {
            "type": "string"
          },
          "version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "The programme's version after the change."
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The settings that changed, in the engine's names. Empty when the change asked for what was already set."
          },
          "as_paid": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "earn_rate_per_major_unit",
              "earn_rates_per_major_unit",
              "welcome_bonus_points",
              "ways_to_earn"
            ],
            "additionalProperties": false,
            "description": "What a shopper is actually paid, worked out from the rules the same way the storefront and the account page work it out. These are the figures to show a shopper. Null when the store has no programme, which cannot price an order.",
            "properties": {
              "earn_rate_per_major_unit": {
                "type": "number",
                "minimum": 0,
                "description": "Points an order pays per whole unit of the store's currency: the sum of the rules that apply to every order. Conditional bonuses (an app-order bonus, a campaign window, a product rule) are left out, because they hold only for some orders. 0 means nothing pays per unit; say nothing about a rate rather than \"earn 0\"."
              },
              "earn_rates_per_major_unit": {
                "type": "array",
                "items": {
                  "type": "number",
                  "minimum": 0
                },
                "description": "The same rules one by one. To price an amount, round each rate times the amount DOWN on its own and add the results: that is how an order is paid, and one rounding over the sum can promise a point more than the order pays."
              },
              "welcome_bonus_points": {
                "type": "integer",
                "minimum": 0,
                "description": "The joining bonus a new shopper is paid, once. 0 when the store pays none, whatever `effective.welcomeBonusPoints` says. At a store whose welcome and birthday bonuses are paid only in its own app (the programme's `effective.bonusesInAppOnly`), it is paid only on the store's own app sign-in, never on a web page: do not offer it there as a web sign-up bonus. The platform template's own welcome sentence in `ways_to_earn` says so."
              },
              "ways_to_earn": {
                "type": "array",
                "description": "`effective.waysToEarn` as a shopper is shown it. The base-rate sentence the app wrote is rebuilt from `earn_rate_per_major_unit`, and left out when that is 0. On a store built from one of the platform's templates, the template's own sentences about the rate, the app-order bonus and the joining bonus are served as written while the rules still pay what they say, corrected when a figure has moved, and left out when nothing pays them. Entries the merchant wrote are unchanged.",
                "items": {
                  "type": "object",
                  "required": [
                    "id",
                    "label",
                    "detail"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "audit_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "RewardPreview": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "reward",
          "warnings",
          "dry_run",
          "outcome",
          "store_key",
          "changed",
          "before_after",
          "currency",
          "examples",
          "point_value",
          "liability",
          "version_after",
          "confirmation",
          "audit_id"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "reward_preview"
          },
          "reward": {
            "$ref": "#/components/schemas/ApiReward"
          },
          "warnings": {
            "type": "array",
            "description": "What may be a mistake, never a refusal: `reward_worth_more_than_points` (worth more than 10 times what its points are worth at the store's point value) and `reward_worth_more_than_minimum_order` (worth more than the smallest order it can be used on; a reward with no minimum can be used on any order, so it always is). Only an amount reward is weighed: a percentage or free shipping is worth what the order makes it.",
            "items": {
              "type": "string",
              "enum": [
                "reward_worth_more_than_points",
                "reward_worth_more_than_minimum_order"
              ]
            }
          },
          "dry_run": {
            "type": "boolean",
            "const": true
          },
          "outcome": {
            "type": "string",
            "enum": [
              "would_apply",
              "no_change"
            ]
          },
          "store_key": {
            "type": "string"
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The settings that would change, in the engine's names. What each moves from and to is `before_after`."
          },
          "before_after": {
            "$ref": "#/components/schemas/PreviewBeforeAfter"
          },
          "currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "examples": {
            "type": "array",
            "description": "What a plain order of each size earns now, before and after the change, under every offer running now.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "order_amount",
                "points_before",
                "points_after"
              ],
              "properties": {
                "order_amount": {
                  "type": "string"
                },
                "points_before": {
                  "type": "integer"
                },
                "points_after": {
                  "type": "integer"
                }
              }
            }
          },
          "point_value": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "points_per_currency_unit_before",
              "points_per_currency_unit_after"
            ],
            "properties": {
              "points_per_currency_unit_before": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "points_per_currency_unit_after": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          "liability": {
            "description": "When the point value changes: what the points shoppers hold are worth before and after. Null when it does not change; `unavailable` when the account has more than one store.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "redeemable_points",
                  "currency",
                  "money_before",
                  "money_after"
                ],
                "properties": {
                  "redeemable_points": {
                    "type": "integer"
                  },
                  "currency": {
                    "type": "string"
                  },
                  "money_before": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "money_after": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "unavailable"
                ],
                "properties": {
                  "unavailable": {
                    "type": "string",
                    "enum": [
                      "more_than_one_store"
                    ]
                  }
                }
              }
            ]
          },
          "version_after": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{32}$",
            "description": "The version the programme would have."
          },
          "confirmation": {
            "description": "For a key tagged for an assistant: send `token` back as the `Matrix-Confirmation` header with the same request. Null when nothing would change.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "token",
                  "expires_at"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "expires_at": {
                    "type": "string"
                  }
                }
              }
            ]
          },
          "audit_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "RewardRemoved": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "reward_id",
          "store_key",
          "version",
          "changed",
          "as_paid",
          "audit_id"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "reward_removed"
          },
          "reward_id": {
            "type": "string"
          },
          "store_key": {
            "type": "string"
          },
          "version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "The programme's version after the change."
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The settings that changed, in the engine's names. Empty when the change asked for what was already set."
          },
          "as_paid": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "earn_rate_per_major_unit",
              "earn_rates_per_major_unit",
              "welcome_bonus_points",
              "ways_to_earn"
            ],
            "additionalProperties": false,
            "description": "What a shopper is actually paid, worked out from the rules the same way the storefront and the account page work it out. These are the figures to show a shopper. Null when the store has no programme, which cannot price an order.",
            "properties": {
              "earn_rate_per_major_unit": {
                "type": "number",
                "minimum": 0,
                "description": "Points an order pays per whole unit of the store's currency: the sum of the rules that apply to every order. Conditional bonuses (an app-order bonus, a campaign window, a product rule) are left out, because they hold only for some orders. 0 means nothing pays per unit; say nothing about a rate rather than \"earn 0\"."
              },
              "earn_rates_per_major_unit": {
                "type": "array",
                "items": {
                  "type": "number",
                  "minimum": 0
                },
                "description": "The same rules one by one. To price an amount, round each rate times the amount DOWN on its own and add the results: that is how an order is paid, and one rounding over the sum can promise a point more than the order pays."
              },
              "welcome_bonus_points": {
                "type": "integer",
                "minimum": 0,
                "description": "The joining bonus a new shopper is paid, once. 0 when the store pays none, whatever `effective.welcomeBonusPoints` says. At a store whose welcome and birthday bonuses are paid only in its own app (the programme's `effective.bonusesInAppOnly`), it is paid only on the store's own app sign-in, never on a web page: do not offer it there as a web sign-up bonus. The platform template's own welcome sentence in `ways_to_earn` says so."
              },
              "ways_to_earn": {
                "type": "array",
                "description": "`effective.waysToEarn` as a shopper is shown it. The base-rate sentence the app wrote is rebuilt from `earn_rate_per_major_unit`, and left out when that is 0. On a store built from one of the platform's templates, the template's own sentences about the rate, the app-order bonus and the joining bonus are served as written while the rules still pay what they say, corrected when a figure has moved, and left out when nothing pays them. Entries the merchant wrote are unchanged.",
                "items": {
                  "type": "object",
                  "required": [
                    "id",
                    "label",
                    "detail"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "audit_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "RewardRemovedPreview": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "reward_id",
          "dry_run",
          "outcome",
          "store_key",
          "changed",
          "before_after",
          "currency",
          "examples",
          "point_value",
          "liability",
          "version_after",
          "confirmation",
          "audit_id"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "reward_removed_preview"
          },
          "reward_id": {
            "type": "string"
          },
          "dry_run": {
            "type": "boolean",
            "const": true
          },
          "outcome": {
            "type": "string",
            "enum": [
              "would_apply",
              "no_change"
            ]
          },
          "store_key": {
            "type": "string"
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The settings that would change, in the engine's names. What each moves from and to is `before_after`."
          },
          "before_after": {
            "$ref": "#/components/schemas/PreviewBeforeAfter"
          },
          "currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "examples": {
            "type": "array",
            "description": "What a plain order of each size earns now, before and after the change, under every offer running now.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "order_amount",
                "points_before",
                "points_after"
              ],
              "properties": {
                "order_amount": {
                  "type": "string"
                },
                "points_before": {
                  "type": "integer"
                },
                "points_after": {
                  "type": "integer"
                }
              }
            }
          },
          "point_value": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "points_per_currency_unit_before",
              "points_per_currency_unit_after"
            ],
            "properties": {
              "points_per_currency_unit_before": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "points_per_currency_unit_after": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          "liability": {
            "description": "When the point value changes: what the points shoppers hold are worth before and after. Null when it does not change; `unavailable` when the account has more than one store.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "redeemable_points",
                  "currency",
                  "money_before",
                  "money_after"
                ],
                "properties": {
                  "redeemable_points": {
                    "type": "integer"
                  },
                  "currency": {
                    "type": "string"
                  },
                  "money_before": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "money_after": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "unavailable"
                ],
                "properties": {
                  "unavailable": {
                    "type": "string",
                    "enum": [
                      "more_than_one_store"
                    ]
                  }
                }
              }
            ]
          },
          "version_after": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{32}$",
            "description": "The version the programme would have."
          },
          "confirmation": {
            "description": "For a key tagged for an assistant: send `token` back as the `Matrix-Confirmation` header with the same request. Null when nothing would change.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "token",
                  "expires_at"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "expires_at": {
                    "type": "string"
                  }
                }
              }
            ]
          },
          "audit_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "RewardDocumentPage": {
        "type": "object",
        "description": "The store's rewards, in the engine's own field names. Each has the `id` the reward calls take. An amount reward has no `kind`; a percentage reward has `kind` \"percentage\" and its `percentOff`, and a free-shipping reward `kind` \"free_shipping\". Choosing how many points to spend is not one of these: it is the programme's `spend_any_amount`, read as `effective.spendAnyAmount`.",
        "required": [
          "data",
          "has_more",
          "next_cursor",
          "version"
        ],
        "additionalProperties": false,
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true,
              "required": [
                "id"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "description": "The reward's id: what changeReward and removeReward take as reward_id."
                }
              }
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "Which stored programme this is: it changes whenever the store's programme settings change (not its look or expiry), so two reads with the same version saw the same programme. Send it as `if_version` with a programme change (`POST /stores/{store_key}/programme`) and the change is refused, 409 `settings_changed`, if the programme has changed since you read it. The embedded admin's save checks the same version."
          }
        }
      },
      "ApiOffer": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "kind",
          "reason",
          "read_only"
        ],
        "description": "An offer in the store's own days. `campaign`: extra points per unit between two days. `product`: points per item of chosen products. `channel`: extra points per unit on orders from chosen sources. `other`: set up another way; read-only here.",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "campaign",
              "product",
              "channel",
              "other"
            ]
          },
          "reason": {
            "type": "string",
            "description": "What the shopper is told the points are for."
          },
          "read_only": {
            "type": "boolean",
            "description": "True for `other`, and for a campaign that has ended: remove it, but not change it."
          },
          "ended": {
            "type": "boolean"
          },
          "points_per_currency_unit": {
            "type": "number"
          },
          "starts_at": {
            "type": "string"
          },
          "ends_at": {
            "type": "string"
          },
          "starts_on": {
            "type": "string",
            "description": "The first day, in the store's time zone."
          },
          "ends_on": {
            "type": "string",
            "description": "The last day, in the store's time zone."
          },
          "points_per_item": {
            "type": "integer"
          },
          "product_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "channel": {
            "type": "object",
            "additionalProperties": false,
            "description": "Orders from any of these count: a sales channel's source name, an app's id, or an order tag; at most 50 of each, and at least one list not empty.",
            "properties": {
              "source_names": {
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 100
                }
              },
              "app_ids": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "order_tags": {
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 100
                }
              }
            }
          }
        }
      },
      "OfferPage": {
        "type": "object",
        "description": "The store's offers in the offer calls' own words, with the programme's version.",
        "required": [
          "data",
          "has_more",
          "next_cursor",
          "version"
        ],
        "additionalProperties": false,
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiOffer"
            }
          },
          "has_more": {
            "type": "boolean"
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "Which stored programme this is: it changes whenever the store's programme settings change (not its look or expiry), so two reads with the same version saw the same programme. Send it as `if_version` with a programme change (`POST /stores/{store_key}/programme`) and the change is refused, 409 `settings_changed`, if the programme has changed since you read it. The embedded admin's save checks the same version."
          }
        }
      },
      "OfferRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "An offer. `kind` says which fields it takes: a campaign takes `points_per_currency_unit`, `starts_on` and `ends_on`; a product offer `points_per_item` and `product_ids`; a channel offer `points_per_currency_unit` and `channel`. A campaign starting today starts when it is made, so orders already placed today are not included. On a change, send only what changes; `kind` cannot change.",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "campaign",
              "product",
              "channel"
            ]
          },
          "reason": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "points_per_currency_unit": {
            "type": "number",
            "maximum": 1000,
            "description": "Extra points for every 1 of the store's currency: more than 0, at most 2 decimal places."
          },
          "starts_on": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "The first day, in the store's time zone: today or later."
          },
          "ends_on": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "The last day, in the store's time zone: not before `starts_on`, and not already past."
          },
          "points_per_item": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000000
          },
          "product_ids": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": [
                "string",
                "integer"
              ]
            },
            "description": "Shopify product ids, at most 500: numbers, or gid://shopify/Product/…"
          },
          "channel": {
            "type": "object",
            "additionalProperties": false,
            "description": "Orders from any of these count: a sales channel's source name, an app's id, or an order tag; at most 50 of each, and at least one list not empty.",
            "properties": {
              "source_names": {
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 100
                }
              },
              "app_ids": {
                "type": "array",
                "items": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "order_tags": {
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 100
                }
              }
            }
          },
          "if_version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "The `version` from a programme read. If the programme has changed since, nothing is changed (409 `settings_changed`)."
          }
        }
      },
      "OfferRemoveRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "if_version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "The `version` from a programme read. If the programme has changed since, nothing is changed (409 `settings_changed`)."
          }
        }
      },
      "OfferResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "offer",
          "store_key",
          "version",
          "changed",
          "as_paid",
          "audit_id"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "offer"
          },
          "offer": {
            "$ref": "#/components/schemas/ApiOffer"
          },
          "store_key": {
            "type": "string"
          },
          "version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "The programme's version after the change."
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The settings that changed, in the engine's names. Empty when the change asked for what was already set."
          },
          "as_paid": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "earn_rate_per_major_unit",
              "earn_rates_per_major_unit",
              "welcome_bonus_points",
              "ways_to_earn"
            ],
            "additionalProperties": false,
            "description": "What a shopper is actually paid, worked out from the rules the same way the storefront and the account page work it out. These are the figures to show a shopper. Null when the store has no programme, which cannot price an order.",
            "properties": {
              "earn_rate_per_major_unit": {
                "type": "number",
                "minimum": 0,
                "description": "Points an order pays per whole unit of the store's currency: the sum of the rules that apply to every order. Conditional bonuses (an app-order bonus, a campaign window, a product rule) are left out, because they hold only for some orders. 0 means nothing pays per unit; say nothing about a rate rather than \"earn 0\"."
              },
              "earn_rates_per_major_unit": {
                "type": "array",
                "items": {
                  "type": "number",
                  "minimum": 0
                },
                "description": "The same rules one by one. To price an amount, round each rate times the amount DOWN on its own and add the results: that is how an order is paid, and one rounding over the sum can promise a point more than the order pays."
              },
              "welcome_bonus_points": {
                "type": "integer",
                "minimum": 0,
                "description": "The joining bonus a new shopper is paid, once. 0 when the store pays none, whatever `effective.welcomeBonusPoints` says. At a store whose welcome and birthday bonuses are paid only in its own app (the programme's `effective.bonusesInAppOnly`), it is paid only on the store's own app sign-in, never on a web page: do not offer it there as a web sign-up bonus. The platform template's own welcome sentence in `ways_to_earn` says so."
              },
              "ways_to_earn": {
                "type": "array",
                "description": "`effective.waysToEarn` as a shopper is shown it. The base-rate sentence the app wrote is rebuilt from `earn_rate_per_major_unit`, and left out when that is 0. On a store built from one of the platform's templates, the template's own sentences about the rate, the app-order bonus and the joining bonus are served as written while the rules still pay what they say, corrected when a figure has moved, and left out when nothing pays them. Entries the merchant wrote are unchanged.",
                "items": {
                  "type": "object",
                  "required": [
                    "id",
                    "label",
                    "detail"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "audit_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "OfferPreview": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "offer",
          "dry_run",
          "outcome",
          "store_key",
          "changed",
          "before_after",
          "currency",
          "examples",
          "point_value",
          "liability",
          "version_after",
          "confirmation",
          "audit_id"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "offer_preview"
          },
          "offer": {
            "$ref": "#/components/schemas/ApiOffer"
          },
          "dry_run": {
            "type": "boolean",
            "const": true
          },
          "outcome": {
            "type": "string",
            "enum": [
              "would_apply",
              "no_change"
            ]
          },
          "store_key": {
            "type": "string"
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The settings that would change, in the engine's names. What each moves from and to is `before_after`."
          },
          "before_after": {
            "$ref": "#/components/schemas/PreviewBeforeAfter"
          },
          "currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "examples": {
            "type": "array",
            "description": "What a plain order of each size earns now, before and after the change, under every offer running now.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "order_amount",
                "points_before",
                "points_after"
              ],
              "properties": {
                "order_amount": {
                  "type": "string"
                },
                "points_before": {
                  "type": "integer"
                },
                "points_after": {
                  "type": "integer"
                }
              }
            }
          },
          "point_value": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "points_per_currency_unit_before",
              "points_per_currency_unit_after"
            ],
            "properties": {
              "points_per_currency_unit_before": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "points_per_currency_unit_after": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          "liability": {
            "description": "When the point value changes: what the points shoppers hold are worth before and after. Null when it does not change; `unavailable` when the account has more than one store.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "redeemable_points",
                  "currency",
                  "money_before",
                  "money_after"
                ],
                "properties": {
                  "redeemable_points": {
                    "type": "integer"
                  },
                  "currency": {
                    "type": "string"
                  },
                  "money_before": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "money_after": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "unavailable"
                ],
                "properties": {
                  "unavailable": {
                    "type": "string",
                    "enum": [
                      "more_than_one_store"
                    ]
                  }
                }
              }
            ]
          },
          "version_after": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{32}$",
            "description": "The version the programme would have."
          },
          "confirmation": {
            "description": "For a key tagged for an assistant: send `token` back as the `Matrix-Confirmation` header with the same request. Null when nothing would change.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "token",
                  "expires_at"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "expires_at": {
                    "type": "string"
                  }
                }
              }
            ]
          },
          "audit_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "OfferRemoved": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "offer_id",
          "store_key",
          "version",
          "changed",
          "as_paid",
          "audit_id"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "offer_removed"
          },
          "offer_id": {
            "type": "string"
          },
          "store_key": {
            "type": "string"
          },
          "version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "The programme's version after the change."
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The settings that changed, in the engine's names. Empty when the change asked for what was already set."
          },
          "as_paid": {
            "type": [
              "object",
              "null"
            ],
            "required": [
              "earn_rate_per_major_unit",
              "earn_rates_per_major_unit",
              "welcome_bonus_points",
              "ways_to_earn"
            ],
            "additionalProperties": false,
            "description": "What a shopper is actually paid, worked out from the rules the same way the storefront and the account page work it out. These are the figures to show a shopper. Null when the store has no programme, which cannot price an order.",
            "properties": {
              "earn_rate_per_major_unit": {
                "type": "number",
                "minimum": 0,
                "description": "Points an order pays per whole unit of the store's currency: the sum of the rules that apply to every order. Conditional bonuses (an app-order bonus, a campaign window, a product rule) are left out, because they hold only for some orders. 0 means nothing pays per unit; say nothing about a rate rather than \"earn 0\"."
              },
              "earn_rates_per_major_unit": {
                "type": "array",
                "items": {
                  "type": "number",
                  "minimum": 0
                },
                "description": "The same rules one by one. To price an amount, round each rate times the amount DOWN on its own and add the results: that is how an order is paid, and one rounding over the sum can promise a point more than the order pays."
              },
              "welcome_bonus_points": {
                "type": "integer",
                "minimum": 0,
                "description": "The joining bonus a new shopper is paid, once. 0 when the store pays none, whatever `effective.welcomeBonusPoints` says. At a store whose welcome and birthday bonuses are paid only in its own app (the programme's `effective.bonusesInAppOnly`), it is paid only on the store's own app sign-in, never on a web page: do not offer it there as a web sign-up bonus. The platform template's own welcome sentence in `ways_to_earn` says so."
              },
              "ways_to_earn": {
                "type": "array",
                "description": "`effective.waysToEarn` as a shopper is shown it. The base-rate sentence the app wrote is rebuilt from `earn_rate_per_major_unit`, and left out when that is 0. On a store built from one of the platform's templates, the template's own sentences about the rate, the app-order bonus and the joining bonus are served as written while the rules still pay what they say, corrected when a figure has moved, and left out when nothing pays them. Entries the merchant wrote are unchanged.",
                "items": {
                  "type": "object",
                  "required": [
                    "id",
                    "label",
                    "detail"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "audit_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "OfferRemovedPreview": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "offer_id",
          "dry_run",
          "outcome",
          "store_key",
          "changed",
          "before_after",
          "currency",
          "examples",
          "point_value",
          "liability",
          "version_after",
          "confirmation",
          "audit_id"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "offer_removed_preview"
          },
          "offer_id": {
            "type": "string"
          },
          "dry_run": {
            "type": "boolean",
            "const": true
          },
          "outcome": {
            "type": "string",
            "enum": [
              "would_apply",
              "no_change"
            ]
          },
          "store_key": {
            "type": "string"
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The settings that would change, in the engine's names. What each moves from and to is `before_after`."
          },
          "before_after": {
            "$ref": "#/components/schemas/PreviewBeforeAfter"
          },
          "currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "examples": {
            "type": "array",
            "description": "What a plain order of each size earns now, before and after the change, under every offer running now.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "order_amount",
                "points_before",
                "points_after"
              ],
              "properties": {
                "order_amount": {
                  "type": "string"
                },
                "points_before": {
                  "type": "integer"
                },
                "points_after": {
                  "type": "integer"
                }
              }
            }
          },
          "point_value": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "points_per_currency_unit_before",
              "points_per_currency_unit_after"
            ],
            "properties": {
              "points_per_currency_unit_before": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "points_per_currency_unit_after": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          "liability": {
            "description": "When the point value changes: what the points shoppers hold are worth before and after. Null when it does not change; `unavailable` when the account has more than one store.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "redeemable_points",
                  "currency",
                  "money_before",
                  "money_after"
                ],
                "properties": {
                  "redeemable_points": {
                    "type": "integer"
                  },
                  "currency": {
                    "type": "string"
                  },
                  "money_before": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "money_after": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "unavailable"
                ],
                "properties": {
                  "unavailable": {
                    "type": "string",
                    "enum": [
                      "more_than_one_store"
                    ]
                  }
                }
              }
            ]
          },
          "version_after": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{32}$",
            "description": "The version the programme would have."
          },
          "confirmation": {
            "description": "For a key tagged for an assistant: send `token` back as the `Matrix-Confirmation` header with the same request. Null when nothing would change.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "token",
                  "expires_at"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "expires_at": {
                    "type": "string"
                  }
                }
              }
            ]
          },
          "audit_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "PreviewBeforeAfter": {
        "type": "object",
        "additionalProperties": false,
        "description": "Every figure the change moves, before and after, in this API's own write field names, worked out from the programme as it would price orders before and after (the same way the storefront works them out). Only the ones that differ are here, so `{}` means none moves. `points_per_currency_unit` is the everyday rate an order pays (a running campaign or other offer left out); `welcome_bonus_points` and `birthday_bonus_points` are 0 when none is paid; `point_value` is how many points are worth one unit of the currency; `ways_to_earn` is the list as a shopper reads it. On a reward's call, `reward` is the reward itself: null before an add, null after a removal.",
        "properties": {
          "points_per_currency_unit": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "before",
              "after"
            ],
            "properties": {
              "before": {
                "type": "number",
                "minimum": 0
              },
              "after": {
                "type": "number",
                "minimum": 0
              }
            }
          },
          "welcome_bonus_points": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "before",
              "after"
            ],
            "properties": {
              "before": {
                "type": "integer",
                "minimum": 0
              },
              "after": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "birthday_bonus_points": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "before",
              "after"
            ],
            "properties": {
              "before": {
                "type": "integer",
                "minimum": 0
              },
              "after": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "point_value": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "before",
              "after"
            ],
            "properties": {
              "before": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "points",
                  "amount"
                ],
                "properties": {
                  "points": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "amount": {
                    "type": "string",
                    "const": "1.00"
                  }
                }
              },
              "after": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "points",
                  "amount"
                ],
                "properties": {
                  "points": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "amount": {
                    "type": "string",
                    "const": "1.00"
                  }
                }
              }
            }
          },
          "timezone": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "before",
              "after"
            ],
            "properties": {
              "before": {
                "type": "string"
              },
              "after": {
                "type": "string"
              }
            }
          },
          "ways_to_earn": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "before",
              "after"
            ],
            "properties": {
              "before": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "id",
                    "label",
                    "detail"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    }
                  }
                }
              },
              "after": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "id",
                    "label",
                    "detail"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "detail": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "referral": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "before",
              "after"
            ],
            "properties": {
              "before": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "enabled",
                      "friend_points",
                      "advocate_points",
                      "qualifying_min_amount",
                      "claim_expiry_days",
                      "advocate_cap_friends"
                    ],
                    "properties": {
                      "enabled": {
                        "type": "boolean"
                      },
                      "friend_points": {
                        "type": "integer"
                      },
                      "advocate_points": {
                        "type": "integer"
                      },
                      "qualifying_min_amount": {
                        "type": "string"
                      },
                      "claim_expiry_days": {
                        "type": "integer"
                      },
                      "advocate_cap_friends": {
                        "type": "integer"
                      }
                    }
                  }
                ]
              },
              "after": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "enabled",
                      "friend_points",
                      "advocate_points",
                      "qualifying_min_amount",
                      "claim_expiry_days",
                      "advocate_cap_friends"
                    ],
                    "properties": {
                      "enabled": {
                        "type": "boolean"
                      },
                      "friend_points": {
                        "type": "integer"
                      },
                      "advocate_points": {
                        "type": "integer"
                      },
                      "qualifying_min_amount": {
                        "type": "string"
                      },
                      "claim_expiry_days": {
                        "type": "integer"
                      },
                      "advocate_cap_friends": {
                        "type": "integer"
                      }
                    }
                  }
                ]
              }
            }
          },
          "spend_any_amount": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "before",
              "after"
            ],
            "properties": {
              "before": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "enabled",
                      "min_points",
                      "max_points",
                      "min_subtotal"
                    ],
                    "properties": {
                      "enabled": {
                        "type": "boolean"
                      },
                      "min_points": {
                        "type": "integer"
                      },
                      "max_points": {
                        "type": [
                          "integer",
                          "null"
                        ]
                      },
                      "min_subtotal": {
                        "type": "string"
                      }
                    }
                  }
                ]
              },
              "after": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "enabled",
                      "min_points",
                      "max_points",
                      "min_subtotal"
                    ],
                    "properties": {
                      "enabled": {
                        "type": "boolean"
                      },
                      "min_points": {
                        "type": "integer"
                      },
                      "max_points": {
                        "type": [
                          "integer",
                          "null"
                        ]
                      },
                      "min_subtotal": {
                        "type": "string"
                      }
                    }
                  }
                ]
              }
            }
          },
          "reward": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "before",
              "after"
            ],
            "properties": {
              "before": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/ApiReward"
                  }
                ]
              },
              "after": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/ApiReward"
                  }
                ]
              }
            }
          }
        }
      },
      "Design": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "programmeName",
          "pointsName",
          "pointsNamePlural",
          "heading",
          "storeName",
          "signedOutPrompt",
          "rewardsHeading",
          "earnHeading",
          "activityHeading",
          "accent",
          "cornerRadius",
          "density",
          "textScale",
          "fontFamily",
          "showRewards",
          "showWaysToEarn",
          "showActivity",
          "showExpiry",
          "showSharedWallet",
          "showProductPoints",
          "showRewardsSite",
          "termsUrl",
          "otherShops",
          "productWidget",
          "accountPage",
          "panel",
          "brand"
        ],
        "description": "The Design screen's settings: the colours, words, panel and product-line look, the account page's layout and the terms link, in the names the programme read serves as `appearance`.",
        "properties": {
          "programmeName": {
            "type": "string",
            "maxLength": 30,
            "description": "What the programme is called: the page title and the panel heading. Never empty: an empty one reads as \"Rewards\"."
          },
          "pointsName": {
            "type": "string",
            "maxLength": 16,
            "description": "What one point is called, like \"star\". Never empty."
          },
          "pointsNamePlural": {
            "type": "string",
            "maxLength": 16,
            "description": "The plural, like \"stars\". Never empty."
          },
          "heading": {
            "type": "string",
            "maxLength": 60,
            "description": "The panel heading when it should differ from `programmeName`; empty uses `programmeName`."
          },
          "storeName": {
            "type": "string",
            "maxLength": 60,
            "description": "The store's name as shoppers and merchants read it, wherever they are told which shop: the account page, the shared-balance line, \"where else you can spend\" and the Matrix Rewards website. Empty uses the name Shopify has for the shop now (`GET /stores/{store_key}/brand`, `from_shopify.name`), else the name it was installed with. Control characters are removed and the ends trimmed."
          },
          "signedOutPrompt": {
            "type": "string",
            "maxLength": 140,
            "description": "What a shopper who is not signed in reads."
          },
          "rewardsHeading": {
            "type": "string",
            "maxLength": 60,
            "description": "The rewards section heading; empty shows the section with none."
          },
          "earnHeading": {
            "type": "string",
            "maxLength": 60,
            "description": "The ways-to-earn section heading; empty shows the section with none."
          },
          "activityHeading": {
            "type": "string",
            "maxLength": 60,
            "description": "The recent-activity section heading; empty shows the section with none."
          },
          "accent": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
            "description": "The one colour, like \"#4f46e5\" (stored in lower case); null follows the theme."
          },
          "cornerRadius": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 24,
            "description": "Corner rounding in pixels; null follows the theme, and 0 is square."
          },
          "density": {
            "type": "string",
            "enum": [
              "comfortable",
              "compact"
            ]
          },
          "textScale": {
            "type": "string",
            "enum": [
              "small",
              "normal",
              "large"
            ],
            "description": "The panel's text size beside the text around it."
          },
          "fontFamily": {
            "type": "string",
            "enum": [
              "theme",
              "system",
              "serif",
              "mono"
            ],
            "description": "The panel's typeface: the theme's, or one of three named stacks."
          },
          "showRewards": {
            "type": "boolean"
          },
          "showWaysToEarn": {
            "type": "boolean"
          },
          "showActivity": {
            "type": "boolean"
          },
          "showExpiry": {
            "type": "boolean"
          },
          "showSharedWallet": {
            "type": "boolean",
            "description": "The shared-balance line, at an account with more than one store."
          },
          "showProductPoints": {
            "type": "boolean",
            "description": "The \"Points on this product\" line on product pages, everywhere at once."
          },
          "showRewardsSite": {
            "type": "boolean",
            "description": "A link on the account page to the Matrix Rewards website, where a shopper signs in with their email to see the points they hold at each store that uses Matrix Rewards. Off unless the merchant switches it on."
          },
          "termsUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "A full https:// address for the programme's terms; null hides the link."
          },
          "otherShops": {
            "type": "object",
            "description": "The shops sharing this store's balance, by store key: whether the account page lists each under \"Where else you can spend your points\", and the web address its link opens (null: the shop's own Shopify address). A shop that does not share the balance is never listed, whatever is set here. A shop with no entry is listed. Keys are store keys (lowercase letters, digits and hyphens); at most 50.",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "show",
                "url"
              ],
              "properties": {
                "show": {
                  "type": "boolean"
                },
                "url": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "productWidget": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "variant",
              "icon",
              "lines",
              "words"
            ],
            "properties": {
              "variant": {
                "type": "string",
                "enum": [
                  "boxed",
                  "tinted",
                  "plain",
                  "badge"
                ]
              },
              "icon": {
                "type": "string",
                "enum": [
                  "none",
                  "star",
                  "gift",
                  "coin"
                ]
              },
              "lines": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "earn",
                  "redeem",
                  "signin"
                ],
                "properties": {
                  "earn": {
                    "type": "boolean"
                  },
                  "redeem": {
                    "type": "boolean"
                  },
                  "signin": {
                    "type": "boolean"
                  }
                },
                "description": "Which of its lines show (each also needs the placed block's own checkbox)."
              },
              "words": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "earn",
                  "rate",
                  "ready",
                  "gap",
                  "signin"
                ],
                "properties": {
                  "earn": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "rate": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "ready": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "gap": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "signin": {
                    "type": "string",
                    "maxLength": 80
                  }
                },
                "description": "Its sentences; empty is the default in the shopper's language. `[points]`, `[reward]` and `[amount]` are filled in on the page."
              }
            },
            "description": "The product-page line's look."
          },
          "accountPage": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "width",
              "cards",
              "corners",
              "button",
              "progress",
              "order"
            ],
            "properties": {
              "width": {
                "type": "string",
                "enum": [
                  "base",
                  "large"
                ]
              },
              "cards": {
                "type": "string",
                "enum": [
                  "outlined",
                  "shaded",
                  "plain"
                ]
              },
              "corners": {
                "type": "string",
                "enum": [
                  "none",
                  "small",
                  "base",
                  "large"
                ]
              },
              "button": {
                "type": "string",
                "enum": [
                  "primary",
                  "secondary"
                ]
              },
              "progress": {
                "type": "boolean",
                "description": "A progress bar towards the next reward."
              },
              "order": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "codes",
                    "rewards",
                    "ways",
                    "activity",
                    "birthday",
                    "referral"
                  ]
                },
                "description": "The sections in order, each once."
              }
            },
            "description": "The Rewards page in the shopper's Shopify account, in Shopify's own named values: that page takes the store's checkout and accounts branding, so no colour or font is set here."
          },
          "panel": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "variant",
              "surface",
              "layout",
              "words"
            ],
            "properties": {
              "variant": {
                "type": "string",
                "enum": [
                  "card",
                  "minimal",
                  "bold"
                ]
              },
              "surface": {
                "type": [
                  "string",
                  "null"
                ],
                "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
                "description": "A background colour for the panel; null follows the theme."
              },
              "layout": {
                "type": "string",
                "enum": [
                  "grid",
                  "list"
                ]
              },
              "words": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "signIn",
                  "use"
                ],
                "properties": {
                  "signIn": {
                    "type": "string",
                    "maxLength": 40
                  },
                  "use": {
                    "type": "string",
                    "maxLength": 40
                  }
                },
                "description": "The words on its two links; empty is the default in the shopper's language."
              }
            },
            "description": "The storefront rewards panel's look."
          },
          "brand": {
            "$ref": "#/components/schemas/BrandDesign"
          }
        }
      },
      "DesignChangeRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Any of the design's settings; each one sent is changed, and the rest are left as they are. A group (`productWidget`, `accountPage`, `panel`, and their `lines` and `words`) is changed one setting at a time: `{ \"panel\": { \"variant\": \"bold\" } }` keeps the panel's layout. `accountPage.order` is replaced whole. At least one setting.",
        "properties": {
          "programmeName": {
            "type": "string",
            "maxLength": 30,
            "description": "What the programme is called: the page title and the panel heading. Never empty: an empty one reads as \"Rewards\"."
          },
          "pointsName": {
            "type": "string",
            "maxLength": 16,
            "description": "What one point is called, like \"star\". Never empty."
          },
          "pointsNamePlural": {
            "type": "string",
            "maxLength": 16,
            "description": "The plural, like \"stars\". Never empty."
          },
          "heading": {
            "type": "string",
            "maxLength": 60,
            "description": "The panel heading when it should differ from `programmeName`; empty uses `programmeName`."
          },
          "storeName": {
            "type": "string",
            "maxLength": 60,
            "description": "The store's name as shoppers and merchants read it, wherever they are told which shop: the account page, the shared-balance line, \"where else you can spend\" and the Matrix Rewards website. Empty uses the name Shopify has for the shop now (`GET /stores/{store_key}/brand`, `from_shopify.name`), else the name it was installed with. Control characters are removed and the ends trimmed."
          },
          "signedOutPrompt": {
            "type": "string",
            "maxLength": 140,
            "description": "What a shopper who is not signed in reads."
          },
          "rewardsHeading": {
            "type": "string",
            "maxLength": 60,
            "description": "The rewards section heading; empty shows the section with none."
          },
          "earnHeading": {
            "type": "string",
            "maxLength": 60,
            "description": "The ways-to-earn section heading; empty shows the section with none."
          },
          "activityHeading": {
            "type": "string",
            "maxLength": 60,
            "description": "The recent-activity section heading; empty shows the section with none."
          },
          "accent": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
            "description": "The one colour, like \"#4f46e5\" (stored in lower case); null follows the theme."
          },
          "cornerRadius": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 24,
            "description": "Corner rounding in pixels; null follows the theme, and 0 is square."
          },
          "density": {
            "type": "string",
            "enum": [
              "comfortable",
              "compact"
            ]
          },
          "textScale": {
            "type": "string",
            "enum": [
              "small",
              "normal",
              "large"
            ],
            "description": "The panel's text size beside the text around it."
          },
          "fontFamily": {
            "type": "string",
            "enum": [
              "theme",
              "system",
              "serif",
              "mono"
            ],
            "description": "The panel's typeface: the theme's, or one of three named stacks."
          },
          "showRewards": {
            "type": "boolean"
          },
          "showWaysToEarn": {
            "type": "boolean"
          },
          "showActivity": {
            "type": "boolean"
          },
          "showExpiry": {
            "type": "boolean"
          },
          "showSharedWallet": {
            "type": "boolean",
            "description": "The shared-balance line, at an account with more than one store."
          },
          "showProductPoints": {
            "type": "boolean",
            "description": "The \"Points on this product\" line on product pages, everywhere at once."
          },
          "showRewardsSite": {
            "type": "boolean",
            "description": "A link on the account page to the Matrix Rewards website, where a shopper signs in with their email to see the points they hold at each store that uses Matrix Rewards. Off unless the merchant switches it on."
          },
          "termsUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "A full https:// address for the programme's terms; null or \"\" hides the link."
          },
          "otherShops": {
            "type": "object",
            "description": "The shops sharing this store's balance, by store key: whether the account page lists each under \"Where else you can spend your points\", and the web address its link opens (null: the shop's own Shopify address). A shop that does not share the balance is never listed, whatever is set here. A shop with no entry is listed. Sent entries are laid over the stored ones; \"\" or null for the address uses the Shopify address. Keys are store keys (lowercase letters, digits and hyphens); at most 50.",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "show": {
                  "type": "boolean"
                },
                "url": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "productWidget": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "variant": {
                "type": "string",
                "enum": [
                  "boxed",
                  "tinted",
                  "plain",
                  "badge"
                ]
              },
              "icon": {
                "type": "string",
                "enum": [
                  "none",
                  "star",
                  "gift",
                  "coin"
                ]
              },
              "lines": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "earn": {
                    "type": "boolean"
                  },
                  "redeem": {
                    "type": "boolean"
                  },
                  "signin": {
                    "type": "boolean"
                  }
                },
                "description": "Which of its lines show (each also needs the placed block's own checkbox)."
              },
              "words": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "earn": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "rate": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "ready": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "gap": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "signin": {
                    "type": "string",
                    "maxLength": 80
                  }
                },
                "description": "Its sentences; empty is the default in the shopper's language. `[points]`, `[reward]` and `[amount]` are filled in on the page."
              }
            },
            "description": "The product-page line's look."
          },
          "accountPage": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "width": {
                "type": "string",
                "enum": [
                  "base",
                  "large"
                ]
              },
              "cards": {
                "type": "string",
                "enum": [
                  "outlined",
                  "shaded",
                  "plain"
                ]
              },
              "corners": {
                "type": "string",
                "enum": [
                  "none",
                  "small",
                  "base",
                  "large"
                ]
              },
              "button": {
                "type": "string",
                "enum": [
                  "primary",
                  "secondary"
                ]
              },
              "progress": {
                "type": "boolean",
                "description": "A progress bar towards the next reward."
              },
              "order": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "codes",
                    "rewards",
                    "ways",
                    "activity",
                    "birthday",
                    "referral"
                  ]
                },
                "description": "The sections in order, each once."
              }
            },
            "description": "The Rewards page in the shopper's Shopify account, in Shopify's own named values: that page takes the store's checkout and accounts branding, so no colour or font is set here."
          },
          "panel": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "variant": {
                "type": "string",
                "enum": [
                  "card",
                  "minimal",
                  "bold"
                ]
              },
              "surface": {
                "type": [
                  "string",
                  "null"
                ],
                "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
                "description": "A background colour for the panel; null follows the theme."
              },
              "layout": {
                "type": "string",
                "enum": [
                  "grid",
                  "list"
                ]
              },
              "words": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "signIn": {
                    "type": "string",
                    "maxLength": 40
                  },
                  "use": {
                    "type": "string",
                    "maxLength": 40
                  }
                },
                "description": "The words on its two links; empty is the default in the shopper's language."
              }
            },
            "description": "The storefront rewards panel's look."
          },
          "if_version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "The `version` from a design read. If the design has changed since, nothing is changed (409 `settings_changed`)."
          },
          "brand": {
            "type": "object",
            "additionalProperties": false,
            "description": "The branding settings to change, one at a time: `{ \"brand\": { \"logo\": \"img_…\" } }` keeps the rest.",
            "properties": {
              "logo": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The store's logo. null uses the picture Shopify has (Settings → Brand); `none` shows none; `img_…` is a picture uploaded to this store (`POST /stores/{store_key}/images`). Another store's picture is refused.",
                "pattern": "^(none|img_[0-9a-f]{32})$"
              },
              "squareLogo": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The logo for square places (the store's tile). null uses the picture Shopify has (Settings → Brand); `none` shows none; `img_…` is a picture uploaded to this store (`POST /stores/{store_key}/images`). Another store's picture is refused.",
                "pattern": "^(none|img_[0-9a-f]{32})$"
              },
              "coverImage": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "A wide picture for the store's card. null uses the picture Shopify has (Settings → Brand); `none` shows none; `img_…` is a picture uploaded to this store (`POST /stores/{store_key}/images`). Another store's picture is refused.",
                "pattern": "^(none|img_[0-9a-f]{32})$"
              },
              "colour": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The brand colour, or null for Shopify's primary brand colour.",
                "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
              },
              "textColour": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The text colour on it, or null for Shopify's, else worked out. Used only where it is readable on the colour (the WCAG AA contrast for normal text); otherwise black or white is used.",
                "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
              },
              "slogan": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "A line under the store's name, or null for Shopify's slogan; empty shows none. Up to 80 characters.",
                "maxLength": 80
              },
              "font": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The typeface of the store's name and points on the website, from a fixed list of fonts we host (no family of your own: a page only ever sets one of these). null uses ours, playfair-display; system is the shopper's device font. Shopify has no font setting, so there is nothing of Shopify's to fall back on.",
                "enum": [
                  "playfair-display",
                  "fraunces",
                  "lora",
                  "inter",
                  "dm-sans",
                  "nunito",
                  "space-grotesk",
                  "system",
                  null
                ]
              }
            }
          }
        }
      },
      "DesignDocument": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "store_key",
          "version",
          "design"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "design"
          },
          "store_key": {
            "type": "string"
          },
          "version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "Which design this is: what a design change sends back as `if_version`."
          },
          "design": {
            "$ref": "#/components/schemas/Design"
          }
        }
      },
      "DesignChange": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "changed",
          "design",
          "store_key",
          "version",
          "audit_id"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "design_change"
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "programmeName",
                "pointsName",
                "pointsNamePlural",
                "heading",
                "storeName",
                "signedOutPrompt",
                "rewardsHeading",
                "earnHeading",
                "activityHeading",
                "accent",
                "cornerRadius",
                "density",
                "textScale",
                "fontFamily",
                "showRewards",
                "showWaysToEarn",
                "showActivity",
                "showExpiry",
                "showSharedWallet",
                "showProductPoints",
                "showRewardsSite",
                "termsUrl",
                "otherShops",
                "productWidget",
                "accountPage",
                "panel"
              ]
            },
            "description": "The settings that changed (a group by its own name). Empty when the change asked for the design already set."
          },
          "design": {
            "$ref": "#/components/schemas/Design"
          },
          "store_key": {
            "type": "string"
          },
          "version": {
            "type": "string",
            "pattern": "^[0-9a-f]{32}$",
            "description": "The design's version after the change."
          },
          "audit_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "DesignChangePreview": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "dry_run",
          "outcome",
          "store_key",
          "changed",
          "before_after",
          "version_after",
          "confirmation",
          "audit_id"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "design_change_preview"
          },
          "dry_run": {
            "type": "boolean",
            "const": true
          },
          "outcome": {
            "type": "string",
            "enum": [
              "would_apply",
              "no_change"
            ]
          },
          "store_key": {
            "type": "string"
          },
          "changed": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "programmeName",
                "pointsName",
                "pointsNamePlural",
                "heading",
                "storeName",
                "signedOutPrompt",
                "rewardsHeading",
                "earnHeading",
                "activityHeading",
                "accent",
                "cornerRadius",
                "density",
                "textScale",
                "fontFamily",
                "showRewards",
                "showWaysToEarn",
                "showActivity",
                "showExpiry",
                "showSharedWallet",
                "showProductPoints",
                "showRewardsSite",
                "termsUrl",
                "otherShops",
                "productWidget",
                "accountPage",
                "panel"
              ]
            },
            "description": "The settings that would change."
          },
          "before_after": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "programmeName": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "string",
                    "maxLength": 30
                  },
                  "after": {
                    "type": "string",
                    "maxLength": 30
                  }
                }
              },
              "pointsName": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "string",
                    "maxLength": 16
                  },
                  "after": {
                    "type": "string",
                    "maxLength": 16
                  }
                }
              },
              "pointsNamePlural": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "string",
                    "maxLength": 16
                  },
                  "after": {
                    "type": "string",
                    "maxLength": 16
                  }
                }
              },
              "heading": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "after": {
                    "type": "string",
                    "maxLength": 60
                  }
                }
              },
              "storeName": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "after": {
                    "type": "string",
                    "maxLength": 60
                  }
                }
              },
              "signedOutPrompt": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "string",
                    "maxLength": 140
                  },
                  "after": {
                    "type": "string",
                    "maxLength": 140
                  }
                }
              },
              "rewardsHeading": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "after": {
                    "type": "string",
                    "maxLength": 60
                  }
                }
              },
              "earnHeading": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "after": {
                    "type": "string",
                    "maxLength": 60
                  }
                }
              },
              "activityHeading": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "after": {
                    "type": "string",
                    "maxLength": 60
                  }
                }
              },
              "accent": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
                  },
                  "after": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
                  }
                }
              },
              "cornerRadius": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 24
                  },
                  "after": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 24
                  }
                }
              },
              "density": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "string",
                    "enum": [
                      "comfortable",
                      "compact"
                    ]
                  },
                  "after": {
                    "type": "string",
                    "enum": [
                      "comfortable",
                      "compact"
                    ]
                  }
                }
              },
              "textScale": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "string",
                    "enum": [
                      "small",
                      "normal",
                      "large"
                    ]
                  },
                  "after": {
                    "type": "string",
                    "enum": [
                      "small",
                      "normal",
                      "large"
                    ]
                  }
                }
              },
              "fontFamily": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "string",
                    "enum": [
                      "theme",
                      "system",
                      "serif",
                      "mono"
                    ]
                  },
                  "after": {
                    "type": "string",
                    "enum": [
                      "theme",
                      "system",
                      "serif",
                      "mono"
                    ]
                  }
                }
              },
              "showRewards": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "boolean"
                  },
                  "after": {
                    "type": "boolean"
                  }
                }
              },
              "showWaysToEarn": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "boolean"
                  },
                  "after": {
                    "type": "boolean"
                  }
                }
              },
              "showActivity": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "boolean"
                  },
                  "after": {
                    "type": "boolean"
                  }
                }
              },
              "showExpiry": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "boolean"
                  },
                  "after": {
                    "type": "boolean"
                  }
                }
              },
              "showSharedWallet": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "boolean"
                  },
                  "after": {
                    "type": "boolean"
                  }
                }
              },
              "showProductPoints": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "boolean"
                  },
                  "after": {
                    "type": "boolean"
                  }
                }
              },
              "showRewardsSite": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "boolean"
                  },
                  "after": {
                    "type": "boolean"
                  }
                }
              },
              "termsUrl": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "after": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              },
              "otherShops": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "object",
                    "description": "The shops sharing this store's balance, by store key: whether the account page lists each under \"Where else you can spend your points\", and the web address its link opens (null: the shop's own Shopify address). A shop that does not share the balance is never listed, whatever is set here. A shop with no entry is listed. Keys are store keys (lowercase letters, digits and hyphens); at most 50.",
                    "additionalProperties": {
                      "type": "object",
                      "additionalProperties": false,
                      "required": [
                        "show",
                        "url"
                      ],
                      "properties": {
                        "show": {
                          "type": "boolean"
                        },
                        "url": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  },
                  "after": {
                    "type": "object",
                    "description": "The shops sharing this store's balance, by store key: whether the account page lists each under \"Where else you can spend your points\", and the web address its link opens (null: the shop's own Shopify address). A shop that does not share the balance is never listed, whatever is set here. A shop with no entry is listed. Keys are store keys (lowercase letters, digits and hyphens); at most 50.",
                    "additionalProperties": {
                      "type": "object",
                      "additionalProperties": false,
                      "required": [
                        "show",
                        "url"
                      ],
                      "properties": {
                        "show": {
                          "type": "boolean"
                        },
                        "url": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                }
              },
              "productWidget": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "variant",
                      "icon",
                      "lines",
                      "words"
                    ],
                    "properties": {
                      "variant": {
                        "type": "string",
                        "enum": [
                          "boxed",
                          "tinted",
                          "plain",
                          "badge"
                        ]
                      },
                      "icon": {
                        "type": "string",
                        "enum": [
                          "none",
                          "star",
                          "gift",
                          "coin"
                        ]
                      },
                      "lines": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "earn",
                          "redeem",
                          "signin"
                        ],
                        "properties": {
                          "earn": {
                            "type": "boolean"
                          },
                          "redeem": {
                            "type": "boolean"
                          },
                          "signin": {
                            "type": "boolean"
                          }
                        },
                        "description": "Which of its lines show (each also needs the placed block's own checkbox)."
                      },
                      "words": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "earn",
                          "rate",
                          "ready",
                          "gap",
                          "signin"
                        ],
                        "properties": {
                          "earn": {
                            "type": "string",
                            "maxLength": 80
                          },
                          "rate": {
                            "type": "string",
                            "maxLength": 80
                          },
                          "ready": {
                            "type": "string",
                            "maxLength": 80
                          },
                          "gap": {
                            "type": "string",
                            "maxLength": 80
                          },
                          "signin": {
                            "type": "string",
                            "maxLength": 80
                          }
                        },
                        "description": "Its sentences; empty is the default in the shopper's language. `[points]`, `[reward]` and `[amount]` are filled in on the page."
                      }
                    }
                  },
                  "after": {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "variant",
                      "icon",
                      "lines",
                      "words"
                    ],
                    "properties": {
                      "variant": {
                        "type": "string",
                        "enum": [
                          "boxed",
                          "tinted",
                          "plain",
                          "badge"
                        ]
                      },
                      "icon": {
                        "type": "string",
                        "enum": [
                          "none",
                          "star",
                          "gift",
                          "coin"
                        ]
                      },
                      "lines": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "earn",
                          "redeem",
                          "signin"
                        ],
                        "properties": {
                          "earn": {
                            "type": "boolean"
                          },
                          "redeem": {
                            "type": "boolean"
                          },
                          "signin": {
                            "type": "boolean"
                          }
                        },
                        "description": "Which of its lines show (each also needs the placed block's own checkbox)."
                      },
                      "words": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "earn",
                          "rate",
                          "ready",
                          "gap",
                          "signin"
                        ],
                        "properties": {
                          "earn": {
                            "type": "string",
                            "maxLength": 80
                          },
                          "rate": {
                            "type": "string",
                            "maxLength": 80
                          },
                          "ready": {
                            "type": "string",
                            "maxLength": 80
                          },
                          "gap": {
                            "type": "string",
                            "maxLength": 80
                          },
                          "signin": {
                            "type": "string",
                            "maxLength": 80
                          }
                        },
                        "description": "Its sentences; empty is the default in the shopper's language. `[points]`, `[reward]` and `[amount]` are filled in on the page."
                      }
                    }
                  }
                }
              },
              "accountPage": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "width",
                      "cards",
                      "corners",
                      "button",
                      "progress",
                      "order"
                    ],
                    "properties": {
                      "width": {
                        "type": "string",
                        "enum": [
                          "base",
                          "large"
                        ]
                      },
                      "cards": {
                        "type": "string",
                        "enum": [
                          "outlined",
                          "shaded",
                          "plain"
                        ]
                      },
                      "corners": {
                        "type": "string",
                        "enum": [
                          "none",
                          "small",
                          "base",
                          "large"
                        ]
                      },
                      "button": {
                        "type": "string",
                        "enum": [
                          "primary",
                          "secondary"
                        ]
                      },
                      "progress": {
                        "type": "boolean",
                        "description": "A progress bar towards the next reward."
                      },
                      "order": {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "enum": [
                            "codes",
                            "rewards",
                            "ways",
                            "activity",
                            "birthday",
                            "referral"
                          ]
                        },
                        "description": "The sections in order, each once."
                      }
                    }
                  },
                  "after": {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "width",
                      "cards",
                      "corners",
                      "button",
                      "progress",
                      "order"
                    ],
                    "properties": {
                      "width": {
                        "type": "string",
                        "enum": [
                          "base",
                          "large"
                        ]
                      },
                      "cards": {
                        "type": "string",
                        "enum": [
                          "outlined",
                          "shaded",
                          "plain"
                        ]
                      },
                      "corners": {
                        "type": "string",
                        "enum": [
                          "none",
                          "small",
                          "base",
                          "large"
                        ]
                      },
                      "button": {
                        "type": "string",
                        "enum": [
                          "primary",
                          "secondary"
                        ]
                      },
                      "progress": {
                        "type": "boolean",
                        "description": "A progress bar towards the next reward."
                      },
                      "order": {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "enum": [
                            "codes",
                            "rewards",
                            "ways",
                            "activity",
                            "birthday",
                            "referral"
                          ]
                        },
                        "description": "The sections in order, each once."
                      }
                    }
                  }
                }
              },
              "panel": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "variant",
                      "surface",
                      "layout",
                      "words"
                    ],
                    "properties": {
                      "variant": {
                        "type": "string",
                        "enum": [
                          "card",
                          "minimal",
                          "bold"
                        ]
                      },
                      "surface": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
                        "description": "A background colour for the panel; null follows the theme."
                      },
                      "layout": {
                        "type": "string",
                        "enum": [
                          "grid",
                          "list"
                        ]
                      },
                      "words": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "signIn",
                          "use"
                        ],
                        "properties": {
                          "signIn": {
                            "type": "string",
                            "maxLength": 40
                          },
                          "use": {
                            "type": "string",
                            "maxLength": 40
                          }
                        },
                        "description": "The words on its two links; empty is the default in the shopper's language."
                      }
                    }
                  },
                  "after": {
                    "type": "object",
                    "additionalProperties": false,
                    "required": [
                      "variant",
                      "surface",
                      "layout",
                      "words"
                    ],
                    "properties": {
                      "variant": {
                        "type": "string",
                        "enum": [
                          "card",
                          "minimal",
                          "bold"
                        ]
                      },
                      "surface": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
                        "description": "A background colour for the panel; null follows the theme."
                      },
                      "layout": {
                        "type": "string",
                        "enum": [
                          "grid",
                          "list"
                        ]
                      },
                      "words": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "signIn",
                          "use"
                        ],
                        "properties": {
                          "signIn": {
                            "type": "string",
                            "maxLength": 40
                          },
                          "use": {
                            "type": "string",
                            "maxLength": 40
                          }
                        },
                        "description": "The words on its two links; empty is the default in the shopper's language."
                      }
                    }
                  }
                }
              },
              "brand": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "before",
                  "after"
                ],
                "properties": {
                  "before": {
                    "$ref": "#/components/schemas/BrandDesign"
                  },
                  "after": {
                    "$ref": "#/components/schemas/BrandDesign"
                  }
                }
              }
            },
            "description": "Each setting that would change, whole (a group as a group), before and after. `{}` when none would."
          },
          "version_after": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{32}$",
            "description": "The version the design would have."
          },
          "confirmation": {
            "description": "For a key tagged for an assistant: send `token` back as the `Matrix-Confirmation` header with the same request. Null when nothing would change.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "token",
                  "expires_at"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "expires_at": {
                    "type": "string"
                  }
                }
              }
            ]
          },
          "audit_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "BrandDesign": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "logo",
          "squareLogo",
          "coverImage",
          "colour",
          "textColour",
          "slogan",
          "font"
        ],
        "description": "The store's branding on the Matrix Rewards website (app.matrixrewards.co.uk): its logos, cover picture, colours and slogan. Each null uses what Shopify has for the shop. Never reaches the storefront or the account page.",
        "properties": {
          "logo": {
            "type": [
              "string",
              "null"
            ],
            "description": "The store's logo. null uses the picture Shopify has (Settings → Brand); `none` shows none; `img_…` is a picture uploaded to this store (`POST /stores/{store_key}/images`). Another store's picture is refused.",
            "pattern": "^(none|img_[0-9a-f]{32})$"
          },
          "squareLogo": {
            "type": [
              "string",
              "null"
            ],
            "description": "The logo for square places (the store's tile). null uses the picture Shopify has (Settings → Brand); `none` shows none; `img_…` is a picture uploaded to this store (`POST /stores/{store_key}/images`). Another store's picture is refused.",
            "pattern": "^(none|img_[0-9a-f]{32})$"
          },
          "coverImage": {
            "type": [
              "string",
              "null"
            ],
            "description": "A wide picture for the store's card. null uses the picture Shopify has (Settings → Brand); `none` shows none; `img_…` is a picture uploaded to this store (`POST /stores/{store_key}/images`). Another store's picture is refused.",
            "pattern": "^(none|img_[0-9a-f]{32})$"
          },
          "colour": {
            "type": [
              "string",
              "null"
            ],
            "description": "The brand colour, or null for Shopify's primary brand colour.",
            "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
          },
          "textColour": {
            "type": [
              "string",
              "null"
            ],
            "description": "The text colour on it, or null for Shopify's, else worked out. Used only where it is readable on the colour (the WCAG AA contrast for normal text); otherwise black or white is used.",
            "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
          },
          "slogan": {
            "type": [
              "string",
              "null"
            ],
            "description": "A line under the store's name, or null for Shopify's slogan; empty shows none. Up to 80 characters.",
            "maxLength": 80
          },
          "font": {
            "type": [
              "string",
              "null"
            ],
            "description": "The typeface of the store's name and points on the website, from a fixed list of fonts we host (no family of your own: a page only ever sets one of these). null uses ours, playfair-display; system is the shopper's device font. Shopify has no font setting, so there is nothing of Shopify's to fall back on.",
            "enum": [
              "playfair-display",
              "fraunces",
              "lora",
              "inter",
              "dm-sans",
              "nunito",
              "space-grotesk",
              "system",
              null
            ]
          }
        }
      },
      "ShopifyBrand": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "slogan",
          "shortDescription",
          "logo",
          "squareLogo",
          "coverImage",
          "colour",
          "textColour"
        ],
        "description": "What Shopify has for the shop: its name and its brand settings (Settings → Brand), read without a key and kept, again at most once a day. Picture addresses are Shopify's CDN only; anything else Shopify sent is left out.",
        "properties": {
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "slogan": {
            "type": [
              "string",
              "null"
            ]
          },
          "shortDescription": {
            "type": [
              "string",
              "null"
            ]
          },
          "logo": {
            "type": [
              "string",
              "null"
            ]
          },
          "squareLogo": {
            "type": [
              "string",
              "null"
            ]
          },
          "coverImage": {
            "type": [
              "string",
              "null"
            ]
          },
          "colour": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
          },
          "textColour": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
          }
        }
      },
      "StoreBranding": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "store_key",
          "name",
          "slogan",
          "logo_url",
          "square_logo_url",
          "cover_image_url",
          "colour",
          "text_colour",
          "font",
          "from_shopify",
          "shopify_read_at",
          "design"
        ],
        "description": "The store's name and branding as the Matrix Rewards website shows them: each the design's setting, else Shopify's. Picture addresses are absolute: ours (`https://app.matrixrewards.co.uk/brand-images/…`) or Shopify's CDN.",
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "store_branding"
            ]
          },
          "store_key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slogan": {
            "type": [
              "string",
              "null"
            ]
          },
          "logo_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "square_logo_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "cover_image_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "colour": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
          },
          "text_colour": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
            "description": "Readable on `colour` (WCAG AA contrast for normal text)."
          },
          "font": {
            "type": "string",
            "description": "The typeface the website draws the name and points in: the design's choice, else playfair-display.",
            "enum": [
              "playfair-display",
              "fraunces",
              "lora",
              "inter",
              "dm-sans",
              "nunito",
              "space-grotesk",
              "system"
            ]
          },
          "from_shopify": {
            "$ref": "#/components/schemas/ShopifyBrand"
          },
          "shopify_read_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When Shopify was last read for this store; null before the first read."
          },
          "design": {
            "$ref": "#/components/schemas/BrandDesign"
          }
        }
      },
      "BrandImage": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "id",
          "store_key",
          "content_type",
          "width",
          "height",
          "bytes",
          "source",
          "url",
          "created_at"
        ],
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "brand_image"
            ]
          },
          "id": {
            "type": "string",
            "pattern": "^img_[0-9a-f]{32}$",
            "description": "What the design's `brand.logo`, `brand.squareLogo` or `brand.coverImage` takes."
          },
          "store_key": {
            "type": "string"
          },
          "content_type": {
            "type": "string",
            "enum": [
              "image/png",
              "image/jpeg",
              "image/webp"
            ]
          },
          "width": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          },
          "source": {
            "type": "string",
            "enum": [
              "upload",
              "shopify_files"
            ]
          },
          "url": {
            "type": "string",
            "description": "Where the picture is served, to anyone, for good."
          },
          "created_at": {
            "type": "string"
          }
        }
      },
      "BrandImageList": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "data",
          "has_more",
          "next_cursor"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BrandImage"
            }
          },
          "has_more": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "next_cursor": {
            "type": "null"
          }
        }
      },
      "BrandImageFromShopifyFiles": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "source_url"
        ],
        "properties": {
          "source_url": {
            "type": "string",
            "description": "A picture in your Shopify Files: an https://cdn.shopify.com/ address. No other address is fetched."
          }
        }
      },
      "BrandImageUploaded": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "object",
          "id",
          "store_key",
          "content_type",
          "width",
          "height",
          "bytes",
          "source",
          "url",
          "created_at",
          "created"
        ],
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "brand_image"
            ]
          },
          "id": {
            "type": "string",
            "pattern": "^img_[0-9a-f]{32}$",
            "description": "What the design's `brand.logo`, `brand.squareLogo` or `brand.coverImage` takes."
          },
          "store_key": {
            "type": "string"
          },
          "content_type": {
            "type": "string",
            "enum": [
              "image/png",
              "image/jpeg",
              "image/webp"
            ]
          },
          "width": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          },
          "bytes": {
            "type": "integer"
          },
          "source": {
            "type": "string",
            "enum": [
              "upload",
              "shopify_files"
            ]
          },
          "url": {
            "type": "string",
            "description": "Where the picture is served, to anyone, for good."
          },
          "created_at": {
            "type": "string"
          },
          "created": {
            "type": "boolean",
            "description": "False when the same picture was already kept for the store (nothing new was stored)."
          }
        }
      }
    }
  },
  "webhooks": {
    "points.earned": {
      "post": {
        "summary": "Points were earned: an order, a welcome or birthday bonus, a referral",
        "description": "Each event is POSTed as JSON to every enabled endpoint subscribed to its type, signed the Standard Webhooks way: `webhook-id` (the same on every retry, and the thing to de-duplicate on), `webhook-timestamp` (unix seconds of this attempt; reject one more than 5 minutes from your clock), and `webhook-signature: v1,<base64 HMAC-SHA256>` over `<webhook-id>.<webhook-timestamp>.<body>`, keyed by the base64 after `whsec_` in the endpoint's secret, decoded to bytes. Verify the raw body, before parsing it. Answer 2xx within 15 seconds; anything else, a redirect, or no answer is a failure, and the event is sent again after 5 seconds, 5 minutes, 30 minutes, then 2, 5, 10, 14, 20 and 24 hours (each ±10%) — ten attempts over about 75 hours — with the same id and the same bytes. Events can arrive out of order: `data.ledger_entry_id` and `data.created_at` order them. An endpoint that has failed 10 deliveries in a row AND been failing for at least 24 hours is disabled: nothing more is sent to it, what was waiting is abandoned, `webhook.disabled` is written to the audit log, and GET /webhooks shows `status: disabled`. Delete it and create it again once the receiver works. An endpoint receives the events recorded after it was created.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "`evt_<ledger entry id>`: the same on every attempt and at every endpoint.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Unix seconds when this attempt was sent.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64 HMAC-SHA256>` of `<webhook-id>.<webhook-timestamp>.<body>`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "examples": {
                "example": {
                  "value": {
                    "type": "points.earned",
                    "timestamp": "2026-09-16T10:00:05.000Z",
                    "data": {
                      "ledger_entry_id": 4,
                      "store_key": "example-store",
                      "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                      "delta_points": 120,
                      "rule_id": "base_earn",
                      "source_type": "order",
                      "created_at": "2026-09-16T10:00:00Z"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else, a redirect, or no answer within 15 seconds is retried."
          }
        }
      }
    },
    "points.adjusted": {
      "post": {
        "summary": "The merchant changed a member's points: in the app, on the web page, or through this API",
        "description": "Each event is POSTed as JSON to every enabled endpoint subscribed to its type, signed the Standard Webhooks way: `webhook-id` (the same on every retry, and the thing to de-duplicate on), `webhook-timestamp` (unix seconds of this attempt; reject one more than 5 minutes from your clock), and `webhook-signature: v1,<base64 HMAC-SHA256>` over `<webhook-id>.<webhook-timestamp>.<body>`, keyed by the base64 after `whsec_` in the endpoint's secret, decoded to bytes. Verify the raw body, before parsing it. Answer 2xx within 15 seconds; anything else, a redirect, or no answer is a failure, and the event is sent again after 5 seconds, 5 minutes, 30 minutes, then 2, 5, 10, 14, 20 and 24 hours (each ±10%) — ten attempts over about 75 hours — with the same id and the same bytes. Events can arrive out of order: `data.ledger_entry_id` and `data.created_at` order them. An endpoint that has failed 10 deliveries in a row AND been failing for at least 24 hours is disabled: nothing more is sent to it, what was waiting is abandoned, `webhook.disabled` is written to the audit log, and GET /webhooks shows `status: disabled`. Delete it and create it again once the receiver works. An endpoint receives the events recorded after it was created.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "`evt_<ledger entry id>`: the same on every attempt and at every endpoint.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Unix seconds when this attempt was sent.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64 HMAC-SHA256>` of `<webhook-id>.<webhook-timestamp>.<body>`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "examples": {
                "example": {
                  "value": {
                    "type": "points.adjusted",
                    "timestamp": "2026-09-16T10:00:05.000Z",
                    "data": {
                      "ledger_entry_id": 2,
                      "store_key": "example-store",
                      "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                      "delta_points": 50,
                      "rule_id": "admin_adjustment",
                      "source_type": "admin_adjustment",
                      "created_at": "2026-09-16T10:00:00.000Z",
                      "adjustment_id": "goodwill-2026-09-16-0001"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else, a redirect, or no answer within 15 seconds is retried."
          }
        }
      }
    },
    "points.redeemed": {
      "post": {
        "summary": "Points were spent on a reward, or taken back because a cancelled code had been spent",
        "description": "Each event is POSTed as JSON to every enabled endpoint subscribed to its type, signed the Standard Webhooks way: `webhook-id` (the same on every retry, and the thing to de-duplicate on), `webhook-timestamp` (unix seconds of this attempt; reject one more than 5 minutes from your clock), and `webhook-signature: v1,<base64 HMAC-SHA256>` over `<webhook-id>.<webhook-timestamp>.<body>`, keyed by the base64 after `whsec_` in the endpoint's secret, decoded to bytes. Verify the raw body, before parsing it. Answer 2xx within 15 seconds; anything else, a redirect, or no answer is a failure, and the event is sent again after 5 seconds, 5 minutes, 30 minutes, then 2, 5, 10, 14, 20 and 24 hours (each ±10%) — ten attempts over about 75 hours — with the same id and the same bytes. Events can arrive out of order: `data.ledger_entry_id` and `data.created_at` order them. An endpoint that has failed 10 deliveries in a row AND been failing for at least 24 hours is disabled: nothing more is sent to it, what was waiting is abandoned, `webhook.disabled` is written to the audit log, and GET /webhooks shows `status: disabled`. Delete it and create it again once the receiver works. An endpoint receives the events recorded after it was created.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "`evt_<ledger entry id>`: the same on every attempt and at every endpoint.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Unix seconds when this attempt was sent.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64 HMAC-SHA256>` of `<webhook-id>.<webhook-timestamp>.<body>`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "examples": {
                "example": {
                  "value": {
                    "type": "points.redeemed",
                    "timestamp": "2026-09-16T10:00:05.000Z",
                    "data": {
                      "ledger_entry_id": 5,
                      "store_key": "example-store",
                      "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                      "delta_points": -500,
                      "rule_id": "redeem",
                      "source_type": "redeem",
                      "created_at": "2026-09-16T10:00:00Z"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else, a redirect, or no answer within 15 seconds is retried."
          }
        }
      }
    },
    "points.returned": {
      "post": {
        "summary": "Points came back: a reward was cancelled, or could not be issued",
        "description": "Each event is POSTed as JSON to every enabled endpoint subscribed to its type, signed the Standard Webhooks way: `webhook-id` (the same on every retry, and the thing to de-duplicate on), `webhook-timestamp` (unix seconds of this attempt; reject one more than 5 minutes from your clock), and `webhook-signature: v1,<base64 HMAC-SHA256>` over `<webhook-id>.<webhook-timestamp>.<body>`, keyed by the base64 after `whsec_` in the endpoint's secret, decoded to bytes. Verify the raw body, before parsing it. Answer 2xx within 15 seconds; anything else, a redirect, or no answer is a failure, and the event is sent again after 5 seconds, 5 minutes, 30 minutes, then 2, 5, 10, 14, 20 and 24 hours (each ±10%) — ten attempts over about 75 hours — with the same id and the same bytes. Events can arrive out of order: `data.ledger_entry_id` and `data.created_at` order them. An endpoint that has failed 10 deliveries in a row AND been failing for at least 24 hours is disabled: nothing more is sent to it, what was waiting is abandoned, `webhook.disabled` is written to the audit log, and GET /webhooks shows `status: disabled`. Delete it and create it again once the receiver works. An endpoint receives the events recorded after it was created.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "`evt_<ledger entry id>`: the same on every attempt and at every endpoint.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Unix seconds when this attempt was sent.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64 HMAC-SHA256>` of `<webhook-id>.<webhook-timestamp>.<body>`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "examples": {
                "example": {
                  "value": {
                    "type": "points.returned",
                    "timestamp": "2026-09-16T10:00:05.000Z",
                    "data": {
                      "ledger_entry_id": 3,
                      "store_key": "example-store",
                      "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                      "delta_points": 500,
                      "rule_id": "redeem_void",
                      "source_type": "redeem_void",
                      "created_at": "2026-09-16T10:00:00.000Z"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else, a redirect, or no answer within 15 seconds is retried."
          }
        }
      }
    },
    "points.clawed_back": {
      "post": {
        "summary": "Points were taken back after a refund",
        "description": "Each event is POSTed as JSON to every enabled endpoint subscribed to its type, signed the Standard Webhooks way: `webhook-id` (the same on every retry, and the thing to de-duplicate on), `webhook-timestamp` (unix seconds of this attempt; reject one more than 5 minutes from your clock), and `webhook-signature: v1,<base64 HMAC-SHA256>` over `<webhook-id>.<webhook-timestamp>.<body>`, keyed by the base64 after `whsec_` in the endpoint's secret, decoded to bytes. Verify the raw body, before parsing it. Answer 2xx within 15 seconds; anything else, a redirect, or no answer is a failure, and the event is sent again after 5 seconds, 5 minutes, 30 minutes, then 2, 5, 10, 14, 20 and 24 hours (each ±10%) — ten attempts over about 75 hours — with the same id and the same bytes. Events can arrive out of order: `data.ledger_entry_id` and `data.created_at` order them. An endpoint that has failed 10 deliveries in a row AND been failing for at least 24 hours is disabled: nothing more is sent to it, what was waiting is abandoned, `webhook.disabled` is written to the audit log, and GET /webhooks shows `status: disabled`. Delete it and create it again once the receiver works. An endpoint receives the events recorded after it was created.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "`evt_<ledger entry id>`: the same on every attempt and at every endpoint.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Unix seconds when this attempt was sent.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64 HMAC-SHA256>` of `<webhook-id>.<webhook-timestamp>.<body>`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "examples": {
                "example": {
                  "value": {
                    "type": "points.clawed_back",
                    "timestamp": "2026-09-16T10:00:05.000Z",
                    "data": {
                      "ledger_entry_id": 6,
                      "store_key": "example-store",
                      "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                      "delta_points": -40,
                      "rule_id": "refund",
                      "source_type": "refund",
                      "created_at": "2026-09-16T10:00:00Z"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else, a redirect, or no answer within 15 seconds is retried."
          }
        }
      }
    },
    "points.expired": {
      "post": {
        "summary": "Points expired",
        "description": "Each event is POSTed as JSON to every enabled endpoint subscribed to its type, signed the Standard Webhooks way: `webhook-id` (the same on every retry, and the thing to de-duplicate on), `webhook-timestamp` (unix seconds of this attempt; reject one more than 5 minutes from your clock), and `webhook-signature: v1,<base64 HMAC-SHA256>` over `<webhook-id>.<webhook-timestamp>.<body>`, keyed by the base64 after `whsec_` in the endpoint's secret, decoded to bytes. Verify the raw body, before parsing it. Answer 2xx within 15 seconds; anything else, a redirect, or no answer is a failure, and the event is sent again after 5 seconds, 5 minutes, 30 minutes, then 2, 5, 10, 14, 20 and 24 hours (each ±10%) — ten attempts over about 75 hours — with the same id and the same bytes. Events can arrive out of order: `data.ledger_entry_id` and `data.created_at` order them. An endpoint that has failed 10 deliveries in a row AND been failing for at least 24 hours is disabled: nothing more is sent to it, what was waiting is abandoned, `webhook.disabled` is written to the audit log, and GET /webhooks shows `status: disabled`. Delete it and create it again once the receiver works. An endpoint receives the events recorded after it was created.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "`evt_<ledger entry id>`: the same on every attempt and at every endpoint.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Unix seconds when this attempt was sent.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64 HMAC-SHA256>` of `<webhook-id>.<webhook-timestamp>.<body>`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "examples": {
                "example": {
                  "value": {
                    "type": "points.expired",
                    "timestamp": "2026-09-16T10:00:05.000Z",
                    "data": {
                      "ledger_entry_id": 7,
                      "store_key": "example-store",
                      "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                      "delta_points": -80,
                      "rule_id": "expiry",
                      "source_type": "expiry",
                      "created_at": "2026-09-16T10:00:00Z"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else, a redirect, or no answer within 15 seconds is retried."
          }
        }
      }
    },
    "points.changed": {
      "post": {
        "summary": "Any other movement — today, points carried over when a shopper's data was erased at another store of the account",
        "description": "Each event is POSTed as JSON to every enabled endpoint subscribed to its type, signed the Standard Webhooks way: `webhook-id` (the same on every retry, and the thing to de-duplicate on), `webhook-timestamp` (unix seconds of this attempt; reject one more than 5 minutes from your clock), and `webhook-signature: v1,<base64 HMAC-SHA256>` over `<webhook-id>.<webhook-timestamp>.<body>`, keyed by the base64 after `whsec_` in the endpoint's secret, decoded to bytes. Verify the raw body, before parsing it. Answer 2xx within 15 seconds; anything else, a redirect, or no answer is a failure, and the event is sent again after 5 seconds, 5 minutes, 30 minutes, then 2, 5, 10, 14, 20 and 24 hours (each ±10%) — ten attempts over about 75 hours — with the same id and the same bytes. Events can arrive out of order: `data.ledger_entry_id` and `data.created_at` order them. An endpoint that has failed 10 deliveries in a row AND been failing for at least 24 hours is disabled: nothing more is sent to it, what was waiting is abandoned, `webhook.disabled` is written to the audit log, and GET /webhooks shows `status: disabled`. Delete it and create it again once the receiver works. An endpoint receives the events recorded after it was created.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "`evt_<ledger entry id>`: the same on every attempt and at every endpoint.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Unix seconds when this attempt was sent.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64 HMAC-SHA256>` of `<webhook-id>.<webhook-timestamp>.<body>`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "examples": {
                "example": {
                  "value": {
                    "type": "points.changed",
                    "timestamp": "2026-09-16T10:00:05.000Z",
                    "data": {
                      "ledger_entry_id": 8,
                      "store_key": "example-store",
                      "member_key": "mx_3f6c2a9e-8b41-4d7a-9e05-c1b7d24f6a18",
                      "delta_points": -20,
                      "rule_id": "erasure_carry",
                      "source_type": "erasure_carry",
                      "created_at": "2026-09-16T10:00:00Z"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else, a redirect, or no answer within 15 seconds is retried."
          }
        }
      }
    },
    "programme.updated": {
      "post": {
        "summary": "The programme changed",
        "description": "Each event is POSTed as JSON to every enabled endpoint subscribed to its type, signed the Standard Webhooks way: `webhook-id` (the same on every retry, and the thing to de-duplicate on), `webhook-timestamp` (unix seconds of this attempt; reject one more than 5 minutes from your clock), and `webhook-signature: v1,<base64 HMAC-SHA256>` over `<webhook-id>.<webhook-timestamp>.<body>`, keyed by the base64 after `whsec_` in the endpoint's secret, decoded to bytes. Verify the raw body, before parsing it. Answer 2xx within 15 seconds; anything else, a redirect, or no answer is a failure, and the event is sent again after 5 seconds, 5 minutes, 30 minutes, then 2, 5, 10, 14, 20 and 24 hours (each ±10%) — ten attempts over about 75 hours — with the same id and the same bytes. Events can arrive out of order: `timestamp` orders them. De-duplicate on `webhook-id`, never on `data.version`, which an expiry or look change leaves as it was. An endpoint that has failed 10 deliveries in a row AND been failing for at least 24 hours is disabled: nothing more is sent to it, what was waiting is abandoned, `webhook.disabled` is written to the audit log, and GET /webhooks shows `status: disabled`. Delete it and create it again once the receiver works. An endpoint receives the events recorded after it was created.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "`evt_prog_<audit id>`: the same on every attempt and at every endpoint.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Unix seconds when this attempt was sent.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64 HMAC-SHA256>` of `<webhook-id>.<webhook-timestamp>.<body>`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProgrammeUpdatedEvent"
              },
              "examples": {
                "example": {
                  "value": {
                    "type": "programme.updated",
                    "timestamp": "2026-09-16T10:00:05.000Z",
                    "data": {
                      "store_key": "example-store",
                      "version": "3f1c9a2b7d4e5f60718293a4b5c6d7e8",
                      "changed": [
                        "rewards"
                      ],
                      "changed_by": "api_key",
                      "audit_id": "5b2f1c7e-8d4a-4e3b-9f6a-1c2d3e4f5a6b"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else, a redirect, or no answer within 15 seconds is retried."
          }
        }
      }
    },
    "webhook.test": {
      "post": {
        "summary": "A test, sent only by POST /webhooks/{endpoint_id}/test",
        "description": "Signed like every event; sent once, never retried, never counted towards disabling.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "`evt_test_` and random hex: a new id for every test, never a ledger entry's.",
            "schema": {
              "type": "string",
              "pattern": "^evt_test_[0-9a-f]+$"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Unix seconds when this attempt was sent.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "`v1,<base64 HMAC-SHA256>` of `<webhook-id>.<webhook-timestamp>.<body>`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookTestEvent"
              },
              "examples": {
                "example": {
                  "value": {
                    "type": "webhook.test",
                    "timestamp": "2026-09-16T10:00:00.000Z",
                    "data": {
                      "endpoint_id": "we_Q3mN8xR2vT6pL9wK4sJ7hF1d"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. A test is sent once and is not retried: any other answer, a redirect, or no answer within 15 seconds is reported by `POST /webhooks/{endpoint_id}/test` as not delivered, and never counts towards disabling the endpoint."
          }
        }
      }
    }
  }
}
