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"
}
}
}
}
}
}