OpenAPI 3.1 spec

Machine-readable description of the Zillo Developer API. Generate SDKs with openapi-generator, Fern, or pipe into Postman/Insomnia/HTTPie.

{
  "openapi": "3.1.0",
  "info": {
    "title": "Zillo Developer API",
    "version": "1.0.0",
    "description": "Programmatic access to a Zillo merchant's products, orders, customers, gift cards, tickets, bookings, vouchers, and memberships. Authenticate with a scoped API key (`zk_live_…` / `zk_test_…`) or an OAuth access token minted via the MCP install flow.",
    "contact": {
      "name": "Zillo Support",
      "url": "https://docs.zillo.app/developers"
    }
  },
  "servers": [
    {
      "url": "https://api.zillo.app/v1",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Products",
      "description": "Read and manage the items a merchant sells: gift cards, tickets, experience slots, memberships, and vouchers. Products live in one of three states (`draft`, `published`, `archived`) — only `published` products appear on the storefront. Type-specific fields (event capacity, slot inventory, recurring plan price, etc.) hang off the base record via subtype tables and are returned inline when relevant."
    },
    {
      "name": "Orders",
      "description": "Customer transactions across every product type. Orders progress through `pending` → `paid` → `fulfilled` (or `refunded` / `partially_refunded`) and carry the breakdown the customer was charged: subtotal, fees, tax, total. Each order spawns the issued items it bought (gift card, tickets, booking, membership, voucher) as separate resources — orders aren't a substitute for those."
    },
    {
      "name": "Customers",
      "description": "The people who've bought from you. A customer record is created on first purchase (deduped by email) and persists across subsequent orders, gift card redemptions, and membership renewals. `accepts_marketing` reflects the checkbox the customer ticked at checkout."
    },
    {
      "name": "Gift cards",
      "description": "Gift card balances issued from a `gift_card` product. Each card has an immutable `initial_cents` and a mutable `balance_cents` that ticks down via `POST /gift_cards/{id}/redeem`. Cards can be voided (e.g. fraud) or expired (per the merchant's gift-card settings); both states refuse redemptions."
    },
    {
      "name": "Tickets",
      "description": "Tickets issued from a `ticket` product (events with finite capacity). Each ticket carries a `qr_token` (the `T-XXXX-…` string that goes on the wallet pass) used by staff to scan at the door. Single-entry tickets (`max_uses=1`) flip `redeemed_at` on first scan; multi-entry tickets (`max_uses>1` — e.g. 3-day festival pass, in/out re-entry) accept up to `max_uses` scans before returning 409 `exhausted`. Every scan appends a row to the redemption history and fires the `ticket.redeemed` webhook with `was_first` set."
    },
    {
      "name": "Bookings",
      "description": "Reservations against an `experience` product. Two flavours: single-use slot bookings (capacity enforced at checkout under `SELECT … FOR UPDATE`, `experience_slot_id` set) and multi-pass walk-in passes (`max_uses>1`, `experience_slot_id` null — the buyer doesn't pick a date, they walk in and get scanned each visit). Same `qr_token` + scan-at-the-door pattern as Tickets, prefixed `B-`. Multi-pass scans accept up to `max_uses` before returning 409 `exhausted`."
    },
    {
      "name": "Vouchers",
      "description": "Non-monetary redeemables. Single-use (`max_uses=1`, e.g. \"Free haircut\") or multi-use packs (`max_uses>1`, e.g. \"10 coffees for $40\"). Ticket-shaped row, no parent event; the `qr_token` is `V-` prefixed so scan-side code can route by prefix without an extra lookup."
    },
    {
      "name": "Memberships",
      "description": "Recurring subscriptions billed through Stripe on the connected account. Statuses mirror Stripe's: `active`, `trialing`, `past_due`, `canceled`. Members carry an `M-` prefixed QR token used for door check-ins via the mobile app's POS / redeem flow."
    },
    {
      "name": "Webhooks",
      "description": "Outbound HTTPS endpoints the API will POST signed JSON event payloads to. Every delivery is signed with HMAC-SHA256 over the body using the endpoint's secret; verify via the `Zillo-Signature` header. Failed deliveries are retried with exponential backoff. See `/developers/webhooks/events` for the catalogue."
    },
    {
      "name": "Redemptions",
      "description": "Cross-resource lookups for at-the-door staff: paste a token, find out which gift card / ticket / booking / voucher / membership it belongs to. Powers the mobile redeem screen and the dashboard's Redeem console."
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "zk_<mode>_<secret>",
        "description": "Pass `Authorization: Bearer zk_live_…` or `zk_test_…`."
      },
      "oauth2": {
        "type": "oauth2",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://dashboard.zillo.app/oauth/authorize",
            "tokenUrl": "https://api.zillo.app/oauth/token",
            "scopes": {
              "products:read": "products:read",
              "products:write": "products:write",
              "orders:read": "orders:read",
              "orders:write": "orders:write",
              "customers:read": "customers:read",
              "customers:write": "customers:write",
              "gift_cards:read": "gift_cards:read",
              "gift_cards:write": "gift_cards:write",
              "gift_cards:redeem": "gift_cards:redeem",
              "tickets:read": "tickets:read",
              "tickets:write": "tickets:write",
              "tickets:redeem": "tickets:redeem",
              "bookings:read": "bookings:read",
              "bookings:write": "bookings:write",
              "bookings:redeem": "bookings:redeem",
              "vouchers:read": "vouchers:read",
              "vouchers:write": "vouchers:write",
              "vouchers:redeem": "vouchers:redeem",
              "memberships:read": "memberships:read",
              "memberships:write": "memberships:write",
              "memberships:checkin": "memberships:checkin",
              "webhooks:read": "webhooks:read",
              "webhooks:write": "webhooks:write"
            }
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid bearer token.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ProRequired": {
        "description": "API access requires Zillo Pro on the store (error `pro_required`). Stores already using the API before Pro launched are unaffected.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded. Inspect `X-RateLimit-Reset`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "error",
          "message"
        ]
      },
      "product": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "product"
            ]
          },
          "created": {
            "type": "integer",
            "description": "Unix seconds."
          },
          "updated": {
            "type": "integer"
          },
          "merchant_id": {
            "type": "string",
            "format": "uuid"
          },
          "type": {
            "type": "string",
            "enum": [
              "gift_card",
              "experience",
              "ticket",
              "membership",
              "voucher",
              "physical_product",
              "digital_product"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "published",
              "archived"
            ]
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "image_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "price_cents": {
            "type": "integer",
            "minimum": 0,
            "description": "Base / 'from' price. For physical & digital products the authoritative price is per-variant."
          },
          "currency": {
            "type": "string"
          },
          "variants": {
            "type": "array",
            "description": "Purchasable variants (physical_product / digital_product). Returned on GET /products/{id}, omitted on list endpoints.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "object": {
                  "type": "string",
                  "enum": [
                    "product_variant"
                  ]
                },
                "options": {
                  "type": "object",
                  "description": "Chosen option values, e.g. {\"Size\":\"M\"}. Empty for a no-options default variant.",
                  "additionalProperties": {
                    "type": "string"
                  }
                },
                "sku": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "price_cents": {
                  "type": "integer",
                  "minimum": 0
                },
                "stock": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "null = untracked / unlimited."
                },
                "sold_count": {
                  "type": "integer",
                  "minimum": 0
                }
              },
              "required": [
                "id",
                "object",
                "options",
                "price_cents",
                "sold_count"
              ]
            }
          }
        },
        "required": [
          "id",
          "object",
          "created",
          "merchant_id",
          "type",
          "status",
          "title",
          "slug",
          "price_cents",
          "currency"
        ]
      },
      "order": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "order"
            ]
          },
          "created": {
            "type": "integer"
          },
          "merchant_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "customer_email": {
            "type": "string",
            "format": "email"
          },
          "customer_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "subtotal_cents": {
            "type": "integer"
          },
          "total_cents": {
            "type": "integer"
          },
          "fee_cents": {
            "type": "integer"
          },
          "tax_cents": {
            "type": "integer"
          },
          "shipping_cents": {
            "type": "integer",
            "description": "Order-level shipping charge (one shipment). 0 when no physical line."
          },
          "shipping_address": {
            "type": [
              "object",
              "null"
            ],
            "description": "Buyer shipping address — present only when the order contains a physical_product line.",
            "properties": {
              "name": {
                "type": "string"
              },
              "line1": {
                "type": "string"
              },
              "line2": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "city": {
                "type": "string"
              },
              "region": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "postal_code": {
                "type": "string"
              },
              "country": {
                "type": "string"
              },
              "phone": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        },
        "required": [
          "id",
          "object",
          "created",
          "merchant_id",
          "status",
          "currency",
          "subtotal_cents",
          "total_cents"
        ]
      },
      "customer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "customer"
            ]
          },
          "created": {
            "type": "integer"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "accepts_marketing": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "object",
          "created",
          "email",
          "accepts_marketing"
        ]
      },
      "gift_card": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "gift_card"
            ]
          },
          "created": {
            "type": "integer"
          },
          "code": {
            "type": "string"
          },
          "initial_cents": {
            "type": "integer"
          },
          "balance_cents": {
            "type": "integer"
          },
          "redeemed_at": {
            "type": [
              "integer",
              "null"
            ]
          },
          "voided_at": {
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "object",
          "created",
          "code",
          "initial_cents",
          "balance_cents"
        ]
      },
      "ticket": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "ticket"
            ]
          },
          "qr_token": {
            "type": "string"
          },
          "seat_index": {
            "type": "integer",
            "minimum": 1
          },
          "max_uses": {
            "type": "integer",
            "minimum": 1,
            "description": "Scans allowed per ticket. `1` for single-entry tickets; `>1` for multi-day or multi-entry passes (e.g. 3-day festival pass, in/out re-entry). Stamped at issuance — later product edits never change already-issued tickets."
          },
          "uses_after": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of scan events recorded so far. Walk `/tickets/{id}/redemptions` for the per-scan event log."
          },
          "remaining": {
            "type": "integer",
            "minimum": 0,
            "description": "Convenience field: `max_uses - uses_after`. Reaches 0 when the ticket is fully used."
          },
          "redeemed_at": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix seconds of the *first* scan. Stays set on multi-entry tickets after subsequent scans — use `uses_after` to tell whether the ticket is fully used."
          },
          "voided_at": {
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "object",
          "qr_token",
          "max_uses",
          "uses_after",
          "remaining"
        ]
      },
      "ticket_redemption": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "ticket_redemption"
            ]
          },
          "created": {
            "type": "integer"
          },
          "merchant_id": {
            "type": "string",
            "format": "uuid"
          },
          "ticket_id": {
            "type": "string",
            "format": "uuid"
          },
          "redeemed_at": {
            "type": "integer"
          },
          "source": {
            "type": [
              "string",
              "null"
            ]
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "object",
          "created",
          "ticket_id",
          "redeemed_at"
        ]
      },
      "booking": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "booking"
            ]
          },
          "qr_token": {
            "type": "string"
          },
          "party_size": {
            "type": "integer"
          },
          "experience_slot_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Reserved slot id, or `null` for multi-pass walk-in bookings (`max_uses > 1`) where no slot was picked at purchase."
          },
          "max_uses": {
            "type": "integer",
            "minimum": 1,
            "description": "Scans allowed on this booking. `1` for traditional slot-based bookings; `>1` for multi-pass walk-in passes (e.g. 10-class yoga pack). Stamped at issuance."
          },
          "uses_after": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of scan events recorded so far. Walk `/bookings/{id}/redemptions` for the per-scan event log."
          },
          "remaining": {
            "type": "integer",
            "minimum": 0,
            "description": "Convenience field: `max_uses - uses_after`."
          },
          "redeemed_at": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix seconds of the *first* scan. Use `uses_after` to tell whether a multi-pass is fully spent."
          },
          "voided_at": {
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "object",
          "qr_token",
          "party_size",
          "max_uses",
          "uses_after",
          "remaining"
        ]
      },
      "booking_redemption": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "booking_redemption"
            ]
          },
          "created": {
            "type": "integer"
          },
          "merchant_id": {
            "type": "string",
            "format": "uuid"
          },
          "booking_id": {
            "type": "string",
            "format": "uuid"
          },
          "redeemed_at": {
            "type": "integer"
          },
          "source": {
            "type": [
              "string",
              "null"
            ]
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "object",
          "created",
          "booking_id",
          "redeemed_at"
        ]
      },
      "voucher": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "voucher"
            ]
          },
          "qr_token": {
            "type": "string"
          },
          "max_uses": {
            "type": "integer",
            "minimum": 1,
            "description": "Total redemptions allowed on this pass. `1` for single-use vouchers; `>1` for prepaid packs (e.g. a 5-class yoga pass). Stamped at issuance — later product edits never change already-sold passes."
          },
          "uses_after": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of redemptions recorded so far. Walk `/vouchers/{id}/redemptions` for the per-scan event log."
          },
          "remaining": {
            "type": "integer",
            "minimum": 0,
            "description": "Convenience field: `max_uses - uses_after`. Reaches 0 when the pass is fully spent."
          },
          "redeemed_at": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix seconds of the *first* redemption. Stays set on multi-use packs after subsequent scans — use `uses_after` to tell whether the pass is fully spent."
          },
          "voided_at": {
            "type": [
              "integer",
              "null"
            ]
          },
          "expires_at": {
            "type": [
              "integer",
              "null"
            ]
          },
          "pass_index": {
            "type": "integer",
            "minimum": 1
          }
        },
        "required": [
          "id",
          "object",
          "qr_token",
          "max_uses",
          "uses_after",
          "remaining"
        ]
      },
      "voucher_redemption": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "voucher_redemption"
            ]
          },
          "created": {
            "type": "integer"
          },
          "merchant_id": {
            "type": "string",
            "format": "uuid"
          },
          "voucher_id": {
            "type": "string",
            "format": "uuid"
          },
          "redeemed_at": {
            "type": "integer"
          },
          "source": {
            "type": [
              "string",
              "null"
            ]
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "object",
          "created",
          "voucher_id",
          "redeemed_at"
        ]
      },
      "membership_usage": {
        "type": "object",
        "description": "Per-period check-in allowance snapshot. Present when the plan sets `max_checkins_per_period` (e.g. '5 classes a month'); omitted on unlimited plans. Counts reset automatically each Stripe billing period.",
        "properties": {
          "max_per_period": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "description": "Total check-ins allowed per period. `null` on unlimited plans."
          },
          "used_this_period": {
            "type": "integer",
            "minimum": 0
          },
          "remaining_this_period": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "period_start": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix seconds — start of the current Stripe billing period."
          },
          "period_end": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix seconds — when the allowance refills."
          }
        },
        "required": [
          "max_per_period",
          "used_this_period",
          "remaining_this_period",
          "period_start",
          "period_end"
        ]
      },
      "membership": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "membership"
            ]
          },
          "qr_token": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "current_period_start": {
            "type": [
              "integer",
              "null"
            ]
          },
          "current_period_end": {
            "type": [
              "integer",
              "null"
            ]
          },
          "cancel_at_period_end": {
            "type": "boolean"
          },
          "usage": {
            "$ref": "#/components/schemas/membership_usage",
            "description": "Live per-period allowance. Present on the detail endpoint and on check-in responses + webhook payloads when the plan has a cap; omitted otherwise (and on the list endpoint, to avoid an N+1 count query — call the detail endpoint when you need it)."
          }
        },
        "required": [
          "id",
          "object",
          "qr_token",
          "status"
        ]
      },
      "membership_checkin": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "membership_checkin"
            ]
          },
          "created": {
            "type": "integer"
          },
          "merchant_id": {
            "type": "string",
            "format": "uuid"
          },
          "membership_id": {
            "type": "string",
            "format": "uuid"
          },
          "checked_in_at": {
            "type": "integer"
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": [
              "string",
              "null"
            ]
          },
          "usage": {
            "$ref": "#/components/schemas/membership_usage",
            "description": "Allowance state AFTER this check-in landed. Present when the underlying plan has a `max_checkins_per_period` cap; omitted on unlimited plans."
          }
        },
        "required": [
          "id",
          "object",
          "created",
          "membership_id",
          "checked_in_at"
        ]
      },
      "webhook_endpoint": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "webhook_endpoint"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "enabled": {
            "type": "boolean"
          },
          "signing_secret_prefix": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "object",
          "url",
          "events",
          "enabled"
        ]
      },
      "webhook_delivery": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "webhook_delivery"
            ]
          },
          "created": {
            "type": "integer"
          },
          "endpoint_id": {
            "type": "string",
            "format": "uuid"
          },
          "event_id": {
            "type": "string"
          },
          "event_type": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "in_flight",
              "succeeded",
              "failed",
              "abandoned"
            ]
          },
          "attempt_count": {
            "type": "integer"
          },
          "last_attempt_at": {
            "type": [
              "integer",
              "null"
            ]
          },
          "next_attempt_at": {
            "type": "integer"
          },
          "response_status": {
            "type": [
              "integer",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "object",
          "created",
          "endpoint_id",
          "event_id",
          "event_type",
          "status",
          "attempt_count",
          "next_attempt_at"
        ]
      }
    }
  },
  "paths": {
    "/products": {
      "get": {
        "tags": [
          "Products"
        ],
        "summary": "List products",
        "description": "Returns a paginated list of products owned by the authenticated merchant, ordered by creation time (newest first). Walk the cursor with `starting_after` / `ending_before` and a `limit` of up to 100 per page. Requires the `products:read` scope.",
        "security": [
          {
            "apiKey": [
              "products:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–100). Defaults to 25.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "Cursor for the next page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "ending_before",
            "in": "query",
            "description": "Cursor for the previous page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "gift_card",
                "experience",
                "ticket",
                "membership",
                "voucher"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "published",
                "archived"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paged list response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "list"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/product"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "object",
                    "data",
                    "has_more",
                    "url"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/products/{id}": {
      "get": {
        "tags": [
          "Products"
        ],
        "summary": "Retrieve a product",
        "description": "Retrieve a single product by id. Returns 404 if no product with that id exists in the authenticated merchant's scope — the id space is per-merchant, so an id from a different merchant will not resolve. Requires the `products:read` scope.",
        "security": [
          {
            "apiKey": [
              "products:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resource.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/product"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/orders": {
      "get": {
        "tags": [
          "Orders"
        ],
        "summary": "List orders",
        "description": "Returns a paginated list of orders owned by the authenticated merchant, ordered by creation time (newest first). Walk the cursor with `starting_after` / `ending_before` and a `limit` of up to 100 per page. Requires the `orders:read` scope.",
        "security": [
          {
            "apiKey": [
              "orders:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–100). Defaults to 25.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "Cursor for the next page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "ending_before",
            "in": "query",
            "description": "Cursor for the previous page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paged list response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "list"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/order"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "object",
                    "data",
                    "has_more",
                    "url"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/orders/{id}": {
      "get": {
        "tags": [
          "Orders"
        ],
        "summary": "Retrieve a order",
        "description": "Retrieve a single order by id. Returns 404 if no order with that id exists in the authenticated merchant's scope — the id space is per-merchant, so an id from a different merchant will not resolve. Requires the `orders:read` scope.",
        "security": [
          {
            "apiKey": [
              "orders:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resource.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/order"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/customers": {
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "List customers",
        "description": "Returns a paginated list of customers owned by the authenticated merchant, ordered by creation time (newest first). Walk the cursor with `starting_after` / `ending_before` and a `limit` of up to 100 per page. Requires the `customers:read` scope.",
        "security": [
          {
            "apiKey": [
              "customers:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–100). Defaults to 25.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "Cursor for the next page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "ending_before",
            "in": "query",
            "description": "Cursor for the previous page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paged list response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "list"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/customer"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "object",
                    "data",
                    "has_more",
                    "url"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/customers/{id}": {
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "Retrieve a customer",
        "description": "Retrieve a single customer by id. Returns 404 if no customer with that id exists in the authenticated merchant's scope — the id space is per-merchant, so an id from a different merchant will not resolve. Requires the `customers:read` scope.",
        "security": [
          {
            "apiKey": [
              "customers:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resource.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/customer"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/gift_cards": {
      "get": {
        "tags": [
          "Gift cards"
        ],
        "summary": "List gift cards",
        "description": "Returns a paginated list of gift cards owned by the authenticated merchant, ordered by creation time (newest first). Walk the cursor with `starting_after` / `ending_before` and a `limit` of up to 100 per page. Requires the `gift_cards:read` scope.",
        "security": [
          {
            "apiKey": [
              "gift_cards:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–100). Defaults to 25.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "Cursor for the next page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "ending_before",
            "in": "query",
            "description": "Cursor for the previous page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paged list response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "list"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/gift_card"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "object",
                    "data",
                    "has_more",
                    "url"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/gift_cards/{id}": {
      "get": {
        "tags": [
          "Gift cards"
        ],
        "summary": "Retrieve a gift card",
        "description": "Retrieve a single gift card by id. Returns 404 if no gift card with that id exists in the authenticated merchant's scope — the id space is per-merchant, so an id from a different merchant will not resolve. Requires the `gift_cards:read` scope.",
        "security": [
          {
            "apiKey": [
              "gift_cards:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resource.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/gift_card"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/gift_cards/{id}/redeem": {
      "post": {
        "tags": [
          "Gift cards"
        ],
        "summary": "Redeem a gift card",
        "description": "Decrement a gift card's balance by `amount_cents`. The card must be `active` (not voided, not expired) and the amount must be ≤ the current balance — otherwise the call returns 400 with a structured error. Idempotent on the (card id, amount, note) tuple within a short window: safe to retry on network blips. Records a `gift_card_transactions` row and fires `gift_card.redeemed`. Requires the `gift_cards:redeem` scope.",
        "security": [
          {
            "apiKey": [
              "gift_cards:redeem"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount_cents": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100000000
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 280
                  }
                },
                "required": [
                  "amount_cents"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated gift card + amount redeemed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "gift_card": {
                      "$ref": "#/components/schemas/gift_card"
                    },
                    "redeemed_amount_cents": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/tickets": {
      "get": {
        "tags": [
          "Tickets"
        ],
        "summary": "List tickets",
        "description": "Returns a paginated list of tickets owned by the authenticated merchant, ordered by creation time (newest first). Walk the cursor with `starting_after` / `ending_before` and a `limit` of up to 100 per page. Requires the `tickets:read` scope.",
        "security": [
          {
            "apiKey": [
              "tickets:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–100). Defaults to 25.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "Cursor for the next page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "ending_before",
            "in": "query",
            "description": "Cursor for the previous page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paged list response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "list"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ticket"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "object",
                    "data",
                    "has_more",
                    "url"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/tickets/{id}": {
      "get": {
        "tags": [
          "Tickets"
        ],
        "summary": "Retrieve a ticket",
        "description": "Retrieve a single ticket by id. Returns 404 if no ticket with that id exists in the authenticated merchant's scope — the id space is per-merchant, so an id from a different merchant will not resolve. Requires the `tickets:read` scope.",
        "security": [
          {
            "apiKey": [
              "tickets:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resource.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ticket"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/tickets/{id}/redeem": {
      "post": {
        "tags": [
          "Tickets"
        ],
        "summary": "Redeem a ticket",
        "description": "Flip the ticket's `redeemed_at` timestamp. One-shot — subsequent calls return 409 `already_redeemed_or_voided`. Fires the `ticket.redeemed` webhook event on success. Requires the `tickets:redeem` scope.",
        "security": [
          {
            "apiKey": [
              "tickets:redeem"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated ticket.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "ticket": {
                      "$ref": "#/components/schemas/ticket"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Ticket is already redeemed, voided, or (for vouchers) expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/bookings": {
      "get": {
        "tags": [
          "Bookings"
        ],
        "summary": "List bookings",
        "description": "Returns a paginated list of bookings owned by the authenticated merchant, ordered by creation time (newest first). Walk the cursor with `starting_after` / `ending_before` and a `limit` of up to 100 per page. Requires the `bookings:read` scope.",
        "security": [
          {
            "apiKey": [
              "bookings:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–100). Defaults to 25.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "Cursor for the next page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "ending_before",
            "in": "query",
            "description": "Cursor for the previous page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paged list response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "list"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/booking"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "object",
                    "data",
                    "has_more",
                    "url"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/bookings/{id}": {
      "get": {
        "tags": [
          "Bookings"
        ],
        "summary": "Retrieve a booking",
        "description": "Retrieve a single booking by id. Returns 404 if no booking with that id exists in the authenticated merchant's scope — the id space is per-merchant, so an id from a different merchant will not resolve. Requires the `bookings:read` scope.",
        "security": [
          {
            "apiKey": [
              "bookings:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resource.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/booking"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/bookings/{id}/redeem": {
      "post": {
        "tags": [
          "Bookings"
        ],
        "summary": "Redeem a booking",
        "description": "Flip the booking's `redeemed_at` timestamp. One-shot — subsequent calls return 409 `already_redeemed_or_voided`. Fires the `booking.redeemed` webhook event on success. Requires the `bookings:redeem` scope.",
        "security": [
          {
            "apiKey": [
              "bookings:redeem"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated booking.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "booking": {
                      "$ref": "#/components/schemas/booking"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Booking is already redeemed, voided, or (for vouchers) expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/vouchers": {
      "get": {
        "tags": [
          "Vouchers"
        ],
        "summary": "List vouchers",
        "description": "Returns a paginated list of vouchers owned by the authenticated merchant, ordered by creation time (newest first). Walk the cursor with `starting_after` / `ending_before` and a `limit` of up to 100 per page. Requires the `vouchers:read` scope.",
        "security": [
          {
            "apiKey": [
              "vouchers:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–100). Defaults to 25.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "Cursor for the next page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "ending_before",
            "in": "query",
            "description": "Cursor for the previous page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paged list response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "list"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/voucher"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "object",
                    "data",
                    "has_more",
                    "url"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/vouchers/{id}": {
      "get": {
        "tags": [
          "Vouchers"
        ],
        "summary": "Retrieve a voucher",
        "description": "Retrieve a single voucher by id. Returns 404 if no voucher with that id exists in the authenticated merchant's scope — the id space is per-merchant, so an id from a different merchant will not resolve. Requires the `vouchers:read` scope.",
        "security": [
          {
            "apiKey": [
              "vouchers:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resource.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/voucher"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/vouchers/{id}/redeem": {
      "post": {
        "tags": [
          "Vouchers"
        ],
        "summary": "Redeem a voucher",
        "description": "Records one redemption against the voucher. Single-use passes accept exactly one scan; multi-use packs (e.g. a 5-class yoga pass) accept up to `max_uses` scans before returning 409 `exhausted`. Atomicity is enforced by an underlying Postgres RPC that locks the parent row before counting + inserting, so two concurrent scans can't over-redeem a pack. Each successful call appends a row to the redemption history (`GET /v1/vouchers/{id}/redemptions`) and fires the `voucher.redeemed` webhook event — subscribers see every scan, not just the first. Requires the `vouchers:redeem` scope.",
        "security": [
          {
            "apiKey": [
              "vouchers:redeem"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated voucher + the new redemption event row.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "voucher": {
                      "$ref": "#/components/schemas/voucher"
                    },
                    "redemption": {
                      "$ref": "#/components/schemas/voucher_redemption"
                    },
                    "was_first": {
                      "type": "boolean",
                      "description": "True iff this scan was the first one on the voucher (single-use scans always set this to true; multi-use only on the opening scan)."
                    }
                  },
                  "required": [
                    "ok",
                    "voucher",
                    "redemption",
                    "was_first"
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Voucher cannot be redeemed. Distinct codes: `voided` (refunded), `expired` (`expires_at` has passed), `exhausted` (max_uses reached — either single-use already scanned, or multi-use pack fully spent).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "voided",
                        "expired",
                        "exhausted"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "error",
                    "message"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/vouchers/{id}/redemptions": {
      "get": {
        "tags": [
          "Vouchers"
        ],
        "summary": "List redemption events for a voucher",
        "description": "Cursor-paginated append-only history of every scan against a voucher. Single-use passes will have at most one entry; multi-use packs (e.g. a 10-coffees pass) have up to `max_uses` entries. Newest first.",
        "security": [
          {
            "apiKey": [
              "vouchers:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "$ref": "#/components/parameters/EndingBefore"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of redemption events.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "list"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/voucher_redemption"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "object",
                    "data",
                    "has_more",
                    "url"
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/memberships": {
      "get": {
        "tags": [
          "Memberships"
        ],
        "summary": "List memberships",
        "description": "Returns a paginated list of memberships owned by the authenticated merchant, ordered by creation time (newest first). Walk the cursor with `starting_after` / `ending_before` and a `limit` of up to 100 per page. Requires the `memberships:read` scope.",
        "security": [
          {
            "apiKey": [
              "memberships:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–100). Defaults to 25.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "Cursor for the next page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "ending_before",
            "in": "query",
            "description": "Cursor for the previous page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paged list response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "list"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/membership"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "object",
                    "data",
                    "has_more",
                    "url"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/memberships/{id}": {
      "get": {
        "tags": [
          "Memberships"
        ],
        "summary": "Retrieve a membership",
        "description": "Retrieve a single membership by id. Returns 404 if no membership with that id exists in the authenticated merchant's scope — the id space is per-merchant, so an id from a different merchant will not resolve. Requires the `memberships:read` scope.",
        "security": [
          {
            "apiKey": [
              "memberships:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resource.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/membership"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/memberships/{id}/checkins": {
      "post": {
        "tags": [
          "Memberships"
        ],
        "summary": "Record a membership check-in",
        "description": "Append a check-in row for the given membership (gym visit, studio drop-in, etc.). For unlimited plans every call writes a new row — use the rate-limiting your client already enforces to dedupe; the server does not. For plans with a per-period cap (e.g. '5 classes a month' via `membership_plans.max_checkins_per_period`), the cap is enforced atomically: scans beyond the allowance return 409 `exhausted`. The allowance refills automatically when Stripe advances the billing period on renewal — no client-side cron needed. Responses on capped plans carry a `usage` block so you can render '3 of 5 used · refills May 1' without a follow-up GET. Fires the `membership.checkin.created` webhook event. Requires the `memberships:checkin` scope.",
        "security": [
          {
            "apiKey": [
              "memberships:checkin"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "maxLength": 280,
                    "description": "Optional staff-facing note ('drop-in class', '+1 guest', etc.)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created check-in. Includes a `usage` block on capped plans (`max_checkins_per_period` set); omitted on unlimited plans.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/membership_checkin"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Check-in rejected. Common codes: `inactive` (membership is canceled / unpaid / incomplete), `exhausted` (per-period allowance is spent — a `usage` block is included with `period_end` telling the client when the allowance refills).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "enum": [
                        "inactive",
                        "exhausted"
                      ]
                    },
                    "message": {
                      "type": "string"
                    },
                    "usage": {
                      "$ref": "#/components/schemas/membership_usage"
                    }
                  },
                  "required": [
                    "error",
                    "message"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/redemptions/lookup": {
      "get": {
        "tags": [
          "Redemptions"
        ],
        "summary": "Resolve a redemption token across artifact types",
        "description": "Cross-artifact resolver for at-the-door staff. Takes a token (gift card `XXXX-XXXX-XXXX`, `T-…` ticket, `B-…` booking, `V-…` voucher, or `M-…` membership) and returns the matching record under an `object` envelope naming the artifact kind. The caller then POSTs to the type-specific redeem endpoint. Requires the read scope for **every** artifact type since the resolver walks all five tables — designed for trusted server-side integrations (kiosks, custom scanners) holding a broad key.",
        "security": [
          {
            "apiKey": [
              "gift_cards:read",
              "tickets:read",
              "bookings:read",
              "vouchers:read",
              "memberships:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "description": "The token to resolve. Prefix-encoded; case-insensitive.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resolved artifact. The `object` field names which schema is populated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "gift_card",
                        "ticket",
                        "booking",
                        "voucher",
                        "membership"
                      ]
                    },
                    "gift_card": {
                      "$ref": "#/components/schemas/gift_card"
                    },
                    "ticket": {
                      "$ref": "#/components/schemas/ticket"
                    },
                    "booking": {
                      "$ref": "#/components/schemas/booking"
                    },
                    "voucher": {
                      "$ref": "#/components/schemas/voucher"
                    },
                    "membership": {
                      "$ref": "#/components/schemas/membership"
                    }
                  },
                  "required": [
                    "object"
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List webhook endpoints",
        "description": "List the merchant's registered webhook endpoints with cursor pagination. Each endpoint exposes its `signing_secret_prefix` (safe to log) but never the plaintext — that's only returned at `POST /webhooks` (or on rotation via `PATCH`).",
        "security": [
          {
            "apiKey": [
              "webhooks:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–100). Defaults to 25.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "Cursor for the next page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "ending_before",
            "in": "query",
            "description": "Cursor for the previous page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paged list response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "list"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/webhook_endpoint"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "object",
                    "data",
                    "has_more",
                    "url"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Register a webhook endpoint",
        "description": "Register a new HTTPS endpoint to receive signed event payloads. The plaintext `signing_secret` is returned in the response **once** — store it somewhere safe; thereafter only `signing_secret_prefix` is readable. Pass `[\"*\"]` in `events` to subscribe to every current and future event, or a specific list (see `/v1/developers/webhooks/events` for the catalogue). Requires the `webhooks:write` scope.",
        "security": [
          {
            "apiKey": [
              "webhooks:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Public HTTPS URL. Plain HTTP is rejected."
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "description": "Event name or `*`."
                    }
                  },
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 280
                  }
                },
                "required": [
                  "url",
                  "events"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created endpoint plus the one-time plaintext `signing_secret`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/webhook_endpoint"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "signing_secret": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "signing_secret"
                      ]
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/{id}": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Retrieve a webhook endpoint",
        "description": "Retrieve a single webhook endpoint by id. Returns 404 if no webhook endpoint with that id exists in the authenticated merchant's scope — the id space is per-merchant, so an id from a different merchant will not resolve. Requires the `webhooks:read` scope.",
        "security": [
          {
            "apiKey": [
              "webhooks:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resource.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/webhook_endpoint"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Update a webhook endpoint",
        "description": "Partial update — only the fields you include are touched. Pass `rotate_signing_secret: true` to mint a new secret; the new plaintext is returned in `signing_secret` exactly once. Disable an endpoint without deleting it by sending `enabled: false`. Requires the `webhooks:write` scope.",
        "security": [
          {
            "apiKey": [
              "webhooks:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string"
                    }
                  },
                  "enabled": {
                    "type": "boolean"
                  },
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 280
                  },
                  "rotate_signing_secret": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated endpoint. Includes `signing_secret` only when rotated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/webhook_endpoint"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete a webhook endpoint",
        "description": "Permanently delete the endpoint. In-flight deliveries are abandoned; new events will not target this URL. Requires the `webhooks:write` scope.",
        "security": [
          {
            "apiKey": [
              "webhooks:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Endpoint deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  },
                  "required": [
                    "deleted",
                    "id"
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/webhooks/{id}/deliveries": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List deliveries for an endpoint",
        "description": "Recent delivery attempts (succeeded / failed / pending / in-flight / abandoned) for the given endpoint, ordered by `next_attempt_at` desc. Useful for debugging which events failed and how many retries Zillo has tried. Requires the `webhooks:read` scope.",
        "security": [
          {
            "apiKey": [
              "webhooks:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size (1–100). Defaults to 25.",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "description": "Cursor for the next page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "ending_before",
            "in": "query",
            "description": "Cursor for the previous page (resource id).",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "in_flight",
                "succeeded",
                "failed",
                "abandoned"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paged list of deliveries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "list"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/webhook_delivery"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "object",
                    "data",
                    "has_more"
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/webhooks/{id}/deliveries/{delivery_id}/resend": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Requeue a delivery for another attempt",
        "description": "Reset a delivery's `next_attempt_at` to now and bump it back into the `pending` queue. Use this to re-fire a failed delivery after fixing the receiver, or to manually replay a succeeded one. Requires the `webhooks:write` scope.",
        "security": [
          {
            "apiKey": [
              "webhooks:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "delivery_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated delivery row.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/webhook_delivery"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    }
  }
}