{
  "openapi": "3.1.0",
  "info": {
    "title": "Zestly API",
    "version": "v1",
    "description": "The REST API of a shop's Zestly loyalty program: members and balances, adjustments, custom actions, rewards and redemptions, referrals, VIP tiers, memberships, analytics and the change log. Keys are created by the merchant in Zestly › Sviluppatori, with scopes. JSON in UTF-8; points are integers, money is in minor units with its currency, instants are ISO 8601 UTC; a customer is its Shopify id. The outgoing webhooks (signed POSTs to the merchant's endpoints) are under `webhooks`. The guide: https://zestly.wavo.it/en/docs/api/reference/."
  },
  "servers": [
    {
      "url": "https://app.zestly.wavo.it/api/v1",
      "description": "The Zestly app's host."
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Members"
    },
    {
      "name": "Rewards"
    },
    {
      "name": "Referrals"
    },
    {
      "name": "Program"
    },
    {
      "name": "Analytics"
    },
    {
      "name": "Changes"
    },
    {
      "name": "Webhooks"
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Authorization: Bearer zst_live_… — a key of one shop, with its scopes (members:read, members:write, points:write, redemptions:write, program:read, referrals:read, vip:write, memberships:write, analytics:read, changes:read, changes:undo, program:write). Keep it on a server: never in a browser or an app."
      }
    },
    "schemas": {
      "Member": {
        "type": "object",
        "properties": {
          "customerId": {
            "type": "string",
            "description": "The Shopify customer id (numeric)."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "excluded",
              "merged"
            ],
            "description": "`excluded`: earns nothing and cannot redeem, keeps its balance. `merged`: its balance moved to another member."
          },
          "balance": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Points available now."
          },
          "pending": {
            "type": "object",
            "properties": {
              "points": {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991,
                "description": "Points earned but not available yet (an order not yet paid off or shipped, the program's waiting days)."
              },
              "nextReleaseAt": {
                "anyOf": [
                  {
                    "type": "string",
                    "format": "date-time",
                    "description": "ISO 8601, UTC."
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "When the next pending points become available; null when unknown (until the order ships) or none."
              }
            },
            "required": [
              "points",
              "nextReleaseAt"
            ],
            "additionalProperties": false
          },
          "lifetimeEarned": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Every point ever earned (never lowered by redemptions)."
          },
          "joinedAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601, UTC."
          },
          "lastActivityAt": {
            "type": "string",
            "format": "date-time",
            "description": "Last earn or redemption: the inactivity expiry counts from it."
          },
          "tier": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The VIP tier's id (GET /tiers)."
                  },
                  "since": {
                    "anyOf": [
                      {
                        "type": "string",
                        "format": "date-time",
                        "description": "ISO 8601, UTC."
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "graceUntil": {
                    "anyOf": [
                      {
                        "type": "string",
                        "format": "date-time",
                        "description": "ISO 8601, UTC."
                      },
                      {
                        "type": "null"
                      }
                    ],
                    "description": "Kept until then although the criteria are no longer met."
                  },
                  "manual": {
                    "type": "boolean",
                    "description": "Set by the merchant («Cambia livello») rather than by the criteria."
                  },
                  "manualUntil": {
                    "anyOf": [
                      {
                        "type": "string",
                        "format": "date-time",
                        "description": "ISO 8601, UTC."
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                },
                "required": [
                  "id",
                  "since",
                  "graceUntil",
                  "manual",
                  "manualUntil"
                ],
                "additionalProperties": false
              },
              {
                "type": "null"
              }
            ],
            "description": "The VIP tier applied now; null without one (or while VIP is off)."
          },
          "referralCode": {
            "description": "The member's live referral code, if it has one.",
            "type": [
              "string",
              "null"
            ]
          },
          "memberships": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "planId": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "active",
                    "grace",
                    "lapsed",
                    "revoked"
                  ]
                },
                "paidThrough": {
                  "anyOf": [
                    {
                      "type": "string",
                      "format": "date-time",
                      "description": "ISO 8601, UTC."
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "graceUntil": {
                  "anyOf": [
                    {
                      "type": "string",
                      "format": "date-time",
                      "description": "ISO 8601, UTC."
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              },
              "required": [
                "planId",
                "status",
                "paidThrough",
                "graceUntil"
              ],
              "additionalProperties": false
            },
            "description": "The member's paid memberships, one per plan."
          }
        },
        "required": [
          "customerId",
          "status",
          "balance",
          "pending",
          "lifetimeEarned",
          "joinedAt",
          "lastActivityAt",
          "tier",
          "referralCode",
          "memberships"
        ],
        "additionalProperties": false,
        "description": "A member: balance, pending points, tier, referral code and memberships. Ids and numbers only: no name, email or phone.",
        "examples": [
          {
            "customerId": "7700112233",
            "status": "active",
            "balance": 620,
            "pending": {
              "points": 40,
              "nextReleaseAt": "2026-10-20T00:00:00.000Z"
            },
            "lifetimeEarned": 920,
            "joinedAt": "2026-01-02T03:04:05.000Z",
            "lastActivityAt": "2026-10-16T09:00:00.000Z",
            "tier": {
              "id": "cm2tier0oro",
              "since": "2026-10-01T00:00:00.000Z",
              "graceUntil": null,
              "manual": false,
              "manualUntil": null
            },
            "referralCode": "ANNA42",
            "memberships": [
              {
                "planId": "cm2plan0club",
                "status": "active",
                "paidThrough": "2026-11-01T00:00:00.000Z",
                "graceUntil": null
              }
            ]
          }
        ]
      },
      "Redemption": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "rewardId": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "enum": [
              "points",
              "rule",
              "vip",
              "referral",
              "membership",
              "import"
            ],
            "description": "points: redeemed by the member; the others were given (a rule, a VIP tier's entry gift, a referral, a membership's monthly benefit); import: a code the shop's previous loyalty app gave, recognized by Zestly as it is (0 points)."
          },
          "type": {
            "type": "string",
            "description": "The reward's type (GET /rewards)."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "issued",
              "used",
              "cancelling",
              "cancelled",
              "expired",
              "failed"
            ]
          },
          "points": {
            "type": "integer",
            "minimum": -9007199254740991,
            "maximum": 9007199254740991,
            "description": "Points (whole numbers)."
          },
          "valueMinor": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              {
                "type": "null"
              }
            ],
            "description": "The value in minor units, when fixed (null for a percentage or free shipping)."
          },
          "currency": {
            "type": "string"
          },
          "code": {
            "description": "The discount code, for a code reward.",
            "type": [
              "string",
              "null"
            ]
          },
          "giftCardLast4": {
            "description": "A gift card's last 4 characters (its code is never listed).",
            "type": [
              "string",
              "null"
            ]
          },
          "origin": {
            "type": "string",
            "description": "Where it was redeemed: widget, cart, hub, checkout, pos, admin, api, system; import for a code of the previous app."
          },
          "expiresAt": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time",
                "description": "ISO 8601, UTC."
              },
              {
                "type": "null"
              }
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601, UTC."
          }
        },
        "required": [
          "id",
          "rewardId",
          "source",
          "type",
          "status",
          "points",
          "valueMinor",
          "currency",
          "code",
          "giftCardLast4",
          "origin",
          "expiresAt",
          "createdAt"
        ],
        "additionalProperties": false,
        "examples": [
          {
            "id": "cm2red0001",
            "rewardId": "cm2rew0005",
            "source": "points",
            "type": "fixed_amount",
            "status": "issued",
            "points": 500,
            "valueMinor": 500,
            "currency": "EUR",
            "code": "ZST-7KQ2-M9XA",
            "giftCardLast4": null,
            "origin": "api",
            "expiresAt": null,
            "createdAt": "2026-10-16T09:00:00.000Z"
          }
        ]
      },
      "Activity": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601, UTC."
          },
          "kind": {
            "type": "string",
            "enum": [
              "earn",
              "release",
              "redeem",
              "reverse",
              "adjust",
              "transfer",
              "expire",
              "credit"
            ],
            "description": "earn, release (pending points became available), redeem, reverse (taken back: a refund, a cancelled reward), adjust, transfer (a transfer or a merge), expire, credit (store credit cashback)."
          },
          "points": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              {
                "type": "null"
              }
            ],
            "description": "Signed points; null for a cashback event."
          },
          "balanceAfter": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              {
                "type": "null"
              }
            ]
          },
          "amountMinor": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": -9007199254740991,
                "maximum": 9007199254740991
              },
              {
                "type": "null"
              }
            ],
            "description": "A cashback event's amount, in minor units."
          },
          "currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "explanation": {
            "type": "object",
            "properties": {
              "key": {
                "type": "string"
              },
              "params": {
                "type": "object",
                "propertyNames": {
                  "type": "string"
                },
                "additionalProperties": {}
              }
            },
            "required": [
              "key",
              "params"
            ],
            "additionalProperties": false,
            "description": "The explanation as data: stable keys, for your own words."
          },
          "text": {
            "type": "string",
            "description": "The explanation in the language asked (`?locale=it|en`, else the shop's)."
          }
        },
        "required": [
          "id",
          "at",
          "kind",
          "points",
          "balanceAfter",
          "amountMinor",
          "currency",
          "explanation",
          "text"
        ],
        "additionalProperties": false
      },
      "Referral": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "review",
              "held",
              "rewarded",
              "rejected",
              "reversed"
            ],
            "description": "review (waiting for the merchant), held (rewarded after the return window), rewarded, rejected, reversed."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "referrerCustomerId": {
            "description": "Who invited (null once erased).",
            "type": [
              "string",
              "null"
            ]
          },
          "refereeCustomerId": {
            "description": "The invited friend (null for a guest order, or once erased).",
            "type": [
              "string",
              "null"
            ]
          },
          "orderId": {
            "description": "The friend's first order (Shopify id).",
            "type": [
              "string",
              "null"
            ]
          },
          "qualifiedAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601, UTC."
          },
          "releaseAt": {
            "type": "string",
            "format": "date-time",
            "description": "When a held referral is rewarded."
          }
        },
        "required": [
          "id",
          "status",
          "reason",
          "referrerCustomerId",
          "refereeCustomerId",
          "orderId",
          "qualifiedAt",
          "releaseAt"
        ],
        "additionalProperties": false
      },
      "Proposal": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "applying",
              "applied",
              "rejected",
              "expired",
              "stale"
            ],
            "description": "pending: waiting for the merchant (24 hours); applied: written, by its proposer as the change's author; rejected; expired; stale: what it changed moved since (propose again)."
          },
          "entity": {
            "type": "string",
            "enum": [
              "rule",
              "reward",
              "tier",
              "program"
            ]
          },
          "op": {
            "type": "string",
            "enum": [
              "create",
              "update",
              "pause",
              "resume"
            ]
          },
          "targetId": {
            "description": "The rule, reward or tier changed; null for a creation or the program.",
            "type": [
              "string",
              "null"
            ]
          },
          "what": {
            "type": "string",
            "description": "The proposal in a sentence."
          },
          "before": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The current settings, as the admin says them (empty for a creation)."
          },
          "after": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The settings once applied."
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What changes."
          },
          "diff": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every value that changes, «field › path: before → after»: what the merchant reviews before «Applica»."
          },
          "reviewUrl": {
            "type": "string",
            "description": "The proposal's page in the shop's admin: the merchant opens it and presses «Applica» (or «Rifiuta»). Nothing changes before that."
          },
          "changeId": {
            "description": "Once applied: the change-log row (undone with «Annulla» in Zestly).",
            "type": [
              "string",
              "null"
            ]
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601, UTC."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601, UTC."
          }
        },
        "required": [
          "id",
          "status",
          "entity",
          "op",
          "targetId",
          "what",
          "before",
          "after",
          "fields",
          "diff",
          "reviewUrl",
          "changeId",
          "expiresAt",
          "createdAt"
        ],
        "additionalProperties": false
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "One of the API's codes (invalid_request, unauthorized, missing_scope, api_paused, app_uninstalled, not_found, method_not_allowed, undo_not_allowed, idempotency_conflict, idempotency_in_progress, payload_too_large, rate_limited, ai_points_daily_limit, server_error) or, with 422, a service's refusal (insufficient_balance, member_inactive, unavailable:ended, already_undone…)."
              },
              "message": {
                "type": "string",
                "description": "For developers, in English. Never parse it: use `code`."
              },
              "requestId": {
                "type": "string",
                "description": "The X-Request-Id: the call's row in «Sviluppatori › Registro chiamate»."
              }
            },
            "required": [
              "code",
              "message",
              "requestId"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": false,
        "examples": [
          {
            "error": {
              "code": "insufficient_balance",
              "message": "The request was refused: insufficient_balance.",
              "requestId": "3f6c1d2e-8a4b-4c1f-9e2d-7b5a6c4d3e21"
            }
          }
        ]
      },
      "WebhookEvent": {
        "type": "object",
        "required": [
          "id",
          "type",
          "apiVersion",
          "occurredAt",
          "shop",
          "data",
          "member"
        ],
        "description": "Every webhook's body. `data` starts with the customer's Shopify id as a GID; `member` is the member when the event was distributed (snapshotAt). Ids and numbers only: never a name, an email or a phone.",
        "properties": {
          "id": {
            "type": "string",
            "description": "= the webhook-id header"
          },
          "type": {
            "type": "string",
            "enum": [
              "member.enrolled",
              "customer.redacted",
              "points.earned",
              "points.released",
              "points.adjusted",
              "points.reversed",
              "points.expired",
              "points.redeemed",
              "points.expiring",
              "reward.issued",
              "reward.cancelled",
              "reward.used",
              "cashback.issued",
              "tier.changed",
              "tier.near",
              "referral.qualified",
              "referral.rewarded",
              "referral.reversed",
              "membership.started",
              "membership.renewed",
              "membership.grace",
              "membership.lapsed",
              "membership.revoked",
              "campaign.started",
              "campaign.ended",
              "milestone.reached",
              "commission.created",
              "commission.approved",
              "commission.reversed",
              "payout.paid",
              "ping"
            ]
          },
          "apiVersion": {
            "type": "string",
            "const": "v1"
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          },
          "shop": {
            "type": "string",
            "description": "The shop's myshopify domain"
          },
          "data": {
            "type": "object",
            "properties": {
              "customerId": {
                "type": "string",
                "description": "gid://shopify/Customer/…"
              }
            },
            "additionalProperties": true
          },
          "member": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "balance": {
                "type": "integer"
              },
              "pending": {
                "type": "integer"
              },
              "tier": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "snapshotAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        },
        "examples": [
          {
            "id": "whd_5f0c1a2b3c4d5e6f7a8b9c0d",
            "type": "points.earned",
            "apiVersion": "v1",
            "occurredAt": "2026-10-16T09:12:03.000Z",
            "shop": "negozio.myshopify.com",
            "data": {
              "customerId": "gid://shopify/Customer/123",
              "points": 120,
              "pending": false,
              "availableAt": null,
              "source": {
                "type": "order",
                "id": "gid://shopify/Order/456"
              }
            },
            "member": {
              "balance": 1450,
              "pending": 0,
              "tier": "oro",
              "snapshotAt": "2026-10-16T09:12:05.000Z"
            }
          }
        ]
      }
    },
    "responses": {
      "Error": {
        "description": "Refused: 400 invalid_request (also an unreadable cursor), 401 unauthorized, 403 missing_scope | api_paused | app_uninstalled | undo_not_allowed, 404 not_found (also a path that is not canonical: a double slash, a percent-encoded letter), 405 method_not_allowed, 409 idempotency_conflict | idempotency_in_progress (with Retry-After), 413 payload_too_large, 422 a service's refusal, 429 rate_limited (with Retry-After), 500 server_error.",
        "headers": {
          "Retry-After": {
            "description": "429: the seconds until the limit's minute ends. 409 idempotency_in_progress: when to ask again.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/members/{customerId}": {
      "get": {
        "operationId": "getMember",
        "tags": [
          "Members"
        ],
        "summary": "Get a member",
        "description": "The member's balance, pending points with the next release date, lifetime points, VIP tier, live referral code and memberships.\n\nScope: `members:read` · Limit: reads: 300/min per key, 600/min per shop.",
        "x-scope": "members:read",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "description": "The Shopify customer id: numeric (`7700112233`) or a GID (`gid://shopify/Customer/7700112233`, URL-encoded).",
            "schema": {
              "type": "string",
              "pattern": "^(?:\\d{1,20}|gid:\\/\\/shopify\\/Customer\\/\\d{1,20})$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "enrollMember",
        "tags": [
          "Members"
        ],
        "summary": "Enroll a customer",
        "description": "Enrolls an existing Shopify customer (checked on Shopify: 404 when there is none) — 201 when enrolled now, 200 when already a member. No signup points: those come with a new customer's account.\n\nScope: `members:write` · Limit: writes that call Shopify: 30/min per key, 60/min per shop.",
        "x-scope": "members:write",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "description": "The Shopify customer id: numeric (`7700112233`) or a GID (`gid://shopify/Customer/7700112233`, URL-encoded).",
            "schema": {
              "type": "string",
              "pattern": "^(?:\\d{1,20}|gid:\\/\\/shopify\\/Customer\\/\\d{1,20})$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required on every write: 1–64 of A–Z a–z 0–9 _ -, one per operation (a UUID, or `<integration>-<operation>-<id>`). It belongs to the API key that sends it (and to the keys it is rotated into, except by «Ruota e revoca subito», which starts afresh): two keys never meet each other's. The same key with the same request answers the stored reply (header Idempotent-Replayed: true) and never moves points twice; with another request it is 409 idempotency_conflict; while the first request still runs, 409 idempotency_in_progress with Retry-After. Replies are kept 7 days; the points, rewards and memberships themselves are never moved twice by the same key, whenever it comes back.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/members/{customerId}/adjustments": {
      "post": {
        "operationId": "adjustPoints",
        "tags": [
          "Members"
        ],
        "summary": "Adjust a member's balance",
        "description": "Adds or removes points, as «Rettifica saldo» in the admin: the reason is shown to the customer, the note only to the staff. Never takes the balance below zero (422 insufficient_balance). `applied: false` when this Idempotency-Key was already applied.\n\nScope: `points:write` · Limit: writes: 150/min per key, 300/min per shop.",
        "x-scope": "points:write",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "description": "The Shopify customer id: numeric (`7700112233`) or a GID (`gid://shopify/Customer/7700112233`, URL-encoded).",
            "schema": {
              "type": "string",
              "pattern": "^(?:\\d{1,20}|gid:\\/\\/shopify\\/Customer\\/\\d{1,20})$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required on every write: 1–64 of A–Z a–z 0–9 _ -, one per operation (a UUID, or `<integration>-<operation>-<id>`). It belongs to the API key that sends it (and to the keys it is rotated into, except by «Ruota e revoca subito», which starts afresh): two keys never meet each other's. The same key with the same request answers the stored reply (header Idempotent-Replayed: true) and never moves points twice; with another request it is 409 idempotency_conflict; while the first request still runs, 409 idempotency_in_progress with Retry-After. Replies are kept 7 days; the points, rewards and memberships themselves are never moved twice by the same key, whenever it comes back.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "points": {
                    "type": "integer",
                    "minimum": -1000000,
                    "maximum": 1000000,
                    "description": "Points to add (positive) or remove (negative); not 0."
                  },
                  "reason": {
                    "type": "string",
                    "enum": [
                      "correction",
                      "courtesy",
                      "complaint",
                      "migration",
                      "other"
                    ],
                    "description": "Shown to the customer, in their language."
                  },
                  "note": {
                    "description": "For the staff only (the change log); never shown to the customer.",
                    "type": "string",
                    "maxLength": 200
                  },
                  "countsForTier": {
                    "description": "Positive points count for the VIP tier too. Default false.",
                    "type": "boolean"
                  },
                  "notify": {
                    "description": "Email the customer the change and the new balance (never the reason or the note), when the program's emails are on. Default false.",
                    "type": "boolean"
                  }
                },
                "required": [
                  "points",
                  "reason"
                ],
                "additionalProperties": false,
                "examples": [
                  {
                    "points": 50,
                    "reason": "courtesy",
                    "note": "Review on our blog"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "applied": {
                      "type": "boolean"
                    },
                    "member": {
                      "$ref": "#/components/schemas/Member"
                    }
                  },
                  "required": [
                    "applied",
                    "member"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/members/{customerId}/exclude": {
      "post": {
        "operationId": "excludeMember",
        "tags": [
          "Members"
        ],
        "summary": "Exclude a member",
        "description": "As «Escludi dal programma»: earns nothing and cannot redeem; the balance is kept. `changed: false` when it already was.\n\nScope: `members:write` · Limit: writes: 150/min per key, 300/min per shop.",
        "x-scope": "members:write",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "description": "The Shopify customer id: numeric (`7700112233`) or a GID (`gid://shopify/Customer/7700112233`, URL-encoded).",
            "schema": {
              "type": "string",
              "pattern": "^(?:\\d{1,20}|gid:\\/\\/shopify\\/Customer\\/\\d{1,20})$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required on every write: 1–64 of A–Z a–z 0–9 _ -, one per operation (a UUID, or `<integration>-<operation>-<id>`). It belongs to the API key that sends it (and to the keys it is rotated into, except by «Ruota e revoca subito», which starts afresh): two keys never meet each other's. The same key with the same request answers the stored reply (header Idempotent-Replayed: true) and never moves points twice; with another request it is 409 idempotency_conflict; while the first request still runs, 409 idempotency_in_progress with Retry-After. Replies are kept 7 days; the points, rewards and memberships themselves are never moved twice by the same key, whenever it comes back.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "changed": {
                      "type": "boolean"
                    },
                    "member": {
                      "$ref": "#/components/schemas/Member"
                    }
                  },
                  "required": [
                    "changed",
                    "member"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/members/{customerId}/reinstate": {
      "post": {
        "operationId": "reinstateMember",
        "tags": [
          "Members"
        ],
        "summary": "Re-enroll an excluded member",
        "description": "As «Riammetti»: earns and redeems again; the VIP tier is decided again. `changed: false` when it was not excluded.\n\nScope: `members:write` · Limit: writes: 150/min per key, 300/min per shop.",
        "x-scope": "members:write",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "description": "The Shopify customer id: numeric (`7700112233`) or a GID (`gid://shopify/Customer/7700112233`, URL-encoded).",
            "schema": {
              "type": "string",
              "pattern": "^(?:\\d{1,20}|gid:\\/\\/shopify\\/Customer\\/\\d{1,20})$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required on every write: 1–64 of A–Z a–z 0–9 _ -, one per operation (a UUID, or `<integration>-<operation>-<id>`). It belongs to the API key that sends it (and to the keys it is rotated into, except by «Ruota e revoca subito», which starts afresh): two keys never meet each other's. The same key with the same request answers the stored reply (header Idempotent-Replayed: true) and never moves points twice; with another request it is 409 idempotency_conflict; while the first request still runs, 409 idempotency_in_progress with Retry-After. Replies are kept 7 days; the points, rewards and memberships themselves are never moved twice by the same key, whenever it comes back.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "changed": {
                      "type": "boolean"
                    },
                    "member": {
                      "$ref": "#/components/schemas/Member"
                    }
                  },
                  "required": [
                    "changed",
                    "member"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/members/{customerId}/actions/{ruleId}": {
      "post": {
        "operationId": "completeAction",
        "tags": [
          "Members"
        ],
        "summary": "Complete a custom action",
        "description": "Awards an active «Azione personalizzata» rule to a member, within its limits: `granted`, `duplicate` (already given: not repeatable, or this Idempotency-Key) or `limited` (over a limit).\n\nScope: `points:write` · Limit: writes: 150/min per key, 300/min per shop.",
        "x-scope": "points:write",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "description": "The Shopify customer id: numeric (`7700112233`) or a GID (`gid://shopify/Customer/7700112233`, URL-encoded).",
            "schema": {
              "type": "string",
              "pattern": "^(?:\\d{1,20}|gid:\\/\\/shopify\\/Customer\\/\\d{1,20})$"
            }
          },
          {
            "name": "ruleId",
            "in": "path",
            "required": true,
            "description": "A custom action rule's id (GET /rules).",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required on every write: 1–64 of A–Z a–z 0–9 _ -, one per operation (a UUID, or `<integration>-<operation>-<id>`). It belongs to the API key that sends it (and to the keys it is rotated into, except by «Ruota e revoca subito», which starts afresh): two keys never meet each other's. The same key with the same request answers the stored reply (header Idempotent-Replayed: true) and never moves points twice; with another request it is 409 idempotency_conflict; while the first request still runs, 409 idempotency_in_progress with Retry-After. Replies are kept 7 days; the points, rewards and memberships themselves are never moved twice by the same key, whenever it comes back.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "granted",
                        "duplicate",
                        "limited"
                      ]
                    },
                    "points": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991,
                      "description": "Points given by this call."
                    },
                    "member": {
                      "$ref": "#/components/schemas/Member"
                    }
                  },
                  "required": [
                    "status",
                    "points",
                    "member"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/members/{customerId}/redemptions": {
      "post": {
        "operationId": "redeemReward",
        "tags": [
          "Rewards"
        ],
        "summary": "Redeem a reward for a member",
        "description": "Spends the member's points on a reward, with the same availability, tier and balance rules as the widget: a discount code, store credit or a gift card made on Shopify. A gift card's code is in this answer only (a replay of the call says null).\n\nScope: `redemptions:write` · Limit: writes that call Shopify: 30/min per key, 60/min per shop.",
        "x-scope": "redemptions:write",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "description": "The Shopify customer id: numeric (`7700112233`) or a GID (`gid://shopify/Customer/7700112233`, URL-encoded).",
            "schema": {
              "type": "string",
              "pattern": "^(?:\\d{1,20}|gid:\\/\\/shopify\\/Customer\\/\\d{1,20})$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required on every write: 1–64 of A–Z a–z 0–9 _ -, one per operation (a UUID, or `<integration>-<operation>-<id>`). It belongs to the API key that sends it (and to the keys it is rotated into, except by «Ruota e revoca subito», which starts afresh): two keys never meet each other's. The same key with the same request answers the stored reply (header Idempotent-Replayed: true) and never moves points twice; with another request it is 409 idempotency_conflict; while the first request still runs, 409 idempotency_in_progress with Retry-After. Replies are kept 7 days; the points, rewards and memberships themselves are never moved twice by the same key, whenever it comes back.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rewardId": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{1,64}$",
                    "description": "GET /rewards."
                  },
                  "points": {
                    "description": "A variable amount reward: the points to spend.",
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 9007199254740991
                  }
                },
                "required": [
                  "rewardId"
                ],
                "additionalProperties": false,
                "examples": [
                  {
                    "rewardId": "cm2rew0005"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "redemption": {
                      "$ref": "#/components/schemas/Redemption"
                    },
                    "giftCardCode": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "redemption",
                    "giftCardCode"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "get": {
        "operationId": "listMemberRedemptions",
        "tags": [
          "Rewards"
        ],
        "summary": "List a member's rewards",
        "description": "The member's rewards, newest first: redeemed with points or given.\n\nScope: `members:read` · Limit: reads: 300/min per key, 600/min per shop.",
        "x-scope": "members:read",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "description": "The Shopify customer id: numeric (`7700112233`) or a GID (`gid://shopify/Customer/7700112233`, URL-encoded).",
            "schema": {
              "type": "string",
              "pattern": "^(?:\\d{1,20}|gid:\\/\\/shopify\\/Customer\\/\\d{1,20})$"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The `next` of the previous page (an unreadable one is 400 invalid_request).",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1–100.",
            "schema": {
              "default": 50,
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Redemption"
                      }
                    },
                    "next": {
                      "description": "Pass it as `cursor` for the next page; null on the last one.",
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "next"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/redemptions/{redemptionId}/cancel": {
      "post": {
        "operationId": "cancelRedemption",
        "tags": [
          "Rewards"
        ],
        "summary": "Cancel an unused reward",
        "description": "As «Storna»: the code or credit is removed on Shopify and the points come back. A reward already used, expired or given without points cannot be cancelled (422).\n\nScope: `redemptions:write` · Limit: writes that call Shopify: 30/min per key, 60/min per shop.",
        "x-scope": "redemptions:write",
        "parameters": [
          {
            "name": "redemptionId",
            "in": "path",
            "required": true,
            "description": "A redemption's id.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required on every write: 1–64 of A–Z a–z 0–9 _ -, one per operation (a UUID, or `<integration>-<operation>-<id>`). It belongs to the API key that sends it (and to the keys it is rotated into, except by «Ruota e revoca subito», which starts afresh): two keys never meet each other's. The same key with the same request answers the stored reply (header Idempotent-Replayed: true) and never moves points twice; with another request it is 409 idempotency_conflict; while the first request still runs, 409 idempotency_in_progress with Retry-After. Replies are kept 7 days; the points, rewards and memberships themselves are never moved twice by the same key, whenever it comes back.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "redemption": {
                      "$ref": "#/components/schemas/Redemption"
                    }
                  },
                  "required": [
                    "redemption"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/members/{customerId}/tier": {
      "put": {
        "operationId": "setTier",
        "tags": [
          "Members"
        ],
        "summary": "Lock or unlock a member's VIP tier",
        "description": "As «Cambia livello»: `lock` keeps the member on a tier (or on none: `tierId: null`) until `until` or for ever; `auto` ends the lock and the tier is decided by the criteria again. `gift`: a move up pays the tier's entry gift. `changed: false` when nothing changed.\n\nScope: `vip:write` · Limit: writes: 150/min per key, 300/min per shop.",
        "x-scope": "vip:write",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "description": "The Shopify customer id: numeric (`7700112233`) or a GID (`gid://shopify/Customer/7700112233`, URL-encoded).",
            "schema": {
              "type": "string",
              "pattern": "^(?:\\d{1,20}|gid:\\/\\/shopify\\/Customer\\/\\d{1,20})$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required on every write: 1–64 of A–Z a–z 0–9 _ -, one per operation (a UUID, or `<integration>-<operation>-<id>`). It belongs to the API key that sends it (and to the keys it is rotated into, except by «Ruota e revoca subito», which starts afresh): two keys never meet each other's. The same key with the same request answers the stored reply (header Idempotent-Replayed: true) and never moves points twice; with another request it is 409 idempotency_conflict; while the first request still runs, 409 idempotency_in_progress with Retry-After. Replies are kept 7 days; the points, rewards and memberships themselves are never moved twice by the same key, whenever it comes back.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mode": {
                    "type": "string",
                    "enum": [
                      "lock",
                      "auto"
                    ]
                  },
                  "tierId": {
                    "description": "lock: the tier (GET /tiers), or null for no tier. Required with lock.",
                    "anyOf": [
                      {
                        "type": "string",
                        "pattern": "^[A-Za-z0-9_-]{1,64}$"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "until": {
                    "description": "lock: when the lock ends (in the future); none: for ever.",
                    "type": "string",
                    "maxLength": 40,
                    "format": "date-time"
                  },
                  "gift": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "mode"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "changed": {
                      "type": "boolean"
                    },
                    "member": {
                      "$ref": "#/components/schemas/Member"
                    }
                  },
                  "required": [
                    "changed",
                    "member"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/members/{customerId}/memberships": {
      "post": {
        "operationId": "grantMembership",
        "tags": [
          "Members"
        ],
        "summary": "Grant or extend a membership",
        "description": "As «Concedi o estendi»: a term of the plan for `months`, or until a day included, after the time already paid; the plan's tier at once. The note is for the staff (the change log).\n\nScope: `memberships:write` · Limit: writes that call Shopify: 30/min per key, 60/min per shop.",
        "x-scope": "memberships:write",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "description": "The Shopify customer id: numeric (`7700112233`) or a GID (`gid://shopify/Customer/7700112233`, URL-encoded).",
            "schema": {
              "type": "string",
              "pattern": "^(?:\\d{1,20}|gid:\\/\\/shopify\\/Customer\\/\\d{1,20})$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required on every write: 1–64 of A–Z a–z 0–9 _ -, one per operation (a UUID, or `<integration>-<operation>-<id>`). It belongs to the API key that sends it (and to the keys it is rotated into, except by «Ruota e revoca subito», which starts afresh): two keys never meet each other's. The same key with the same request answers the stored reply (header Idempotent-Replayed: true) and never moves points twice; with another request it is 409 idempotency_conflict; while the first request still runs, 409 idempotency_in_progress with Retry-After. Replies are kept 7 days; the points, rewards and memberships themselves are never moved twice by the same key, whenever it comes back.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "planId": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{1,64}$"
                  },
                  "months": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 36
                  },
                  "until": {
                    "type": "string",
                    "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                    "description": "A day in the shop's time zone, YYYY-MM-DD."
                  },
                  "note": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 500
                  }
                },
                "required": [
                  "planId",
                  "note"
                ],
                "additionalProperties": false,
                "examples": [
                  {
                    "planId": "cm2plan0club",
                    "months": 3,
                    "note": "Courtesy after a late delivery"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "changeId": {
                      "type": "string",
                      "description": "The change-log row (POST /changes/{id}/undo takes the term back)."
                    },
                    "member": {
                      "$ref": "#/components/schemas/Member"
                    }
                  },
                  "required": [
                    "changeId",
                    "member"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/members/{customerId}/memberships/{planId}": {
      "delete": {
        "operationId": "revokeMembership",
        "tags": [
          "Members"
        ],
        "summary": "Revoke a membership",
        "description": "As «Revoca»: every live term of the plan ends now, its tier goes; the monthly benefits not used yet are taken back, other points and codes stay.\n\nScope: `memberships:write` · Limit: writes that call Shopify: 30/min per key, 60/min per shop.",
        "x-scope": "memberships:write",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "description": "The Shopify customer id: numeric (`7700112233`) or a GID (`gid://shopify/Customer/7700112233`, URL-encoded).",
            "schema": {
              "type": "string",
              "pattern": "^(?:\\d{1,20}|gid:\\/\\/shopify\\/Customer\\/\\d{1,20})$"
            }
          },
          {
            "name": "planId",
            "in": "path",
            "required": true,
            "description": "A membership plan's id (GET /membership-plans).",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required on every write: 1–64 of A–Z a–z 0–9 _ -, one per operation (a UUID, or `<integration>-<operation>-<id>`). It belongs to the API key that sends it (and to the keys it is rotated into, except by «Ruota e revoca subito», which starts afresh): two keys never meet each other's. The same key with the same request answers the stored reply (header Idempotent-Replayed: true) and never moves points twice; with another request it is 409 idempotency_conflict; while the first request still runs, 409 idempotency_in_progress with Retry-After. Replies are kept 7 days; the points, rewards and memberships themselves are never moved twice by the same key, whenever it comes back.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "changeId": {
                      "type": "string"
                    },
                    "member": {
                      "$ref": "#/components/schemas/Member"
                    }
                  },
                  "required": [
                    "changeId",
                    "member"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/changes/{changeId}/undo": {
      "post": {
        "operationId": "undoChange",
        "tags": [
          "Changes"
        ],
        "summary": "Undo a change",
        "description": "As «Annulla» in the change log, for a member's change (an adjustment, an exclusion, a tier, a membership, a bulk job; a change to the program is 403 undo_not_allowed): through the service that made it, while it still holds (422 with the reason otherwise: already_undone, superseded, would_go_negative, window_expired, not_undoable…).\n\nScope: `changes:undo` · Limit: writes: 150/min per key, 300/min per shop.",
        "x-scope": "changes:undo",
        "parameters": [
          {
            "name": "changeId",
            "in": "path",
            "required": true,
            "description": "A change's id (GET /changes).",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required on every write: 1–64 of A–Z a–z 0–9 _ -, one per operation (a UUID, or `<integration>-<operation>-<id>`). It belongs to the API key that sends it (and to the keys it is rotated into, except by «Ruota e revoca subito», which starts afresh): two keys never meet each other's. The same key with the same request answers the stored reply (header Idempotent-Replayed: true) and never moves points twice; with another request it is 409 idempotency_conflict; while the first request still runs, 409 idempotency_in_progress with Retry-After. Replies are kept 7 days; the points, rewards and memberships themselves are never moved twice by the same key, whenever it comes back.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "undone": {
                      "type": "boolean",
                      "const": true
                    }
                  },
                  "required": [
                    "undone"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/members": {
      "get": {
        "operationId": "listMembers",
        "tags": [
          "Members"
        ],
        "summary": "List members",
        "description": "Members by join date (oldest first, stable), filtered; erased members never. `updatedSince`: a balance, tier or status changed since.\n\nScope: `members:read` · Limit: reads: 300/min per key, 600/min per shop.",
        "x-scope": "members:read",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "excluded",
                "merged"
              ]
            }
          },
          {
            "name": "tier",
            "in": "query",
            "required": false,
            "description": "A VIP tier's id, or `none`.",
            "schema": {
              "type": "string",
              "pattern": "^(none|[A-Za-z0-9_-]{1,64})$"
            }
          },
          {
            "name": "minBalance",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 1000000000
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "required": false,
            "description": "ISO 8601 instant.",
            "schema": {
              "type": "string",
              "maxLength": 40,
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The `next` of the previous page (an unreadable one is 400 invalid_request).",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1–250.",
            "schema": {
              "default": 50,
              "type": "integer",
              "minimum": 1,
              "maximum": 250
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "customerId": {
                            "type": "string",
                            "description": "The Shopify customer id (numeric)."
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "active",
                              "excluded",
                              "merged"
                            ],
                            "description": "`excluded`: earns nothing and cannot redeem, keeps its balance. `merged`: its balance moved to another member."
                          },
                          "balance": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991,
                            "description": "Points available now."
                          },
                          "lifetimeEarned": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991,
                            "description": "Every point ever earned (never lowered by redemptions)."
                          },
                          "joinedAt": {
                            "type": "string",
                            "format": "date-time",
                            "description": "ISO 8601, UTC."
                          },
                          "lastActivityAt": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Last earn or redemption: the inactivity expiry counts from it."
                          },
                          "pending": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991,
                            "description": "Points (whole numbers)."
                          },
                          "tierId": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        },
                        "required": [
                          "customerId",
                          "status",
                          "balance",
                          "lifetimeEarned",
                          "joinedAt",
                          "lastActivityAt",
                          "pending",
                          "tierId"
                        ],
                        "additionalProperties": false
                      }
                    },
                    "next": {
                      "description": "Pass it as `cursor` for the next page; null on the last one.",
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "next"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/members/{customerId}/view": {
      "get": {
        "operationId": "getMemberView",
        "tags": [
          "Members"
        ],
        "summary": "Get the customer's loyalty view (headless)",
        "description": "The same view the widget and the Loyalty Hub show the logged-in customer — balance, rewards they can redeem, ways to earn with their progress, VIP ladder, referral, memberships, the latest activity — in `?locale`, for a headless storefront's server (never call it from a browser). A customer who is not a member yet gets `member: false`. The profile says only whether a birthday is set.\n\nScope: `members:read` · Limit: reads: 300/min per key, 600/min per shop.",
        "x-scope": "members:read",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "description": "The Shopify customer id: numeric (`7700112233`) or a GID (`gid://shopify/Customer/7700112233`, URL-encoded).",
            "schema": {
              "type": "string",
              "pattern": "^(?:\\d{1,20}|gid:\\/\\/shopify\\/Customer\\/\\d{1,20})$"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The `next` of the previous page (an unreadable one is 400 invalid_request).",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "v": {
                      "type": "number",
                      "const": 1
                    },
                    "member": {
                      "type": "boolean"
                    },
                    "status": {
                      "anyOf": [
                        {
                          "type": "string",
                          "enum": [
                            "active",
                            "excluded"
                          ]
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "balance": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991,
                      "description": "Points (whole numbers)."
                    },
                    "pending": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991,
                      "description": "Points (whole numbers)."
                    },
                    "pendingUntil": {
                      "anyOf": [
                        {
                          "type": "string",
                          "format": "date-time",
                          "description": "ISO 8601, UTC."
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "lifetime": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991,
                      "description": "Points (whole numbers)."
                    },
                    "redeemedValueMinor": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991
                    },
                    "rewards": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {},
                        "additionalProperties": {}
                      }
                    },
                    "progress": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {},
                        "additionalProperties": {}
                      }
                    },
                    "done": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "profile": {
                      "type": "object",
                      "properties": {
                        "birthdaySet": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "birthdaySet"
                      ],
                      "additionalProperties": false
                    },
                    "activity": {
                      "type": "object",
                      "properties": {
                        "rows": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {},
                            "additionalProperties": {}
                          }
                        },
                        "next": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      },
                      "required": [
                        "rows",
                        "next"
                      ],
                      "additionalProperties": false
                    },
                    "vip": {
                      "anyOf": [
                        {
                          "type": "object",
                          "properties": {},
                          "additionalProperties": {}
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "referral": {
                      "anyOf": [
                        {
                          "type": "object",
                          "properties": {},
                          "additionalProperties": {}
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "memberships": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {},
                        "additionalProperties": {}
                      }
                    }
                  },
                  "required": [
                    "v",
                    "member",
                    "status",
                    "balance",
                    "pending",
                    "pendingUntil",
                    "lifetime",
                    "redeemedValueMinor",
                    "rewards",
                    "progress",
                    "done",
                    "profile",
                    "activity",
                    "vip",
                    "referral",
                    "memberships"
                  ],
                  "additionalProperties": {},
                  "description": "The widget's view (`/apps/zestly/me`), version 1."
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/members/{customerId}/activity": {
      "get": {
        "operationId": "getMemberActivity",
        "tags": [
          "Members"
        ],
        "summary": "List a member's points activity",
        "description": "The member's points movements and cashback events, newest first, each explained.\n\nScope: `members:read` · Limit: reads: 300/min per key, 600/min per shop.",
        "x-scope": "members:read",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "description": "The Shopify customer id: numeric (`7700112233`) or a GID (`gid://shopify/Customer/7700112233`, URL-encoded).",
            "schema": {
              "type": "string",
              "pattern": "^(?:\\d{1,20}|gid:\\/\\/shopify\\/Customer\\/\\d{1,20})$"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "earn",
                "release",
                "redeem",
                "reverse",
                "adjust",
                "transfer",
                "expire",
                "credit"
              ]
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The `next` of the previous page (an unreadable one is 400 invalid_request).",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1–100.",
            "schema": {
              "default": 30,
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Activity"
                      }
                    },
                    "next": {
                      "description": "Pass it as `cursor` for the next page; null on the last one.",
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "next"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/members/{customerId}/referrals": {
      "get": {
        "operationId": "getMemberReferrals",
        "tags": [
          "Referrals"
        ],
        "summary": "Get a member's referrals",
        "description": "The member's referral code, its link's openings, its referrals by status and the latest 10, and who invited the member.\n\nScope: `referrals:read` · Limit: reads: 300/min per key, 600/min per shop.",
        "x-scope": "referrals:read",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "required": true,
            "description": "The Shopify customer id: numeric (`7700112233`) or a GID (`gid://shopify/Customer/7700112233`, URL-encoded).",
            "schema": {
              "type": "string",
              "pattern": "^(?:\\d{1,20}|gid:\\/\\/shopify\\/Customer\\/\\d{1,20})$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "description": "The member's usable code (live, or being set up on Shopify).",
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "clicks": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991
                    },
                    "counts": {
                      "type": "object",
                      "propertyNames": {
                        "type": "string"
                      },
                      "additionalProperties": {
                        "type": "integer",
                        "minimum": -9007199254740991,
                        "maximum": 9007199254740991
                      },
                      "description": "Referrals by status."
                    },
                    "latest": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Referral"
                      }
                    },
                    "invitedBy": {
                      "description": "The customer id of who invited this member.",
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "code",
                    "clicks",
                    "counts",
                    "latest",
                    "invitedBy"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/referrals": {
      "get": {
        "operationId": "listReferrals",
        "tags": [
          "Referrals"
        ],
        "summary": "List referrals",
        "description": "The shop's referrals, newest first, all or one status.\n\nScope: `referrals:read` · Limit: reads: 300/min per key, 600/min per shop.",
        "x-scope": "referrals:read",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "review",
                "held",
                "rewarded",
                "rejected",
                "reversed"
              ]
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The `next` of the previous page (an unreadable one is 400 invalid_request).",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1–250.",
            "schema": {
              "default": 50,
              "type": "integer",
              "minimum": 1,
              "maximum": 250
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Referral"
                      }
                    },
                    "next": {
                      "description": "Pass it as `cursor` for the next page; null on the last one.",
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "next"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/program": {
      "get": {
        "operationId": "getProgram",
        "tags": [
          "Program"
        ],
        "summary": "Get the program",
        "description": "The program's status and settings in a few sentences (`?locale`), its points' name and which parts are on.\n\nScope: `program:read` · Limit: reads: 300/min per key, 600/min per shop.",
        "x-scope": "program:read",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "draft",
                        "active",
                        "paused"
                      ]
                    },
                    "currency": {
                      "type": "string"
                    },
                    "pointsName": {
                      "type": "object",
                      "propertyNames": {
                        "type": "string"
                      },
                      "additionalProperties": {
                        "type": "object",
                        "properties": {
                          "one": {
                            "type": "string"
                          },
                          "other": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "one",
                          "other"
                        ],
                        "additionalProperties": false
                      }
                    },
                    "summary": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "The program's settings as the admin says them."
                    },
                    "vip": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "enabled"
                      ],
                      "additionalProperties": false
                    },
                    "referral": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "enabled"
                      ],
                      "additionalProperties": false
                    },
                    "memberships": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "enabled"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "status",
                    "currency",
                    "pointsName",
                    "summary",
                    "vip",
                    "referral",
                    "memberships"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/rules": {
      "get": {
        "operationId": "listRules",
        "tags": [
          "Program"
        ],
        "summary": "List the ways to earn",
        "description": "The earning rules (archived ones never), each with its summary in `?locale` as the admin writes it.\n\nScope: `program:read` · Limit: reads that may call Shopify: 30/min per key, 60/min per shop.",
        "x-scope": "program:read",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "active",
                              "paused"
                            ]
                          },
                          "title": {
                            "type": "object",
                            "propertyNames": {
                              "type": "string"
                            },
                            "additionalProperties": {
                              "type": "string"
                            },
                            "description": "Per language (`it`, `en`)."
                          },
                          "summary": {
                            "type": "string"
                          },
                          "config": {
                            "type": "object",
                            "propertyNames": {
                              "type": "string"
                            },
                            "additionalProperties": {}
                          },
                          "limits": {
                            "type": "object",
                            "propertyNames": {
                              "type": "string"
                            },
                            "additionalProperties": {}
                          }
                        },
                        "required": [
                          "id",
                          "type",
                          "status",
                          "title",
                          "summary",
                          "config",
                          "limits"
                        ],
                        "additionalProperties": false
                      }
                    }
                  },
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/rewards": {
      "get": {
        "operationId": "listRewards",
        "tags": [
          "Program"
        ],
        "summary": "List the rewards",
        "description": "The rewards catalogue (archived ones never), each with its summary in `?locale`.\n\nScope: `program:read` · Limit: reads that may call Shopify: 30/min per key, 60/min per shop.",
        "x-scope": "program:read",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "active",
                              "paused"
                            ]
                          },
                          "pointsCost": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991,
                            "description": "0 for a variable amount (the member chooses the points)."
                          },
                          "title": {
                            "type": "object",
                            "propertyNames": {
                              "type": "string"
                            },
                            "additionalProperties": {
                              "type": "string"
                            },
                            "description": "Per language (`it`, `en`)."
                          },
                          "summary": {
                            "type": "string"
                          },
                          "featured": {
                            "type": "boolean"
                          },
                          "availability": {
                            "type": "object",
                            "propertyNames": {
                              "type": "string"
                            },
                            "additionalProperties": {},
                            "description": "vipTierIds (reserved to tiers), channel (online | pos | both), startsAt, endsAt."
                          }
                        },
                        "required": [
                          "id",
                          "type",
                          "status",
                          "pointsCost",
                          "title",
                          "summary",
                          "featured",
                          "availability"
                        ],
                        "additionalProperties": false
                      }
                    }
                  },
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/tiers": {
      "get": {
        "operationId": "listTiers",
        "tags": [
          "Program"
        ],
        "summary": "List the VIP tiers",
        "description": "The VIP tiers by rank (archived ones never), with their criteria and benefits.\n\nScope: `program:read` · Limit: reads: 300/min per key, 600/min per shop.",
        "x-scope": "program:read",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "handle": {
                            "type": "string"
                          },
                          "rank": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991
                          },
                          "name": {
                            "type": "object",
                            "propertyNames": {
                              "type": "string"
                            },
                            "additionalProperties": {
                              "type": "string"
                            },
                            "description": "Per language (`it`, `en`)."
                          },
                          "audience": {
                            "type": "string",
                            "enum": [
                              "all",
                              "b2b",
                              "membership"
                            ],
                            "description": "membership: reached only with a paid membership."
                          },
                          "criteria": {
                            "type": "object",
                            "propertyNames": {
                              "type": "string"
                            },
                            "additionalProperties": {}
                          },
                          "benefits": {
                            "type": "object",
                            "propertyNames": {
                              "type": "string"
                            },
                            "additionalProperties": {}
                          }
                        },
                        "required": [
                          "id",
                          "handle",
                          "rank",
                          "name",
                          "audience",
                          "criteria",
                          "benefits"
                        ],
                        "additionalProperties": false
                      }
                    }
                  },
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/membership-plans": {
      "get": {
        "operationId": "listMembershipPlans",
        "tags": [
          "Program"
        ],
        "summary": "List the membership plans",
        "description": "The paid membership plans (archived ones never) with their members in force.\n\nScope: `program:read` · Limit: reads: 300/min per key, 600/min per shop.",
        "x-scope": "program:read",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "handle": {
                            "type": "string"
                          },
                          "name": {
                            "type": "object",
                            "propertyNames": {
                              "type": "string"
                            },
                            "additionalProperties": {
                              "type": "string"
                            },
                            "description": "Per language (`it`, `en`)."
                          },
                          "enabled": {
                            "type": "boolean",
                            "description": "false: «In pausa», not on sale."
                          },
                          "renewal": {
                            "type": "string",
                            "enum": [
                              "fixed",
                              "auto"
                            ]
                          },
                          "termUnit": {
                            "type": "string",
                            "enum": [
                              "day",
                              "week",
                              "month",
                              "year"
                            ]
                          },
                          "termCount": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991
                          },
                          "graceDays": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991
                          },
                          "vipTierId": {
                            "type": "string"
                          },
                          "members": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991,
                            "description": "Memberships active or in their grace."
                          }
                        },
                        "required": [
                          "id",
                          "handle",
                          "name",
                          "enabled",
                          "renewal",
                          "termUnit",
                          "termCount",
                          "graceDays",
                          "vipTierId",
                          "members"
                        ],
                        "additionalProperties": false
                      }
                    }
                  },
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/analytics/summary": {
      "get": {
        "operationId": "getAnalyticsSummary",
        "tags": [
          "Analytics"
        ],
        "summary": "Get the analytics of a period",
        "description": "The sums of «Analisi» for a period of the shop's days (at most 731; default: the last 30 days). Revenue «with the program» is attribution, not increment.\n\nScope: `analytics:read` · Limit: reads: 300/min per key, 600/min per shop.",
        "x-scope": "analytics:read",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "A day in the shop's time zone, YYYY-MM-DD.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "A day in the shop's time zone, YYYY-MM-DD.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "from": {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                      "description": "A day in the shop's time zone, YYYY-MM-DD."
                    },
                    "to": {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                      "description": "A day in the shop's time zone, YYYY-MM-DD."
                    },
                    "currency": {
                      "type": "string"
                    },
                    "totals": {
                      "type": "object",
                      "propertyNames": {
                        "type": "string"
                      },
                      "additionalProperties": {
                        "type": "number"
                      },
                      "description": "The overview CSV's columns (money in minor units of `currency`): membersNew, ordersAll, netAll, ordersMember, netMember, ordersInfluenced, netInfluenced, ordersReferral, netReferral, refundedAll, costReward, costVip, costReferral, costCredit, costGiftCard, pointsEarned, pointsReversed, pointsRedeemed, pointsExpired, pointsAdjusted, rewardsIssued, rewardsUsed, the levers' orders and revenue, emailsSent, emailsBounced."
                    },
                    "otherCurrencyDays": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991,
                      "description": "Days with orders in another currency, left out."
                    },
                    "pending": {
                      "type": "boolean",
                      "description": "Some changed days are still being computed."
                    }
                  },
                  "required": [
                    "from",
                    "to",
                    "currency",
                    "totals",
                    "otherCurrencyDays",
                    "pending"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/changes": {
      "get": {
        "operationId": "listChanges",
        "tags": [
          "Changes"
        ],
        "summary": "List the change log",
        "description": "Every change to the program and the members, newest first, with its author («API · {key name}» for this API's) and a sentence in `?locale`.\n\nScope: `changes:read` · Limit: reads: 300/min per key, 600/min per shop.",
        "x-scope": "changes:read",
        "parameters": [
          {
            "name": "customerId",
            "in": "query",
            "required": false,
            "description": "Only the changes about this customer.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{1,20}$"
            }
          },
          {
            "name": "author",
            "in": "query",
            "required": false,
            "description": "merchant: everything but Zestly's own automatic changes.",
            "schema": {
              "default": "all",
              "type": "string",
              "enum": [
                "merchant",
                "system",
                "all"
              ]
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The `next` of the previous page (an unreadable one is 400 invalid_request).",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1–100.",
            "schema": {
              "default": 50,
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "ISO 8601, UTC."
                          },
                          "author": {
                            "type": "string",
                            "description": "admin:{userId}, api:{keyId}, pos:{userId} or pos:{userId}:{staffMemberId}, system…"
                          },
                          "authorLabel": {
                            "type": "string"
                          },
                          "action": {
                            "type": "string"
                          },
                          "customerId": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "text": {
                            "type": "string"
                          },
                          "undoOf": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "undoneAt": {
                            "anyOf": [
                              {
                                "type": "string",
                                "format": "date-time",
                                "description": "ISO 8601, UTC."
                              },
                              {
                                "type": "null"
                              }
                            ]
                          }
                        },
                        "required": [
                          "id",
                          "at",
                          "author",
                          "authorLabel",
                          "action",
                          "customerId",
                          "text",
                          "undoOf",
                          "undoneAt"
                        ],
                        "additionalProperties": false
                      }
                    },
                    "next": {
                      "description": "Pass it as `cursor` for the next page; null on the last one.",
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "data",
                    "next"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/proposals": {
      "post": {
        "operationId": "createProposal",
        "tags": [
          null
        ],
        "summary": "Propose a change to the program",
        "description": "A rule or reward created, changed, paused or resumed, a VIP tier changed, the program's settings changed, paused or resumed — checked now with the admin's own rules (422 invalid_proposal says what is wrong), never written by this call: the merchant reviews it in Zestly (`reviewUrl`) and applies it, with your key as the change's author, undoable from the change log. Nothing is ever deleted. At most 20 waiting (422 too_many).\n\nScope: `program:write` · Limit: writes: 150/min per key, 300/min per shop.",
        "x-scope": "program:write",
        "parameters": [
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required on every write: 1–64 of A–Z a–z 0–9 _ -, one per operation (a UUID, or `<integration>-<operation>-<id>`). It belongs to the API key that sends it (and to the keys it is rotated into, except by «Ruota e revoca subito», which starts afresh): two keys never meet each other's. The same key with the same request answers the stored reply (header Idempotent-Replayed: true) and never moves points twice; with another request it is 409 idempotency_conflict; while the first request still runs, 409 idempotency_in_progress with Retry-After. Replies are kept 7 days; the points, rewards and memberships themselves are never moved twice by the same key, whenever it comes back.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "entity": {
                    "type": "string",
                    "enum": [
                      "rule",
                      "reward",
                      "tier",
                      "program"
                    ],
                    "description": "rule (a way to earn), reward, tier (a VIP tier: update only) or program (the settings; pause and resume stop and restart earning)."
                  },
                  "op": {
                    "type": "string",
                    "enum": [
                      "create",
                      "update",
                      "pause",
                      "resume"
                    ],
                    "description": "create (rule, reward), update, pause, resume (rule, reward, program). Nothing is ever deleted."
                  },
                  "id": {
                    "description": "The rule's, reward's or tier's id (list_rules, list_rewards, list_tiers); not for create or program.",
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{1,64}$"
                  },
                  "type": {
                    "description": "create: the rule's type (as list_rules shows them: purchase, signup, newsletter, birthday, custom_action…) or the reward's type (fixed_amount, percentage, free_shipping, free_product, store_credit, gift_card, variable_amount).",
                    "type": "string",
                    "maxLength": 40
                  },
                  "input": {
                    "description": "create: the whole thing in the shape list_rules / list_rewards show (status, config, display.title.it and .en, limits; a reward's pointsCost, availability). update: only what changes — objects are merged into the current values, arrays replaced. program: config sections, only pointsName, earning, expiry, codes, eligibility.",
                    "type": "object",
                    "propertyNames": {
                      "type": "string"
                    },
                    "additionalProperties": {}
                  }
                },
                "required": [
                  "entity",
                  "op"
                ],
                "additionalProperties": false,
                "examples": [
                  {
                    "entity": "rule",
                    "op": "create",
                    "type": "newsletter",
                    "input": {
                      "status": "active",
                      "config": {
                        "reward": {
                          "kind": "points",
                          "points": 50
                        }
                      },
                      "display": {
                        "title": {
                          "it": "Iscrizione alla newsletter",
                          "en": "Newsletter sign-up"
                        }
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "proposal": {
                      "$ref": "#/components/schemas/Proposal"
                    }
                  },
                  "required": [
                    "proposal"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "proposal": {
                      "$ref": "#/components/schemas/Proposal"
                    }
                  },
                  "required": [
                    "proposal"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/proposals/{proposalId}": {
      "get": {
        "operationId": "getProposal",
        "tags": [
          null
        ],
        "summary": "Get a proposal",
        "description": "A proposal and its state: still waiting, applied (with its change-log row), rejected, expired or stale.\n\nScope: `program:write` · Limit: reads: 300/min per key, 600/min per shop.",
        "x-scope": "program:write",
        "parameters": [
          {
            "name": "proposalId",
            "in": "path",
            "required": true,
            "description": "A proposal's id (POST /proposals).",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{1,64}$"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "The language of the texts in the answer (it, en); default: the shop's.",
            "schema": {
              "type": "string",
              "enum": [
                "it",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "proposal": {
                      "$ref": "#/components/schemas/Proposal"
                    }
                  },
                  "required": [
                    "proposal"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  },
  "webhooks": {
    "member.enrolled": {
      "post": {
        "operationId": "webhook:member.enrolled",
        "tags": [
          "Webhooks"
        ],
        "summary": "member.enrolled: origin (\"import\" when an import from another app made the member; else absent)",
        "description": "Zestly POSTs this to the endpoints that take `member.enrolled` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: origin (\"import\" when an import from another app made the member; else absent). Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "customer.redacted": {
      "post": {
        "operationId": "webhook:customer.redacted",
        "tags": [
          "Webhooks"
        ],
        "summary": "customer.redacted: nothing more: forget this customer (`member` is null)",
        "description": "Zestly POSTs this to the endpoints that take `customer.redacted` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: nothing more: forget this customer (`member` is null). Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "points.earned": {
      "post": {
        "operationId": "webhook:points.earned",
        "tags": [
          "Webhooks"
        ],
        "summary": "points.earned: points, pending, availableAt, source {type, id}",
        "description": "Zestly POSTs this to the endpoints that take `points.earned` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: points, pending, availableAt, source {type, id}. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "points.released": {
      "post": {
        "operationId": "webhook:points.released",
        "tags": [
          "Webhooks"
        ],
        "summary": "points.released: points, source {type, id}",
        "description": "Zestly POSTs this to the endpoints that take `points.released` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: points, source {type, id}. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "points.adjusted": {
      "post": {
        "operationId": "webhook:points.adjusted",
        "tags": [
          "Webhooks"
        ],
        "summary": "points.adjusted: points (signed), source {type, id}",
        "description": "Zestly POSTs this to the endpoints that take `points.adjusted` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: points (signed), source {type, id}. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "points.reversed": {
      "post": {
        "operationId": "webhook:points.reversed",
        "tags": [
          "Webhooks"
        ],
        "summary": "points.reversed: points (negative), source {type, id}",
        "description": "Zestly POSTs this to the endpoints that take `points.reversed` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: points (negative), source {type, id}. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "points.expired": {
      "post": {
        "operationId": "webhook:points.expired",
        "tags": [
          "Webhooks"
        ],
        "summary": "points.expired: points (negative)",
        "description": "Zestly POSTs this to the endpoints that take `points.expired` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: points (negative). Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "points.redeemed": {
      "post": {
        "operationId": "webhook:points.redeemed",
        "tags": [
          "Webhooks"
        ],
        "summary": "points.redeemed: points (negative), redemptionId",
        "description": "Zestly POSTs this to the endpoints that take `points.redeemed` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: points (negative), redemptionId. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "points.expiring": {
      "post": {
        "operationId": "webhook:points.expiring",
        "tags": [
          "Webhooks"
        ],
        "summary": "points.expiring: expiresAt, days (30 or 7); sent only if still true when due",
        "description": "Zestly POSTs this to the endpoints that take `points.expiring` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: expiresAt, days (30 or 7); sent only if still true when due. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "reward.issued": {
      "post": {
        "operationId": "webhook:reward.issued",
        "tags": [
          "Webhooks"
        ],
        "summary": "reward.issued: redemptionId, rewardId, type, source, points, value {amountMinor, currency}, code (a discount code; a gift card's never), giftCardLast4, expiresAt",
        "description": "Zestly POSTs this to the endpoints that take `reward.issued` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: redemptionId, rewardId, type, source, points, value {amountMinor, currency}, code (a discount code; a gift card's never), giftCardLast4, expiresAt. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "reward.cancelled": {
      "post": {
        "operationId": "webhook:reward.cancelled",
        "tags": [
          "Webhooks"
        ],
        "summary": "reward.cancelled: redemptionId, rewardId, type, source, points, value {amountMinor, currency}, code (a discount code; a gift card's never), giftCardLast4, expiresAt",
        "description": "Zestly POSTs this to the endpoints that take `reward.cancelled` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: redemptionId, rewardId, type, source, points, value {amountMinor, currency}, code (a discount code; a gift card's never), giftCardLast4, expiresAt. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "reward.used": {
      "post": {
        "operationId": "webhook:reward.used",
        "tags": [
          "Webhooks"
        ],
        "summary": "reward.used: redemptionId, rewardId, type, source, points, value {amountMinor, currency}, code (a discount code; a gift card's never), giftCardLast4, expiresAt, orderId (GID)",
        "description": "Zestly POSTs this to the endpoints that take `reward.used` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: redemptionId, rewardId, type, source, points, value {amountMinor, currency}, code (a discount code; a gift card's never), giftCardLast4, expiresAt, orderId (GID). Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "cashback.issued": {
      "post": {
        "operationId": "webhook:cashback.issued",
        "tags": [
          "Webhooks"
        ],
        "summary": "cashback.issued: amount {amountMinor, currency}",
        "description": "Zestly POSTs this to the endpoints that take `cashback.issued` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: amount {amountMinor, currency}. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "tier.changed": {
      "post": {
        "operationId": "webhook:tier.changed",
        "tags": [
          "Webhooks"
        ],
        "summary": "tier.changed: from, to (handles), direction (up, down, grace, same: a change between two tiers of equal rank), cause",
        "description": "Zestly POSTs this to the endpoints that take `tier.changed` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: from, to (handles), direction (up, down, grace, same: a change between two tiers of equal rank), cause. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "tier.near": {
      "post": {
        "operationId": "webhook:tier.near",
        "tags": [
          "Webhooks"
        ],
        "summary": "tier.near: tier (handle); sent only if not reached when due",
        "description": "Zestly POSTs this to the endpoints that take `tier.near` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: tier (handle); sent only if not reached when due. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "referral.qualified": {
      "post": {
        "operationId": "webhook:referral.qualified",
        "tags": [
          "Webhooks"
        ],
        "summary": "referral.qualified: referralId, points",
        "description": "Zestly POSTs this to the endpoints that take `referral.qualified` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: referralId, points. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "referral.rewarded": {
      "post": {
        "operationId": "webhook:referral.rewarded",
        "tags": [
          "Webhooks"
        ],
        "summary": "referral.rewarded: referralId, points",
        "description": "Zestly POSTs this to the endpoints that take `referral.rewarded` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: referralId, points. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "referral.reversed": {
      "post": {
        "operationId": "webhook:referral.reversed",
        "tags": [
          "Webhooks"
        ],
        "summary": "referral.reversed: referralId, points",
        "description": "Zestly POSTs this to the endpoints that take `referral.reversed` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: referralId, points. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "membership.started": {
      "post": {
        "operationId": "webhook:membership.started",
        "tags": [
          "Webhooks"
        ],
        "summary": "membership.started: membershipId, plan (handle), paidThrough",
        "description": "Zestly POSTs this to the endpoints that take `membership.started` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: membershipId, plan (handle), paidThrough. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "membership.renewed": {
      "post": {
        "operationId": "webhook:membership.renewed",
        "tags": [
          "Webhooks"
        ],
        "summary": "membership.renewed: membershipId, plan (handle), paidThrough",
        "description": "Zestly POSTs this to the endpoints that take `membership.renewed` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: membershipId, plan (handle), paidThrough. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "membership.grace": {
      "post": {
        "operationId": "webhook:membership.grace",
        "tags": [
          "Webhooks"
        ],
        "summary": "membership.grace: membershipId, plan (handle), paidThrough; sent only if still so when due",
        "description": "Zestly POSTs this to the endpoints that take `membership.grace` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: membershipId, plan (handle), paidThrough; sent only if still so when due. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "membership.lapsed": {
      "post": {
        "operationId": "webhook:membership.lapsed",
        "tags": [
          "Webhooks"
        ],
        "summary": "membership.lapsed: membershipId, plan (handle), paidThrough; a renewing membership's only after its renewal window, and only if still lapsed",
        "description": "Zestly POSTs this to the endpoints that take `membership.lapsed` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: membershipId, plan (handle), paidThrough; a renewing membership's only after its renewal window, and only if still lapsed. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "membership.revoked": {
      "post": {
        "operationId": "webhook:membership.revoked",
        "tags": [
          "Webhooks"
        ],
        "summary": "membership.revoked: membershipId, plan (handle), paidThrough",
        "description": "Zestly POSTs this to the endpoints that take `membership.revoked` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: membershipId, plan (handle), paidThrough. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "campaign.started": {
      "post": {
        "operationId": "webhook:campaign.started",
        "tags": [
          "Webhooks"
        ],
        "summary": "campaign.started: no customerId; campaignId, name, kind (multiplier, reward_sale, member_sale), startsAt, endsAt",
        "description": "Zestly POSTs this to the endpoints that take `campaign.started` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: no customerId; campaignId, name, kind (multiplier, reward_sale, member_sale), startsAt, endsAt. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "campaign.ended": {
      "post": {
        "operationId": "webhook:campaign.ended",
        "tags": [
          "Webhooks"
        ],
        "summary": "campaign.ended: no customerId; campaignId, name, kind, startsAt, endsAt, reason (time, budget, merchant)",
        "description": "Zestly POSTs this to the endpoints that take `campaign.ended` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: no customerId; campaignId, name, kind, startsAt, endsAt, reason (time, budget, merchant). Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "milestone.reached": {
      "post": {
        "operationId": "webhook:milestone.reached",
        "tags": [
          "Webhooks"
        ],
        "summary": "milestone.reached: kind (badge, mission, streak), ruleId, points (null when it pays a reward or the badge alone)",
        "description": "Zestly POSTs this to the endpoints that take `milestone.reached` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: kind (badge, mission, streak), ruleId, points (null when it pays a reward or the badge alone). Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "commission.created": {
      "post": {
        "operationId": "webhook:commission.created",
        "tags": [
          "Webhooks"
        ],
        "summary": "commission.created: no customerId; commissionId, affiliateId, orderId (GID), amount {amountMinor, currency}, status (pending or review), reason null",
        "description": "Zestly POSTs this to the endpoints that take `commission.created` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: no customerId; commissionId, affiliateId, orderId (GID), amount {amountMinor, currency}, status (pending or review), reason null. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "commission.approved": {
      "post": {
        "operationId": "webhook:commission.approved",
        "tags": [
          "Webhooks"
        ],
        "summary": "commission.approved: no customerId; commissionId, affiliateId, orderId (GID), amount {amountMinor, currency}, status approved",
        "description": "Zestly POSTs this to the endpoints that take `commission.approved` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: no customerId; commissionId, affiliateId, orderId (GID), amount {amountMinor, currency}, status approved. Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "commission.reversed": {
      "post": {
        "operationId": "webhook:commission.reversed",
        "tags": [
          "Webhooks"
        ],
        "summary": "commission.reversed: no customerId; commissionId, affiliateId, orderId (GID), amount {amountMinor, currency} (what is left: 0), status reversed (or rejected: the merchant refused it), reason (refund, cancel, merchant)",
        "description": "Zestly POSTs this to the endpoints that take `commission.reversed` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: no customerId; commissionId, affiliateId, orderId (GID), amount {amountMinor, currency} (what is left: 0), status reversed (or rejected: the merchant refused it), reason (refund, cancel, merchant). Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "payout.paid": {
      "post": {
        "operationId": "webhook:payout.paid",
        "tags": [
          "Webhooks"
        ],
        "summary": "payout.paid: no customerId; payoutId, affiliateId, method (manual, store_credit, gift_card), amount {amountMinor, currency}, commissions (count)",
        "description": "Zestly POSTs this to the endpoints that take `payout.paid` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: no customerId; payoutId, affiliateId, method (manual, store_credit, gift_card), amount {amountMinor, currency}, commissions (count). Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    },
    "ping": {
      "post": {
        "operationId": "webhook:ping",
        "tags": [
          "Webhooks"
        ],
        "summary": "ping: nothing (no customerId; «Invia un evento di prova»)",
        "description": "Zestly POSTs this to the endpoints that take `ping` (managed in Zestly › Sviluppatori › Webhook; no API for them in v1). `data` beyond `customerId`: nothing (no customerId; «Invia un evento di prova»). Answer any 2xx within 10 seconds; anything else (a timeout, a 3xx — never followed —, a 4xx or 5xx) fails: 8 attempts: at once, then after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (about 45 hours, almost 2 days), then given up. A 410 turns the endpoint off at once; 3 days of failures turn it off too.",
        "parameters": [
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "The delivery's id (whd_…), the body's `id`: the same on every attempt. Dedupe on it (delivery is at least once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Seconds since the epoch of this attempt: refuse one more than 5 minutes off.",
            "schema": {
              "type": "string",
              "pattern": "^\\d+$"
            }
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks: `v1,` + base64(HMAC-SHA256(key, id + \".\" + timestamp + \".\" + raw body)), the key being the base64 after `whsec_`; two, space-separated, during a secret's 24-hour rotation. Compare in constant time.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received (the body is not read)."
          }
        }
      }
    }
  }
}
