{
  "openapi": "3.1.0",
  "info": {
    "title": "API caisse.bzh",
    "version": "1.4.0",
    "summary": "Carte publique, commande à emporter et synchronisation signée du catalogue et du stock.",
    "description": "L'API de caisse.bzh, la caisse en ligne des restaurants, bars et commerces.\n\nTrois familles d'appels :\n\n- **Publics** (`/api/carte`, `/api/order`) : sans authentification, pour afficher la carte d'un établissement et lui transmettre une commande à emporter. Réservés aux métiers qui ont une carte publique (restaurant, bar).\n- **Billetterie et boutique publiques** (`/api/webtix/catalog`, `/api/boutique/catalog`) : sans authentification, à lire depuis votre serveur, pour montrer sur votre site les spectacles, les séances et les articles en ligne, avec un lien vers la page de réservation ou de commande de la caisse.\n- **API avec clé** (`/api/v1/…`) : catalogue, stock, ventes et commandes en ligne d'un établissement (et pour les musées : séances, fréquentation, contrôle d'accès, groupes, pass, billetterie en ligne, journal fiscal et export complet), avec une clé créée dans la console (page « Accès API »). En-tête `Authorization: Bearer cbz_live_…`.\n- **Synchronisation** (`/api/sync`) : appels signés HMAC-SHA256 entre une boutique en ligne et la caisse. La boutique envoie son catalogue et ses mouvements de stock ; la caisse renvoie ses ventes et remboursements sur le webhook de la boutique.\n\n**User-Agent.** Chaque appel porte un `User-Agent` qui nomme votre outil (`ma-boutique-sync/1.0`). Le pare-feu écarte les robots d'analyse et les scripts anonymes : les User-Agent par défaut des bibliothèques HTTP de Python (urllib, requests), Go et Java reçoivent un 403 `{\"error\":\"blocked\"}`.\n\nGuide : https://caisse.bzh/developpeurs · SDK JavaScript : https://caisse.bzh/sdk/caisse-bzh.mjs",
    "contact": {
      "name": "caisse.bzh",
      "email": "bonjour@caisse.bzh",
      "url": "https://caisse.bzh/developpeurs"
    }
  },
  "servers": [
    {
      "url": "https://caisse.bzh"
    }
  ],
  "externalDocs": {
    "description": "Guide développeurs",
    "url": "https://caisse.bzh/developpeurs"
  },
  "tags": [
    {
      "name": "Carte publique",
      "description": "Lecture de la carte et commande à emporter, sans authentification."
    },
    {
      "name": "Billetterie et boutique publiques",
      "description": "Les offres et séances de la billetterie en ligne, le catalogue de la boutique en ligne, sans authentification. La réservation, la commande et le paiement se font sur la page de la caisse. Lien de réservation direct : `/billetterie/<slug>#o=<offerId>&d=<AAAA-MM-JJ>&s=<sessionId>` (ou `/<langue>/tickets/<slug>#…`), chaque valeur étant vérifiée par la page."
    },
    {
      "name": "API avec clé",
      "description": "Les données d'un établissement, pour vos propres outils. Une clé = un établissement ; droits lecture seule, lecture et écriture, ou export complet ; 120 appels par minute et par clé. Les musées y trouvent aussi les séances, la fréquentation, le contrôle d'accès, le stock, les groupes, les pass et les commandes de la billetterie en ligne."
    },
    {
      "name": "Synchronisation",
      "description": "Catalogue et stock entre une boutique en ligne et la caisse, appels signés."
    }
  ],
  "paths": {
    "/api/carte": {
      "get": {
        "tags": [
          "Carte publique"
        ],
        "operationId": "getCarte",
        "summary": "Lire la carte d'un établissement",
        "description": "Les mêmes informations que la page publique `/carte/<slug>` : identité, horaires, catégories et articles. Les `id` d'articles servent à passer commande. Réponse mise en cache 60 secondes. Ouvert aux navigateurs de tout site (CORS).",
        "parameters": [
          {
            "name": "slug",
            "in": "query",
            "required": true,
            "description": "Identifiant public de l'établissement, celui de son adresse `/carte/<slug>`.",
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "example": "comptoir-brest"
          }
        ],
        "responses": {
          "200": {
            "description": "La carte.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Carte"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PublicError"
          },
          "404": {
            "$ref": "#/components/responses/PublicError"
          }
        },
        "security": []
      }
    },
    "/api/order": {
      "post": {
        "tags": [
          "Carte publique"
        ],
        "operationId": "placeOrder",
        "summary": "Passer une commande à emporter",
        "description": "La commande arrive dans la file « Commandes en ligne » de l'établissement, qui l'accepte puis la marque prête. Les prix et le total sont toujours recalculés par la caisse à partir de la carte : un montant envoyé est ignoré, une ligne dont l'article est inconnu est retirée. Paiement au retrait. À appeler depuis un serveur : l'appel n'est pas ouvert aux navigateurs d'autres sites (CORS). 60 commandes par tranche de 10 minutes et par établissement.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrderInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Commande enregistrée.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "order"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "order": {
                      "$ref": "#/components/schemas/PlacedOrder"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PublicError"
          },
          "404": {
            "$ref": "#/components/responses/PublicError"
          },
          "502": {
            "$ref": "#/components/responses/PublicError"
          },
          "429": {
            "$ref": "#/components/responses/PublicError"
          },
          "409": {
            "$ref": "#/components/responses/PublicError"
          },
          "422": {
            "$ref": "#/components/responses/PublicError"
          }
        },
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/api/webtix/catalog": {
      "get": {
        "tags": [
          "Billetterie et boutique publiques"
        ],
        "operationId": "getTicketCatalog",
        "summary": "Offres et séances de la billetterie en ligne",
        "description": "Les offres vendues en ligne (spectacles, expositions, visites) avec leurs tarifs et, avec `offer`, les séances des jours demandés et leur niveau de remplissage. Offres mises en cache 30 secondes, séances 5 secondes. À appeler depuis votre serveur (pas de CORS).\n\nPour envoyer le visiteur réserver une séance : `https://caisse.bzh/billetterie/<slug>#o=<offerId>&d=<day>&s=<id>`. Une valeur qui ne convient plus (séance complète, jour passé) est ignorée et la page s'ouvre normalement.",
        "parameters": [
          {
            "name": "slug",
            "in": "query",
            "required": true,
            "description": "Identifiant de l'établissement, celui de l'adresse `/billetterie/<slug>`.",
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "example": "theatre-du-kanndi"
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "fr",
                "en",
                "de",
                "it",
                "es"
              ],
              "default": "fr"
            }
          },
          {
            "name": "offer",
            "in": "query",
            "required": false,
            "description": "Avec une offre : ses séances.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Premier jour des séances (AAAA-MM-JJ).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "days",
            "in": "query",
            "required": false,
            "description": "Nombre de jours de séances.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 31
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les offres, et les séances avec `offer`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TicketCatalog"
                }
              }
            }
          },
          "404": {
            "description": "Établissement inconnu ou sans ce service.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "example": {
                  "ok": false,
                  "error": "unknown_shop"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/boutique/catalog": {
      "get": {
        "tags": [
          "Billetterie et boutique publiques"
        ],
        "operationId": "getShopCatalog",
        "summary": "Catalogue de la boutique en ligne",
        "description": "Les articles visibles dans la boutique en ligne d'un commerce, par catégorie : prix TTC en euros (le plus bas et le plus haut des déclinaisons), photo, ce qu'il en reste, adresse de la page de l'article. Mis en cache 15 secondes. À appeler depuis votre serveur (pas de CORS). Une boutique fermée répond `open: false` sans catégorie. La commande se passe sur la page de la boutique (retrait en magasin).",
        "parameters": [
          {
            "name": "slug",
            "in": "query",
            "required": true,
            "description": "Identifiant de l'établissement, celui de l'adresse `/boutique/<slug>`.",
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "example": "fournil-kerne"
          }
        ],
        "responses": {
          "200": {
            "description": "Le catalogue en ligne.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShopCatalog"
                }
              }
            }
          },
          "404": {
            "description": "Établissement inconnu ou sans ce service.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicError"
                },
                "example": {
                  "ok": false,
                  "error": "unknown_shop"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/v1/shop": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getShop",
        "summary": "L'établissement de la clé",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "L'établissement, ses fonctions et la clé employée.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "shop",
                    "features",
                    "key"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "shop": {
                      "$ref": "#/components/schemas/Shop"
                    },
                    "features": {
                      "type": "object",
                      "properties": {
                        "onlineOrders": {
                          "type": "boolean"
                        },
                        "stock": {
                          "type": "boolean"
                        },
                        "slots": {
                          "type": "boolean"
                        },
                        "accessControl": {
                          "type": "boolean"
                        },
                        "attendance": {
                          "type": "boolean"
                        },
                        "groups": {
                          "type": "boolean"
                        },
                        "passes": {
                          "type": "boolean"
                        },
                        "onlineTickets": {
                          "type": "boolean"
                        }
                      }
                    },
                    "key": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "scope": {
                          "enum": [
                            "read",
                            "write",
                            "export"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/catalogue": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getCatalogue",
        "summary": "Catégories et articles",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Le catalogue complet, déclinaisons et options comprises.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "categories",
                    "items"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "categories": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Category"
                      }
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Item"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/catalogue/items": {
      "post": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "createItem",
        "summary": "Créer un article",
        "description": "Clé en lecture et écriture.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ItemCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Article créé.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "item"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "item": {
                      "$ref": "#/components/schemas/Item"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "409": {
            "$ref": "#/components/responses/ApiError"
          },
          "422": {
            "$ref": "#/components/responses/ApiError"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/api/v1/catalogue/items/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getItem",
        "summary": "Lire un article",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "L'article.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "item"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "item": {
                      "$ref": "#/components/schemas/Item"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      },
      "patch": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "updateItem",
        "summary": "Modifier un article",
        "description": "Seuls les champs envoyés changent ; une chaîne vide efface `description`, `sku` ou `barcode`. Un article synchronisé avec une boutique en ligne (`managedBy: sync`) se modifie sur la boutique : 409 `managed_by_sync`.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ItemUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Article modifié.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "item"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "item": {
                      "$ref": "#/components/schemas/Item"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "409": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      },
      "delete": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "deleteItem",
        "summary": "Supprimer un article",
        "description": "409 `managed_by_sync` pour un article synchronisé, 409 `last_item` pour le dernier article d'une carte de restaurant.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Article supprimé.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "deleted"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "deleted": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "409": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/stock": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getStock",
        "summary": "Stock des articles synchronisés",
        "description": "Pour les commerces qui suivent un stock (404 `not_available` sinon). Le stock est tenu pour les articles reçus d'une boutique en ligne ; il bouge avec les ventes en caisse et les mouvements de la boutique.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Quantité par clé de synchronisation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "items"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StockLine"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/sales": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listSales",
        "summary": "Ventes encaissées",
        "description": "Les tickets encaissés, du plus récent au plus ancien (environ les 2000 derniers). Pour la comptabilité, le journal fiscal scellé de la console fait foi. Avec `cursor` (vide pour commencer) ou `include=refunds` : lecture page par page de l'historique illimité, du plus ancien au plus récent, 200 ventes par page, remboursements compris si demandés ; renvoyez `nextCursor` tant qu'il n'est pas nul. Sans ces deux paramètres, la réponse est exactement celle d'avant.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Premier jour, AAAA-MM-JJ."
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Dernier jour inclus."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Curseur de la page suivante (vide pour la première page)."
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "refunds"
              ]
            },
            "description": "`refunds` : ajoute les remboursements."
          }
        ],
        "responses": {
          "200": {
            "description": "Les ventes.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "required": [
                        "ok",
                        "count",
                        "hasMore",
                        "sales"
                      ],
                      "properties": {
                        "ok": {
                          "const": true
                        },
                        "count": {
                          "type": "integer"
                        },
                        "hasMore": {
                          "type": "boolean"
                        },
                        "sales": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Sale"
                          }
                        }
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "ok",
                        "sales",
                        "nextCursor"
                      ],
                      "properties": {
                        "ok": {
                          "const": true
                        },
                        "order": {
                          "const": "oldest_first"
                        },
                        "count": {
                          "type": "integer"
                        },
                        "nextCursor": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "sales": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SaleV1"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "409": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/orders": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listOrders",
        "summary": "Commandes en ligne",
        "description": "Pour les métiers à carte publique (404 `not_available` sinon).",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "enum": [
                "new",
                "accepted",
                "ready",
                "collected",
                "cancelled"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les commandes, plus récentes d'abord.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "orders"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "orders": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Order"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/orders/{id}/{action}": {
      "post": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "updateOrder",
        "summary": "Faire avancer une commande",
        "description": "`accept` envoie la commande à l'écran cuisine ; `collected` et `cancel` la retirent de l'écran. Clé en lecture et écriture.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "action",
            "in": "path",
            "required": true,
            "schema": {
              "enum": [
                "accept",
                "ready",
                "collected",
                "cancel"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La commande à jour.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "order"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "order": {
                      "$ref": "#/components/schemas/Order"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/sync": {
      "post": {
        "tags": [
          "Synchronisation"
        ],
        "operationId": "syncMessage",
        "summary": "Envoyer un message signé à la caisse",
        "description": "Point d'entrée unique de la synchronisation, du côté de la caisse. Le jeton et le secret se trouvent dans la console, page « Boutique en ligne », où l'on renseigne aussi l'adresse HTTPS de votre webhook.\n\n**Signature.** `x-wd29-signature` = HMAC-SHA256 hexadécimal, avec le secret, de la chaîne `1\\n{timestamp}\\n{nonce}\\n{corps brut}`. L'horodatage (secondes Unix) doit être à moins de 300 secondes de l'heure de la caisse. Sans signature valide, la réponse est 401 et rien n'est lu ni écrit ; un jeton inconnu reçoit la même réponse.\n\n**Idempotence.** Chaque événement porte un `id` de 32 caractères hexadécimaux ; un événement déjà reçu est accepté sans être appliqué une seconde fois.\n\n**Sens inverse.** Quand la caisse vend ou rembourse un article synchronisé, elle envoie à votre webhook un message signé de la même façon (`source: \"ps\"`, voir le schéma `TillMessage`). Répondez `{\"ok\": true}` en 2xx ; sinon la caisse réessaie plus tard, dans l'ordre.",
        "security": [
          {
            "SyncSignature": []
          }
        ],
        "parameters": [
          {
            "name": "shop",
            "in": "query",
            "required": true,
            "description": "Jeton de la boutique (24 caractères hexadécimaux).",
            "schema": {
              "type": "string",
              "pattern": "^[a-f0-9]{24}$"
            }
          },
          {
            "name": "x-wd29-timestamp",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            },
            "description": "Secondes Unix."
          },
          {
            "name": "x-wd29-nonce",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-f0-9]{32}$"
            },
            "description": "16 octets aléatoires, en hexadécimal."
          }
        ],
        "requestBody": {
          "required": true,
          "description": "4 Mo au plus.",
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SyncHealthRequest"
                  },
                  {
                    "$ref": "#/components/schemas/SyncEventsRequest"
                  },
                  {
                    "$ref": "#/components/schemas/SyncTickRequest"
                  }
                ],
                "discriminator": {
                  "propertyName": "op",
                  "mapping": {
                    "health": "#/components/schemas/SyncHealthRequest",
                    "events": "#/components/schemas/SyncEventsRequest",
                    "tick": "#/components/schemas/SyncTickRequest"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message traité.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/SyncHealth"
                    },
                    {
                      "$ref": "#/components/schemas/SyncAccepted"
                    },
                    {
                      "$ref": "#/components/schemas/SyncOk"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SyncError"
          },
          "401": {
            "$ref": "#/components/responses/SyncError"
          },
          "503": {
            "$ref": "#/components/responses/SyncError"
          }
        }
      }
    },
    "/api/v1/access/gates": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listAccessGates",
        "summary": "Portes et zones du contrôle d'accès",
        "description": "Les zones et les portes du site (musées). Une porte n'est jamais supprimée, seulement mise hors service ou archivée.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Zones et portes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "zones",
                    "gates"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "enabled": {
                      "type": "boolean"
                    },
                    "training": {
                      "type": "boolean"
                    },
                    "zones": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "index": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "gates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AccessGate"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/access/keys": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listAccessKeys",
        "summary": "Clés publiques des billets",
        "description": "Les clés publiques Ed25519 qui signent les billets de l'établissement (jamais une clé privée), pour vérifier la signature d'un billet sans réseau. Une clé « retiredAt » ne signe plus mais ses billets restent valables ; un billet signé par une clé « revokedAt » est refusé ; une clé « device » (caisse hors ligne) ne vaut que pour les billets émis avant « notAfter ». « site » est l'étiquette que portent les billets de cet établissement. Seul POST /api/v1/access/validate sait si un billet a déjà servi. Erreurs : unavailable (503).",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Les clés.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "algorithm",
                    "format",
                    "site",
                    "keys"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "algorithm": {
                      "const": "Ed25519"
                    },
                    "format": {
                      "const": "CBZ1"
                    },
                    "site": {
                      "type": "string"
                    },
                    "current": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "keys": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "kid": {
                            "type": "integer"
                          },
                          "publicKey": {
                            "type": "string",
                            "pattern": "^[0-9a-f]{64}$"
                          },
                          "scope": {
                            "enum": [
                              "tenant",
                              "device"
                            ]
                          },
                          "createdAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "notAfter": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "retiredAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "revokedAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/access/validate": {
      "post": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "validateTicket",
        "summary": "Valider un billet à une porte",
        "description": "Pour un tourniquet, une borne ou le terminal d'un autre fabricant. Clé en lecture et écriture. La réponse est celle des scanners de caisse.bzh : un billet refusé répond 200 avec verdict « refuse ». Le même eid renvoyé répond replayed: true sans compter une seconde entrée : en cas de réponse perdue, renvoyez la même requête. Limite : 120 appels par minute et par clé, soit environ 2 entrées par seconde ; une porte plus chargée prend une clé par contrôleur. Erreurs : bad_request, bad_eid, bad_n (400), read_only_key (403), unknown_gate (404), gate_inactive, access_disabled (409), busy, unavailable (503 : réessayez avec le même eid).",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccessValidateInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Le verdict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessVerdict"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "409": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/access/attendance": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getAccessAttendance",
        "summary": "Entrées validées aux portes, pour un jour",
        "description": "Les entrées validées aux portes (scans), par heure, porte, zone et tarif. Un jour clos et scellé au journal répond son recomptage scellé (« sealed », avec sa révision) ; sinon les compteurs en direct des portes. À ne pas confondre avec GET /api/v1/attendance, qui compte les billets émis. Erreurs : bad_day (400), unavailable (503).",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "day",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Jour, AAAA-MM-JJ (heure de Paris). Par défaut aujourd'hui."
          }
        ],
        "responses": {
          "200": {
            "description": "La fréquentation du jour.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "kind",
                    "day",
                    "counters"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "kind": {
                      "const": "validated_entries"
                    },
                    "day": {
                      "type": "string",
                      "format": "date"
                    },
                    "training": {
                      "type": "boolean"
                    },
                    "sealed": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "revision": {
                          "type": "integer"
                        },
                        "journalSeq": {
                          "type": "integer"
                        },
                        "at": {
                          "type": "string"
                        },
                        "revisionPending": {
                          "type": "boolean"
                        }
                      }
                    },
                    "counters": {
                      "type": "object",
                      "properties": {
                        "entries": {
                          "type": "integer"
                        },
                        "visitors": {
                          "type": "integer"
                        },
                        "reentries": {
                          "type": "integer"
                        },
                        "exits": {
                          "type": "integer"
                        },
                        "refused": {
                          "type": "integer"
                        },
                        "duplicates": {
                          "type": "integer"
                        },
                        "forced": {
                          "type": "integer"
                        },
                        "withoutTicket": {
                          "type": "integer"
                        },
                        "typed": {
                          "type": "integer"
                        },
                        "cancelled": {
                          "type": "integer"
                        }
                      }
                    },
                    "byHour": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "hour": {
                            "type": "integer"
                          },
                          "entries": {
                            "type": "integer"
                          },
                          "visitors": {
                            "type": "integer"
                          },
                          "exits": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "byGate": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "gate": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "entries": {
                            "type": "integer"
                          },
                          "visitors": {
                            "type": "integer"
                          },
                          "exits": {
                            "type": "integer"
                          },
                          "refused": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "byZone": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "zone": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "index": {
                            "type": "integer"
                          },
                          "entries": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "byTariff": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "product": {
                            "type": "string"
                          },
                          "productName": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "tariff": {
                            "type": "string"
                          },
                          "tariffName": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "entries": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "refusals": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "code": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "present": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "Aujourd'hui seulement.",
                      "properties": {
                        "now": {
                          "type": "integer"
                        },
                        "estimated": {
                          "type": "boolean"
                        },
                        "capacity": {
                          "type": "integer"
                        },
                        "level": {
                          "enum": [
                            "ok",
                            "near",
                            "full"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/access/events": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listAccessEvents",
        "summary": "Journal des passages aux portes",
        "description": "Les passages d'un jour, du plus ancien au plus récent : entrées, sorties, refus, annulations. Sans l'identifiant de l'agent : le journal complet sort par l'export des données. Pagination : renvoyez « next » dans since tant que « more » vaut true. Erreurs : bad_day, bad_since, bad_limit (400), unknown_gate (404), log_broken (409), unavailable (503).",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "day",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Jour, AAAA-MM-JJ (heure de Paris). Par défaut aujourd'hui."
          },
          {
            "name": "gate",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Une seule porte."
          },
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Curseur : la valeur « next » de la réponse précédente (porte:numéro, séparés par des virgules), ou un numéro de passage avec gate."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les passages.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "day",
                    "count",
                    "more",
                    "next",
                    "events"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "day": {
                      "type": "string",
                      "format": "date"
                    },
                    "training": {
                      "type": "boolean"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "more": {
                      "type": "boolean"
                    },
                    "next": {
                      "type": "string"
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AccessEvent"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "409": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/reports/days": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getDayReports",
        "summary": "Totaux par jour",
        "description": "Chiffre d'affaires, remboursements, moyens de paiement et TVA jour par jour, depuis l'historique illimité. 3 660 jours au plus par appel.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Premier jour."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Dernier jour inclus (aujourd'hui par défaut)."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "days"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "range": {
                      "type": "object"
                    },
                    "totals": {
                      "type": "object"
                    },
                    "refunds": {
                      "type": "object"
                    },
                    "byMethod": {
                      "type": "object"
                    },
                    "vat": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "days": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "day": {
                            "type": "string",
                            "format": "date"
                          },
                          "sales": {
                            "type": "integer"
                          },
                          "refunds": {
                            "type": "integer"
                          },
                          "revenue": {
                            "type": "number"
                          },
                          "tips": {
                            "type": "number"
                          },
                          "total": {
                            "type": "number"
                          },
                          "covers": {
                            "type": "integer"
                          },
                          "reconciled": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "409": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/fiscal/entries": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listFiscalEntries",
        "summary": "Journal fiscal, page par page",
        "description": "Les écritures scellées du journal de l'établissement après le numéro `after`, dans l'ordre, 200 au plus. Une écriture illisible répond 503 plutôt qu'une page incomplète.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Dernier numéro déjà lu (0 pour commencer)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            },
            "description": "1 à 200."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "entries",
                    "next"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "headSeq": {
                      "type": "integer"
                    },
                    "entries": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "next": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/export": {
      "post": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "startExport",
        "summary": "Export complet du compte",
        "description": "Clé « Export complet » seulement. Inscrit l'export au journal fiscal (6 par jour au plus) et donne les parties à lire avec GET /api/v1/export/{id}/parts/{part}.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "responses": {
          "201": {
            "description": "Export commencé.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "export"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "export": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "startedAt": {
                          "type": "string"
                        },
                        "watermark": {
                          "type": "object",
                          "properties": {
                            "seq": {
                              "type": "integer"
                            },
                            "hash": {
                              "type": "string"
                            }
                          }
                        },
                        "parts": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "format": {
                                "type": "string"
                              },
                              "years": {
                                "type": [
                                  "array",
                                  "null"
                                ]
                              },
                              "files": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/export/{id}/parts/{part}": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getExportPart",
        "summary": "Une page d'une partie de l'export",
        "description": "Clé « Export complet » seulement. Renvoyez `nextCursor` tant qu'il n'est pas nul.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Identifiant de l'export."
          },
          {
            "name": "part",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Identifiant de la partie."
          },
          {
            "name": "year",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Année, pour une partie découpée par année."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Curseur de la page suivante."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "text"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "text": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "409": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/activities": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listActivities",
        "summary": "Activités à séances",
        "description": "Visites guidées, ateliers, créneaux d'entrée : les activités actives du site.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "activities"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "activities": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string"
                          },
                          "durationMin": {
                            "type": "integer"
                          },
                          "capacity": {
                            "type": "integer"
                          },
                          "maxPerBooking": {
                            "type": "integer"
                          },
                          "public": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/sessions": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listSessions",
        "summary": "Séances et places restantes",
        "description": "Les séances d'une période (31 jours au plus), avec la jauge exacte : capacité, places libres, vendues, retenues.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Premier jour (aujourd'hui par défaut)."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Dernier jour inclus."
          },
          {
            "name": "activity",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Une seule activité."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "sessions"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "sessions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SessionV1"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/sessions/{id}": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getSession",
        "summary": "Une séance",
        "description": "Une séance et sa jauge.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Identifiant de la séance."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "session"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "session": {
                      "$ref": "#/components/schemas/SessionV1"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/sessions/{id}/holds": {
      "post": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "holdSeats",
        "summary": "Retenir des places",
        "description": "Clé lecture et écriture. Pose ou change une retenue ABSOLUE (« cette retenue vaut N places ») : un nouvel essai avec le même `id` est sans danger. `quantity: 0` libère. Les quotas des revendeurs s'appliquent. Plus assez de places : 409 `sold_out` avec `available`.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Identifiant de la séance."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "id",
                  "quantity"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "pattern": "^[a-zA-Z0-9_-]{4,40}$",
                    "description": "Votre identifiant de retenue."
                  },
                  "quantity": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 500
                  },
                  "ttlMinutes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 1440
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "hold"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "hold": {
                      "$ref": "#/components/schemas/HoldV1"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "409": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/sessions/{id}/holds/{holdId}/confirm": {
      "post": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "confirmHold",
        "summary": "Confirmer une retenue",
        "description": "Clé lecture et écriture, et réglage « confirmation par API » ouvert par l'établissement (sinon 403 `confirm_disabled`). Avec `lines` (ou quand la séance n'a qu'un produit et un seul tarif ouvert), la confirmation enregistre la vente et émet les billets, comme un revendeur : la réponse ajoute `sale` et `tickets`. Sans tarif possible, c'est une réservation ferme sans billet (`tickets` vide), facturée à part. Rejouer la même `reference` rend la même vente. Erreurs : `bad_lines` 400, `item_required`, `quantity_mismatch`, `reference_taken`, `unknown_product`, `tariff_closed`, `seated_session` 409, `seal_pending`, `fiscal_unavailable`, `tickets_unavailable` 503 (réessayer avec la même référence).",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Identifiant de la séance."
          },
          {
            "name": "holdId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Votre identifiant de retenue."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "reference"
                ],
                "properties": {
                  "reference": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9:_.-]{1,38}$"
                  },
                  "itemId": {
                    "type": "string",
                    "description": "Produit vendu (facultatif quand la séance n'en a qu'un)."
                  },
                  "lines": {
                    "type": "array",
                    "description": "Tarifs et quantités ; leur somme égale le nombre de places de la retenue.",
                    "items": {
                      "type": "object",
                      "required": [
                        "variantId",
                        "quantity"
                      ],
                      "properties": {
                        "variantId": {
                          "type": "string"
                        },
                        "quantity": {
                          "type": "integer",
                          "minimum": 1
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "hold"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "hold": {
                      "$ref": "#/components/schemas/HoldV1"
                    },
                    "sale": {
                      "type": "object",
                      "description": "La vente enregistrée (absente sans tarif).",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "reference": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "training": {
                          "type": "boolean"
                        },
                        "fiscalNo": {
                          "type": "integer"
                        },
                        "totalCents": {
                          "type": "integer"
                        },
                        "persons": {
                          "type": "integer"
                        },
                        "ticketsUrl": {
                          "type": "string"
                        }
                      }
                    },
                    "tickets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "number": {
                            "type": "string"
                          },
                          "code": {
                            "type": "string"
                          },
                          "tariff": {
                            "type": "string"
                          },
                          "day": {
                            "type": "string"
                          },
                          "token": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "409": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/sessions/{id}/holds/{holdId}": {
      "delete": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "releaseHold",
        "summary": "Libérer une retenue",
        "description": "Clé lecture et écriture. Une retenue confirmée demande `confirmed=1` et la confirmation par API ouverte.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Identifiant de la séance."
          },
          {
            "name": "holdId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Votre identifiant de retenue."
          },
          {
            "name": "confirmed",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "1"
              ]
            },
            "description": "`1` pour libérer des places confirmées."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "409": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/attendance": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getAttendance",
        "summary": "Fréquentation : entrées émises",
        "description": "Les entrées ÉMISES (billets vendus et comptages manuels), par jour ou par mois, avec les catégories de visiteurs, l'origine et les heures. Ce ne sont pas les entrées validées aux portes : celles-ci sont GET /api/v1/access/attendance. 800 jours au plus. Montants en euros.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Premier jour de visite (1er du mois par défaut)."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Dernier jour inclus (aujourd'hui par défaut)."
          },
          {
            "name": "group",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "day",
                "month"
              ],
              "default": "day"
            },
            "description": "Regroupement de `series`."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "kind",
                    "total",
                    "series"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "kind": {
                      "const": "admissions_issued"
                    },
                    "range": {
                      "type": "object",
                      "properties": {
                        "from": {
                          "type": "string",
                          "format": "date"
                        },
                        "to": {
                          "type": "string",
                          "format": "date"
                        },
                        "group": {
                          "enum": [
                            "day",
                            "month"
                          ]
                        }
                      }
                    },
                    "total": {
                      "type": "object",
                      "properties": {
                        "visitors": {
                          "type": "integer"
                        },
                        "paid": {
                          "type": "integer"
                        },
                        "free": {
                          "type": "integer"
                        },
                        "admissionRevenue": {
                          "type": "number"
                        }
                      }
                    },
                    "sources": {
                      "type": "object",
                      "properties": {
                        "journal": {
                          "type": "object",
                          "properties": {
                            "visitors": {
                              "type": "integer"
                            },
                            "paid": {
                              "type": "integer"
                            },
                            "free": {
                              "type": "integer"
                            }
                          }
                        },
                        "manual": {
                          "type": "object",
                          "properties": {
                            "visitors": {
                              "type": "integer"
                            },
                            "paid": {
                              "type": "integer"
                            },
                            "free": {
                              "type": "integer"
                            }
                          }
                        },
                        "imported": {
                          "oneOf": [
                            {
                              "type": "object",
                              "properties": {
                                "visitors": {
                                  "type": "integer"
                                },
                                "paid": {
                                  "type": "integer"
                                },
                                "free": {
                                  "type": "integer"
                                }
                              }
                            },
                            {
                              "type": "null"
                            }
                          ]
                        }
                      }
                    },
                    "categories": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          },
                          "audience": {
                            "type": "string"
                          },
                          "audienceLabel": {
                            "type": "string"
                          },
                          "visitors": {
                            "type": "integer"
                          },
                          "free": {
                            "type": "integer"
                          },
                          "revenue": {
                            "type": "number"
                          }
                        }
                      }
                    },
                    "origin": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "code": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          },
                          "visitors": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "hours": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "hour": {
                            "type": "integer"
                          },
                          "visitors": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "series": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "period": {
                            "type": "string"
                          },
                          "visitors": {
                            "type": "integer"
                          },
                          "paid": {
                            "type": "integer"
                          },
                          "free": {
                            "type": "integer"
                          },
                          "counted": {
                            "type": "integer"
                          },
                          "admissionRevenue": {
                            "type": "number"
                          },
                          "salesTotal": {
                            "type": "number"
                          }
                        }
                      }
                    },
                    "coverage": {
                      "type": "object",
                      "properties": {
                        "reconciledDays": {
                          "type": "integer"
                        },
                        "liveDays": {
                          "type": "integer"
                        },
                        "incompleteDays": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/attendance/year": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getAttendanceYear",
        "summary": "Fréquentation : aide à la saisie de l'enquête annuelle",
        "description": "Musées. Les rubriques de l'enquête annuelle des musées pour l'année demandée, calculées pour l'établissement de la clé (entrées émises : billets et comptages), avec l'identifiant Museofile de son site. Si le site compte plusieurs établissements, un avertissement le dit, ces chiffres sont ceux de cet établissement seulement, et openData vaut null (la ligne open data porte l'identifiant du musée entier : elle se prend dans la console, page Bilan des sites). L'historique importé est donné à part et n'est jamais compté. Chiffres déclaratifs, le musée reste l'auteur de sa réponse. Erreurs : invalid_year, year_not_started (400), not_available (404), unavailable (503).",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "year",
            "in": "query",
            "required": false,
            "description": "Année, AAAA (défaut : l'année en cours).",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rubriques de l'année.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AttendanceYear"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/stock/levels": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getStockLevels",
        "summary": "Niveaux du stock natif",
        "description": "Quantités par article et par lieu, stock natif de la caisse et articles tenus par la boutique en ligne (`source: web`). Jamais de coût d'achat ni de valeur. GET /api/v1/stock reste inchangé.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "location",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Un seul lieu (identifiant de GET /api/v1/stock/locations)."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "active",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "active": {
                      "type": "boolean"
                    },
                    "untracked": {
                      "type": "integer"
                    },
                    "stale": {
                      "type": "boolean"
                    },
                    "webUnavailable": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "unit": {
                            "type": "string"
                          },
                          "itemId": {
                            "type": "string"
                          },
                          "variantId": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "sku": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "barcode": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "name": {
                            "type": "string"
                          },
                          "categoryId": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "quantity": {
                            "type": "number"
                          },
                          "byLocation": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "number"
                            }
                          },
                          "tracked": {
                            "type": "boolean"
                          },
                          "source": {
                            "enum": [
                              "native",
                              "web"
                            ]
                          },
                          "min": {
                            "type": "number"
                          },
                          "max": {
                            "type": "number"
                          },
                          "low": {
                            "type": "boolean"
                          },
                          "negative": {
                            "type": "boolean"
                          },
                          "orphan": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/stock/locations": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listStockLocations",
        "summary": "Lieux du stock",
        "description": "Réserve, boutique, dépôt : les lieux du stock natif.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "active",
                    "locations"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "active": {
                      "type": "boolean"
                    },
                    "sellLocation": {
                      "type": "string"
                    },
                    "locations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "sells": {
                            "type": "boolean"
                          },
                          "archived": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/stock/movements": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listStockMovements",
        "summary": "Mouvements de stock",
        "description": "Le journal du stock natif dans l'ordre (ventes, entrées, ajustements, transferts, inventaires), sans coût ni nom de la personne. `void: true` : lot sans effet (référence déjà prise). Renvoyez `next` dans `after` tant qu'il n'est pas nul.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "Dernier numéro déjà lu (0 pour commencer)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            },
            "description": "1 à 200."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "movements",
                    "next"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "headSeq": {
                      "type": "integer"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "next": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "movements": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "seq": {
                            "type": "integer"
                          },
                          "id": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string"
                          },
                          "at": {
                            "type": "string"
                          },
                          "postedAt": {
                            "type": "string"
                          },
                          "terminal": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "reference": {
                            "type": "string"
                          },
                          "document": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "fiscalSeq": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "void": {
                            "type": "boolean"
                          },
                          "lines": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "unit": {
                                  "type": "string"
                                },
                                "location": {
                                  "type": "string"
                                },
                                "quantity": {
                                  "type": "integer"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "reason": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "409": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/group-bookings": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listGroupBookings",
        "summary": "Dossiers de groupes d'un mois",
        "description": "Les dossiers de groupes et de scolaires dont la visite tombe dans le mois.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "month",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}$"
            },
            "description": "AAAA-MM (mois en cours par défaut)."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "bookings"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "month": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "bookings": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/GroupBookingRow"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/group-bookings/{id}": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getGroupBooking",
        "summary": "Un dossier de groupe",
        "description": "Le dossier : organisation, effectifs, créneau, bon de commande, documents. Le contact est donné par son nom et une adresse e-mail masquée.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Identifiant du dossier."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "booking"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "booking": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/documents": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listDocuments",
        "summary": "Devis, factures et avoirs d'un mois",
        "description": "Les documents du module groupes émis ou préparés dans le mois. Montants en euros TTC.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "month",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}$"
            },
            "description": "AAAA-MM (mois en cours par défaut)."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "documents"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "month": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "documents": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DocumentRow"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/documents/{id}": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getDocument",
        "summary": "Un document",
        "description": "Lignes, totaux par taux de TVA, paiements scellés, acheteur et empreinte du document figé.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Identifiant du document (gd_...)."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "document"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "document": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/documents/{id}/facturx": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getFacturX",
        "summary": "Fichier Factur-X d'une facture",
        "description": "Le XML CII (profil EN 16931) joint au PDF/A-3 d'une facture, d'une facture d'acompte ou d'un avoir émis. Un devis répond 409 `no_facturx`.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Identifiant du document."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "xml"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "filename": {
                      "type": "string"
                    },
                    "sha256": {
                      "type": "string"
                    },
                    "xml": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "409": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "500": {
            "$ref": "#/components/responses/ApiError"
          },
          "502": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/organisations": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listOrganisations",
        "summary": "Organisations clientes",
        "description": "Écoles, mairies, associations, entreprises du compte (tous les sites), 30 au plus, les plus récemment modifiées d'abord.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Nom, ville ou SIRET."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "organisations"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "count": {
                      "type": "integer"
                    },
                    "organisations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "siret": {
                            "type": "string"
                          },
                          "city": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string"
                          },
                          "updatedAt": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/organisations/{id}": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getOrganisation",
        "summary": "Une organisation",
        "description": "La fiche : identité, SIRET, codes service Chorus Pro, conditions de paiement. Le nombre de contacts seulement, jamais leurs coordonnées.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Identifiant de l'organisation."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "organisation"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "organisation": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/passes": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listPasses",
        "summary": "Pass et cartes d'abonnement",
        "description": "Les pass de l'organisation (tous les sites), 50 par page. Jamais le code des cartes ni les coordonnées du titulaire.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Nom du titulaire ou début du numéro."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "enum": [
                "active",
                "suspended",
                "refunding",
                "refunded",
                "cancelled",
                "anonymised"
              ]
            },
            "description": "Statut."
          },
          {
            "name": "product",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Formule."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "À partir de 0."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "passes"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "training": {
                      "type": "boolean"
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "pages": {
                      "type": "integer"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "passes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "productId": {
                            "type": "string"
                          },
                          "holder": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "end": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/passes/{id}": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getPass",
        "summary": "Un pass",
        "description": "Formule, validité, entrées restantes, membres, nombre de passages.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Numéro du pass (10 caractères)."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "pass"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "training": {
                      "type": "boolean"
                    },
                    "pass": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/web-orders": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "listWebOrders",
        "summary": "Commandes de la billetterie en ligne",
        "description": "Les commandes d'un jour (heure de Paris), des plus récentes aux plus anciennes, 50 par page. Ce ne sont pas les commandes à emporter (GET /api/v1/orders). Adresse e-mail de l'acheteur masquée ; montants en centimes.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "day",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "AAAA-MM-JJ (aujourd'hui par défaut)."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0
            },
            "description": "À partir de 0."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "orders"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "day": {
                      "type": "string",
                      "format": "date"
                    },
                    "total": {
                      "type": "integer"
                    },
                    "page": {
                      "type": "integer"
                    },
                    "more": {
                      "type": "boolean"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "orders": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    },
    "/api/v1/web-orders/{ref}": {
      "get": {
        "tags": [
          "API avec clé"
        ],
        "operationId": "getWebOrder",
        "summary": "Une commande de la billetterie en ligne",
        "description": "Par sa référence, par exemple WEB-20261007-0042.",
        "security": [
          {
            "ApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "ref",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Référence de la commande."
          }
        ],
        "responses": {
          "200": {
            "description": "Réponse.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "order"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "order": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ApiError"
          },
          "401": {
            "$ref": "#/components/responses/ApiError"
          },
          "403": {
            "$ref": "#/components/responses/ApiError"
          },
          "404": {
            "$ref": "#/components/responses/ApiError"
          },
          "429": {
            "$ref": "#/components/responses/ApiError"
          },
          "503": {
            "$ref": "#/components/responses/ApiError"
          }
        }
      }
    }
  },
  "webhooks": {
    "tillStock": {
      "post": {
        "tags": [
          "Synchronisation"
        ],
        "operationId": "tillToShop",
        "summary": "Ventes et remboursements envoyés par la caisse",
        "description": "Appel signé de la caisse vers votre webhook, mêmes en-têtes `x-wd29-*` et même calcul de signature, avec le secret de la boutique. Vérifiez la signature (fonction `verifySignature` du SDK) avant de lire le corps, et refusez un nonce déjà vu.",
        "parameters": [
          {
            "name": "x-wd29-timestamp",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "x-wd29-nonce",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "x-wd29-signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TillMessage"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reçu. Le corps doit contenir `ok: true`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SyncOk"
                }
              }
            }
          }
        },
        "security": []
      }
    }
  },
  "components": {
    "securitySchemes": {
      "SyncSignature": {
        "type": "apiKey",
        "in": "header",
        "name": "x-wd29-signature",
        "description": "HMAC-SHA256 hexadécimal (64 caractères) de `1\\n{x-wd29-timestamp}\\n{x-wd29-nonce}\\n{corps brut}` avec le secret de la boutique. Ce n'est pas une clé fixe : elle se calcule pour chaque requête (fonction `signedHeaders` du SDK)."
      },
      "ApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "cbz_live_<40 hex>",
        "description": "Clé créée dans la console, page « Accès API ». Elle ne vaut que pour son établissement. Trois droits : lecture seule, lecture et écriture, export complet (export, journal fiscal, totaux par jour et ventes par curseur seulement ; créée par le titulaire du compte)."
      }
    },
    "responses": {
      "PublicError": {
        "description": "Requête refusée. Codes : `no_slug`, `bad_json`, `unknown_shop` (404 : établissement inconnu ou sans carte publique), `no_name`, `no_items`, `persist_failed` (502), `rate_limited` (429, voir `Retry-After`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/PublicError"
            }
          }
        }
      },
      "SyncError": {
        "description": "`bridge_auth` (401 : jeton ou signature), `bridge_request` (400 : message invalide ; `message` vaut `disabled` si la synchronisation est coupée dans la console, `platform` si `source` n'est pas `woo`, `unsupported` pour une opération inconnue), `stock_busy` (503 : réessayer).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/SyncError"
            }
          }
        }
      },
      "ApiError": {
        "description": "`unauthorized` (401 : clé absente, inconnue ou révoquée), `read_only_key` (403), `not_found` (404), `not_available` (404 : fonction absente pour ce métier), `invalid_*` et `bad_json` (400), `managed_by_sync` et `last_item` (409), `rate_limited` (429, voir `Retry-After`). Musées et données : `export_key` (403 : une clé « Export complet » ne sert qu'à l'export), `export_scope_required` (403), `history_not_ready` (409 : historique illimité pas encore activé), `cursor_stale` (409), `catching_up` (503 : le stock rattrape les ventes), `unavailable` (503), `not_configured` (503), `sold_out`, `hold_expired`, `confirm_disabled` (séances). Factur-X : `pdf_drift` et `doc_integrity` (500 : le fichier reconstruit ne correspond plus à celui produit la première fois, il n'est pas délivré), `persist_failed` (502).",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "ok",
                "error"
              ],
              "properties": {
                "ok": {
                  "const": false
                },
                "error": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "PublicError": {
        "type": "object",
        "required": [
          "ok",
          "error"
        ],
        "properties": {
          "ok": {
            "const": false
          },
          "error": {
            "type": "string"
          }
        }
      },
      "WeekHours": {
        "type": "object",
        "required": [
          "tz",
          "days"
        ],
        "properties": {
          "tz": {
            "type": "string",
            "example": "Europe/Paris"
          },
          "days": {
            "type": "object",
            "description": "Clés `mon` à `sun`. Une plage dont la fermeture précède l'ouverture se poursuit après minuit.",
            "additionalProperties": {
              "type": "object",
              "required": [
                "closed",
                "ranges"
              ],
              "properties": {
                "closed": {
                  "type": "boolean"
                },
                "ranges": {
                  "type": "array",
                  "maxItems": 3,
                  "items": {
                    "type": "array",
                    "prefixItems": [
                      {
                        "type": "string",
                        "pattern": "^\\d{2}:\\d{2}$"
                      },
                      {
                        "type": "string",
                        "pattern": "^\\d{2}:\\d{2}$"
                      }
                    ],
                    "minItems": 2,
                    "maxItems": 2
                  }
                }
              }
            }
          }
        }
      },
      "Carte": {
        "type": "object",
        "required": [
          "ok",
          "shop",
          "hours",
          "openNow",
          "categories",
          "items"
        ],
        "properties": {
          "ok": {
            "const": true
          },
          "shop": {
            "type": "object",
            "required": [
              "slug",
              "name",
              "currency"
            ],
            "properties": {
              "slug": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "tagline": {
                "type": "string"
              },
              "address": {
                "type": "string"
              },
              "phone": {
                "type": "string"
              },
              "currency": {
                "type": "string",
                "example": "EUR"
              }
            }
          },
          "hours": {
            "$ref": "#/components/schemas/WeekHours"
          },
          "openNow": {
            "type": "boolean",
            "description": "Ouvert à l'instant, dans le fuseau de l'établissement. Une commande reste possible fermé : elle est alors une précommande."
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "icon": {
                  "type": "string"
                }
              }
            }
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "categoryId",
                "name",
                "price"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "description": "À reprendre dans `items[].itemId` de la commande."
                },
                "categoryId": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "price": {
                  "type": "number",
                  "description": "Prix TTC en euros."
                },
                "description": {
                  "type": "string"
                },
                "tags": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "example": [
                    "végé"
                  ]
                }
              }
            }
          }
        }
      },
      "OrderInput": {
        "type": "object",
        "required": [
          "slug",
          "customerName",
          "items"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "maxLength": 64
          },
          "customerName": {
            "type": "string",
            "maxLength": 80
          },
          "phone": {
            "type": "string",
            "maxLength": 30
          },
          "pickupAt": {
            "type": "string",
            "maxLength": 40,
            "description": "`HH:MM` ou un libellé court. Vide : « Dès que possible »."
          },
          "note": {
            "type": "string",
            "maxLength": 200
          },
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 40,
            "items": {
              "type": "object",
              "required": [
                "itemId"
              ],
              "properties": {
                "itemId": {
                  "type": "string"
                },
                "qty": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 20,
                  "default": 1
                },
                "note": {
                  "type": "string",
                  "maxLength": 200
                }
              }
            }
          }
        },
        "example": {
          "slug": "comptoir-brest",
          "customerName": "Léna",
          "phone": "06 12 34 56 78",
          "pickupAt": "12:30",
          "items": [
            {
              "itemId": "en2",
              "qty": 1
            }
          ]
        }
      },
      "PlacedOrder": {
        "type": "object",
        "required": [
          "id",
          "no",
          "pickupAt",
          "total",
          "status",
          "payment"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "no": {
            "type": "integer",
            "description": "Numéro du jour, celui qu'annonce le comptoir."
          },
          "pickupAt": {
            "type": "string"
          },
          "total": {
            "type": "number",
            "description": "TTC en euros, recalculé par la caisse."
          },
          "status": {
            "const": "nouveau"
          },
          "payment": {
            "type": "object",
            "properties": {
              "method": {
                "const": "onsite"
              },
              "status": {
                "const": "pending"
              }
            }
          }
        }
      },
      "SyncEnvelope": {
        "type": "object",
        "required": [
          "version",
          "source",
          "op"
        ],
        "properties": {
          "version": {
            "const": 1
          },
          "source": {
            "const": "woo",
            "description": "Le côté boutique en ligne du protocole, quelle que soit votre plateforme."
          },
          "op": {
            "type": "string"
          }
        }
      },
      "SyncHealthRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SyncEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "op": {
                "const": "health"
              }
            }
          }
        ],
        "description": "Répond même si la synchronisation est coupée dans la console."
      },
      "SyncTickRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SyncEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "op": {
                "const": "tick"
              }
            }
          }
        ],
        "description": "Réveille la caisse : elle renvoie aussitôt les mouvements de stock en attente vers votre webhook."
      },
      "SyncEventsRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SyncEnvelope"
          },
          {
            "type": "object",
            "required": [
              "events"
            ],
            "properties": {
              "op": {
                "const": "events"
              },
              "events": {
                "type": "array",
                "maxItems": 20,
                "items": {
                  "$ref": "#/components/schemas/SyncEvent"
                }
              }
            }
          }
        ]
      },
      "RecordKey": {
        "type": "string",
        "pattern": "^(woo|ps):(product|variant):[1-9][0-9]*$",
        "description": "Clé d'un article ou d'une déclinaison de votre boutique : `woo:product:<id>` ou `woo:variant:<id>`, id entier positif.",
        "example": "woo:product:408"
      },
      "SyncEvent": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "payload"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-f0-9]{32}$",
            "description": "Clé d'idempotence."
          },
          "kind": {
            "enum": [
              "product",
              "stock",
              "order",
              "customer"
            ],
            "description": "`order` et `customer` sont acceptés sans être conservés."
          },
          "key": {
            "$ref": "#/components/schemas/RecordKey"
          },
          "payload": {
            "oneOf": [
              {
                "type": "object",
                "required": [
                  "data"
                ],
                "properties": {
                  "data": {
                    "$ref": "#/components/schemas/ProductSnapshot"
                  }
                },
                "title": "product"
              },
              {
                "type": "object",
                "required": [
                  "delta"
                ],
                "properties": {
                  "delta": {
                    "type": "integer"
                  }
                },
                "title": "stock (variation)"
              },
              {
                "type": "object",
                "required": [
                  "set_mode",
                  "quantity"
                ],
                "properties": {
                  "set_mode": {
                    "const": true
                  },
                  "quantity": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "null : stock non suivi."
                  }
                },
                "title": "stock (quantité)"
              }
            ]
          }
        }
      },
      "Prices": {
        "type": "object",
        "properties": {
          "regular": {
            "type": [
              "number",
              "string",
              "null"
            ]
          },
          "sale": {
            "type": [
              "number",
              "string",
              "null"
            ],
            "description": "Prioritaire sur `regular` s'il est renseigné."
          },
          "tax_rate": {
            "type": [
              "number",
              "null"
            ],
            "description": "TVA en pourcentage (20, 10, 5.5)."
          },
          "basis": {
            "enum": [
              "gross",
              "net"
            ],
            "default": "gross",
            "description": "`net` : prix HT, la caisse ajoute la TVA."
          }
        }
      },
      "Identifiers": {
        "type": "object",
        "properties": {
          "ean13": {
            "type": "string"
          },
          "gtin": {
            "type": "string"
          },
          "upc": {
            "type": "string"
          }
        }
      },
      "ProductSnapshot": {
        "type": "object",
        "required": [
          "key"
        ],
        "description": "Instantané complet d'un article : il remplace l'article en caisse. Les articles saisis à la main en caisse ne sont jamais touchés.",
        "properties": {
          "key": {
            "$ref": "#/components/schemas/RecordKey"
          },
          "name": {
            "type": "string",
            "maxLength": 80
          },
          "status": {
            "type": "string",
            "description": "Seul `publish` garde l'article en caisse."
          },
          "deleted": {
            "type": "boolean"
          },
          "archived": {
            "type": "boolean"
          },
          "sku": {
            "type": "string"
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "Chemins de catégories ; la caisse garde le premier niveau du plus long.",
            "example": [
              [
                "Vêtements",
                "Pantalons"
              ]
            ]
          },
          "prices": {
            "$ref": "#/components/schemas/Prices"
          },
          "identifiers": {
            "$ref": "#/components/schemas/Identifiers"
          },
          "variants": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "key",
                "status"
              ],
              "properties": {
                "key": {
                  "$ref": "#/components/schemas/RecordKey"
                },
                "status": {
                  "type": "string"
                },
                "attributes": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  },
                  "example": {
                    "taille": "M",
                    "couleur": "Bleu"
                  }
                },
                "sku": {
                  "type": "string"
                },
                "prices": {
                  "$ref": "#/components/schemas/Prices"
                },
                "identifiers": {
                  "$ref": "#/components/schemas/Identifiers"
                }
              }
            }
          },
          "inventory": {
            "type": "array",
            "description": "Quantités de départ, prises en compte seulement pour une clé encore inconnue. Ensuite, seuls les événements `stock` font bouger le stock.",
            "items": {
              "type": "object",
              "required": [
                "key",
                "quantity"
              ],
              "properties": {
                "key": {
                  "$ref": "#/components/schemas/RecordKey"
                },
                "quantity": {
                  "type": [
                    "integer",
                    "null"
                  ]
                }
              }
            }
          }
        }
      },
      "SyncOk": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "const": true
          }
        }
      },
      "SyncAccepted": {
        "type": "object",
        "required": [
          "ok",
          "accepted"
        ],
        "properties": {
          "ok": {
            "const": true
          },
          "accepted": {
            "type": "integer"
          }
        }
      },
      "SyncHealth": {
        "type": "object",
        "required": [
          "ok",
          "protocol",
          "mode"
        ],
        "properties": {
          "ok": {
            "const": true
          },
          "protocol": {
            "const": 1
          },
          "platform": {
            "const": "ps"
          },
          "software": {
            "const": "caisse.bzh"
          },
          "mode": {
            "enum": [
              "live",
              "disabled"
            ]
          },
          "diagnostics": {
            "type": "object",
            "properties": {
              "ok": {
                "type": "boolean"
              },
              "mode": {
                "type": "string"
              },
              "queue": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "direction": {
                      "const": "out"
                    },
                    "state": {
                      "const": "pending"
                    },
                    "count": {
                      "type": "integer"
                    }
                  }
                }
              },
              "issues": {
                "type": "array",
                "items": {}
              }
            }
          }
        }
      },
      "SyncError": {
        "type": "object",
        "required": [
          "ok",
          "code"
        ],
        "properties": {
          "ok": {
            "const": false
          },
          "code": {
            "enum": [
              "bridge_auth",
              "bridge_request"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "TillMessage": {
        "type": "object",
        "required": [
          "version",
          "source",
          "op"
        ],
        "properties": {
          "version": {
            "const": 1
          },
          "source": {
            "const": "ps",
            "description": "La caisse."
          },
          "op": {
            "enum": [
              "events",
              "tick"
            ],
            "description": "`tick` : la caisse vient d'envoyer des mouvements, appliquez votre file."
          },
          "events": {
            "type": "array",
            "description": "Un événement par appel, dans l'ordre. `delta` négatif pour une vente, positif pour un remboursement.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "kind",
                "key",
                "payload"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "pattern": "^[a-f0-9]{32}$"
                },
                "kind": {
                  "const": "stock"
                },
                "key": {
                  "$ref": "#/components/schemas/RecordKey"
                },
                "payload": {
                  "type": "object",
                  "required": [
                    "delta"
                  ],
                  "properties": {
                    "delta": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "Shop": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "siret": {
            "type": "string"
          },
          "vatNumber": {
            "type": "string"
          },
          "kind": {
            "enum": [
              "restaurant",
              "bar",
              "boutique",
              "alimentaire",
              "musee"
            ]
          }
        }
      },
      "Category": {
        "type": "object",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "icon": {
            "type": "string"
          },
          "course": {
            "enum": [
              "starter",
              "main",
              "dessert",
              "drink"
            ]
          },
          "vatRate": {
            "type": "number",
            "description": "0.2 pour 20 %."
          }
        }
      },
      "Item": {
        "type": "object",
        "required": [
          "id",
          "categoryId",
          "name",
          "price",
          "managedBy"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "categoryId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "price": {
            "type": "number",
            "description": "€ TTC"
          },
          "description": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sku": {
            "type": "string"
          },
          "barcode": {
            "type": "string"
          },
          "variants": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "price": {
                  "type": "number"
                },
                "sku": {
                  "type": "string"
                },
                "barcode": {
                  "type": "string"
                },
                "syncKey": {
                  "type": "string"
                }
              }
            }
          },
          "modifiers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "required": {
                  "type": "boolean"
                },
                "multiple": {
                  "type": "boolean"
                },
                "options": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "name": {
                        "type": "string"
                      },
                      "price": {
                        "type": "number"
                      }
                    }
                  }
                }
              }
            }
          },
          "syncKey": {
            "type": "string",
            "description": "Clé de l'article dans la boutique en ligne reliée."
          },
          "managedBy": {
            "enum": [
              "caisse",
              "sync"
            ],
            "description": "`sync` : l'article vient de la boutique en ligne et ne se modifie que là-bas."
          }
        }
      },
      "ItemCreate": {
        "type": "object",
        "required": [
          "name",
          "price",
          "categoryId"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "price": {
            "type": "number",
            "minimum": 0,
            "maximum": 100000,
            "description": "€ TTC"
          },
          "categoryId": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "maxLength": 200
          },
          "tags": {
            "type": "array",
            "maxItems": 6,
            "items": {
              "type": "string",
              "maxLength": 24
            }
          },
          "sku": {
            "type": "string",
            "maxLength": 64
          },
          "barcode": {
            "type": "string",
            "maxLength": 32,
            "pattern": "^[0-9A-Za-z-]*$"
          }
        },
        "example": {
          "name": "Galette complète",
          "price": 11.5,
          "categoryId": "plats",
          "tags": [
            "signature"
          ]
        }
      },
      "ItemUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "price": {
            "type": "number",
            "minimum": 0,
            "maximum": 100000,
            "description": "€ TTC"
          },
          "categoryId": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "maxLength": 200
          },
          "tags": {
            "type": "array",
            "maxItems": 6,
            "items": {
              "type": "string",
              "maxLength": 24
            }
          },
          "sku": {
            "type": "string",
            "maxLength": 64
          },
          "barcode": {
            "type": "string",
            "maxLength": 32,
            "pattern": "^[0-9A-Za-z-]*$"
          }
        },
        "example": {
          "price": 12
        }
      },
      "StockLine": {
        "type": "object",
        "required": [
          "syncKey",
          "quantity"
        ],
        "properties": {
          "syncKey": {
            "$ref": "#/components/schemas/RecordKey"
          },
          "quantity": {
            "type": [
              "integer",
              "null"
            ],
            "description": "null : stock non suivi."
          },
          "itemId": {
            "type": "string"
          },
          "variantId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "sku": {
            "type": "string"
          }
        }
      },
      "Sale": {
        "type": "object",
        "required": [
          "id",
          "number",
          "day",
          "settledAt",
          "total",
          "items",
          "payments"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "number": {
            "type": "integer",
            "description": "Numéro de ticket du jour."
          },
          "fiscalNumber": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Numéro dans le journal fiscal scellé."
          },
          "day": {
            "type": "string",
            "format": "date"
          },
          "settledAt": {
            "type": "string",
            "format": "date-time"
          },
          "settledBy": {
            "type": "string"
          },
          "type": {
            "enum": [
              "table",
              "counter"
            ]
          },
          "label": {
            "type": "string"
          },
          "covers": {
            "type": "integer"
          },
          "subtotal": {
            "type": "number"
          },
          "discount": {
            "type": "number"
          },
          "total": {
            "type": "number",
            "description": "€ TTC"
          },
          "tip": {
            "type": "number"
          },
          "loyaltyRedemption": {
            "type": "number",
            "description": "€ réglés en points de fidélité."
          },
          "vat": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "rate": {
                  "type": "number"
                },
                "gross": {
                  "type": "number"
                },
                "net": {
                  "type": "number"
                },
                "vat": {
                  "type": "number"
                }
              }
            }
          },
          "payments": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "method": {
                  "type": "string",
                  "description": "cash, card, mobile, voucher, cheque, transfer, ancv, gift, payfip, web_card, passculture, deferred"
                },
                "amount": {
                  "type": "number"
                },
                "tx": {
                  "type": "string",
                  "description": "Paiement carte piloté par le terminal : identifiant de la transaction."
                },
                "card": {
                  "type": "object",
                  "description": "Réponse du terminal de paiement (jamais le numéro complet de la carte).",
                  "properties": {
                    "brand": {
                      "type": "string"
                    },
                    "last4": {
                      "type": "string"
                    },
                    "auth": {
                      "type": "string"
                    },
                    "mode": {
                      "type": "string"
                    },
                    "app": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "qty": {
                  "type": "number"
                },
                "unitPrice": {
                  "type": "number"
                },
                "total": {
                  "type": "number"
                },
                "vatRate": {
                  "type": "number"
                },
                "itemId": {
                  "type": "string"
                },
                "variantId": {
                  "type": "string"
                },
                "sku": {
                  "type": "string"
                }
              }
            }
          },
          "training": {
            "type": "boolean",
            "description": "Vente du mode formation, hors chiffre d'affaires."
          }
        }
      },
      "Order": {
        "type": "object",
        "required": [
          "id",
          "number",
          "status",
          "items",
          "total"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "number": {
            "type": "integer"
          },
          "status": {
            "enum": [
              "new",
              "accepted",
              "ready",
              "collected",
              "cancelled"
            ]
          },
          "customerName": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "pickupAt": {
            "type": "string"
          },
          "note": {
            "type": "string"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "itemId": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "qty": {
                  "type": "integer"
                },
                "unitPrice": {
                  "type": "number"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "total": {
            "type": "number"
          },
          "payment": {
            "type": "object",
            "properties": {
              "method": {
                "enum": [
                  "onsite",
                  "card"
                ]
              },
              "status": {
                "enum": [
                  "pending",
                  "paid"
                ]
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AccessGate": {
        "type": "object",
        "required": [
          "id",
          "name",
          "zone",
          "direction",
          "active"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifiant de la porte, à envoyer dans « gate »."
          },
          "name": {
            "type": "string"
          },
          "zone": {
            "type": "string",
            "description": "Identifiant de la zone contrôlée par cette porte."
          },
          "direction": {
            "enum": [
              "in",
              "out",
              "both"
            ]
          },
          "active": {
            "type": "boolean"
          },
          "archivedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "AccessValidateInput": {
        "type": "object",
        "required": [
          "code",
          "gate",
          "eid"
        ],
        "properties": {
          "code": {
            "type": "string",
            "maxLength": 400,
            "description": "Texte lu dans le QR code du billet (CBZ1/...)."
          },
          "gate": {
            "type": "string",
            "description": "Porte où se présente le visiteur (GET /api/v1/access/gates)."
          },
          "eid": {
            "type": "string",
            "maxLength": 120,
            "pattern": "^key_[a-f0-9]+-[A-Za-z0-9_-]+$",
            "description": "Identifiant unique du scan, choisi par vous : l'identifiant de la clé (key_...), un tiret, puis lettres, chiffres, _ ou -. Renvoyé à l'identique en cas de nouvel essai, il ne compte jamais deux entrées."
          },
          "n": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500,
            "description": "Nombre de personnes, pour un billet de groupe. Sans n, un billet de groupe répond verdict « ask » avec « max »."
          },
          "dir": {
            "enum": [
              "in",
              "out"
            ],
            "description": "Sens du passage, pour une porte à double sens."
          },
          "dryRun": {
            "type": "boolean",
            "default": false,
            "description": "Vérifier sans consommer : aucune entrée, aucune ligne au journal de la porte."
          }
        }
      },
      "AccessVerdict": {
        "type": "object",
        "required": [
          "ok",
          "verdict",
          "res",
          "replayed",
          "logged",
          "view",
          "serverNow"
        ],
        "properties": {
          "ok": {
            "const": true
          },
          "verdict": {
            "enum": [
              "accept",
              "refuse",
              "ask"
            ],
            "description": "Un refus est une réponse 200, pas une erreur."
          },
          "res": {
            "type": "string",
            "description": "entry, reentry, exit, ou le motif du refus : already_used, group_full, not_yet, expired, revoked, wrong_site, zone_not_included, training_ticket, live_ticket, passback, unknown_ticket, unknown_format, bad_signature, unknown_key, pass_refused..."
          },
          "n": {
            "type": "integer"
          },
          "max": {
            "type": "integer"
          },
          "remaining": {
            "type": "integer"
          },
          "replayed": {
            "type": "boolean",
            "description": "Ce scan (même eid) avait déjà été enregistré : rien n'est compté une seconde fois."
          },
          "logged": {
            "type": "boolean"
          },
          "dryRun": {
            "type": "boolean"
          },
          "training": {
            "type": "boolean"
          },
          "ticket": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "tid": {
                "type": "string"
              },
              "no": {
                "type": "string"
              },
              "product": {
                "type": "string"
              },
              "tariff": {
                "type": "string"
              },
              "proof": {
                "type": "boolean"
              },
              "admits": {
                "type": "integer"
              },
              "validFrom": {
                "type": "integer"
              },
              "validTo": {
                "type": "integer"
              },
              "slot": {
                "type": "boolean"
              },
              "holder": {
                "type": "string"
              }
            }
          },
          "first": {
            "type": "object",
            "properties": {
              "at": {
                "type": "integer"
              },
              "gate": {
                "type": "string"
              },
              "gateName": {
                "type": "string"
              }
            }
          },
          "view": {
            "type": "object",
            "description": "Le texte de l'écran des scanners, en français.",
            "properties": {
              "tone": {
                "enum": [
                  "ok",
                  "warn",
                  "no"
                ]
              },
              "icon": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "detail": {
                "type": "string"
              }
            }
          },
          "serverNow": {
            "type": "integer"
          },
          "ms": {
            "type": "integer"
          }
        }
      },
      "AccessEvent": {
        "type": "object",
        "properties": {
          "gate": {
            "type": "string"
          },
          "seq": {
            "type": "integer"
          },
          "eid": {
            "type": "string"
          },
          "ticket": {
            "type": [
              "string",
              "null"
            ]
          },
          "kind": {
            "enum": [
              "entry",
              "exit",
              "refusal",
              "cancellation"
            ]
          },
          "result": {
            "type": "string"
          },
          "resultLabel": {
            "type": "string"
          },
          "zone": {
            "type": "string"
          },
          "persons": {
            "type": "integer"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "receivedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "device": {
            "type": "string",
            "description": "Scanner (scn_...), clé (key_...) ou « session »."
          },
          "mode": {
            "enum": [
              "online",
              "offline"
            ]
          },
          "input": {
            "type": "string"
          },
          "product": {
            "type": [
              "string",
              "null"
            ]
          },
          "tariff": {
            "type": [
              "string",
              "null"
            ]
          },
          "session": {
            "type": [
              "string",
              "null"
            ]
          },
          "reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "cancelledEntryAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "SaleV1": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Sale"
          }
        ],
        "type": "object",
        "properties": {
          "seq": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Numéro de l'écriture du journal fiscal."
          },
          "refund": {
            "type": "boolean"
          },
          "refundOf": {
            "type": [
              "integer",
              "null"
            ]
          },
          "terminal": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "SessionV1": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "date": {
            "type": "string",
            "format": "date"
          },
          "start": {
            "type": "string"
          },
          "end": {
            "type": "string"
          },
          "startsAt": {
            "type": "string"
          },
          "endsAt": {
            "type": "string"
          },
          "activityId": {
            "type": "string"
          },
          "activityName": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "lang": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "level": {
            "type": "string"
          },
          "capacity": {
            "type": "integer"
          },
          "free": {
            "type": "integer"
          },
          "sold": {
            "type": "integer"
          },
          "held": {
            "type": "integer"
          }
        }
      },
      "HoldV1": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "quantity": {
            "type": "integer"
          },
          "state": {
            "enum": [
              "hold",
              "confirmed",
              "released"
            ]
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "GroupBookingRow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "number": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "date": {
            "type": [
              "string",
              "null"
            ]
          },
          "start": {
            "type": [
              "string",
              "null"
            ]
          },
          "organisation": {
            "type": "string"
          },
          "organisationId": {
            "type": [
              "string",
              "null"
            ]
          },
          "persons": {
            "type": "integer"
          },
          "adults": {
            "type": "integer"
          },
          "kind": {
            "type": "string"
          },
          "optionUntil": {
            "type": [
              "string",
              "null"
            ]
          },
          "training": {
            "type": "boolean"
          },
          "updatedAt": {
            "type": "string"
          }
        }
      },
      "DocumentRow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "enum": [
              "quote",
              "invoice",
              "deposit_request",
              "deposit",
              "credit",
              "titre"
            ]
          },
          "number": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string"
          },
          "organisation": {
            "type": "string"
          },
          "organisationId": {
            "type": [
              "string",
              "null"
            ]
          },
          "bookingId": {
            "type": [
              "string",
              "null"
            ]
          },
          "bookingNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "gross": {
            "type": "number"
          },
          "due": {
            "type": "number"
          },
          "issueDate": {
            "type": [
              "string",
              "null"
            ]
          },
          "dueDate": {
            "type": [
              "string",
              "null"
            ]
          },
          "training": {
            "type": "boolean"
          }
        }
      },
      "AttendanceYear": {
        "type": "object",
        "required": [
          "ok",
          "year",
          "scope",
          "shopId",
          "rows",
          "openData",
          "sources",
          "warnings"
        ],
        "properties": {
          "ok": {
            "const": true
          },
          "year": {
            "type": "string"
          },
          "scope": {
            "const": "shop"
          },
          "shopId": {
            "type": "string"
          },
          "site": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "museofileId": {
                "type": "string",
                "description": "M suivi de 4 chiffres, ou vide."
              },
              "shops": {
                "type": "integer",
                "description": "Nombre d'établissements du site."
              }
            }
          },
          "profile": {
            "type": "string"
          },
          "rows": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "value": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "unit": {
                  "enum": [
                    "",
                    "%"
                  ]
                },
                "sources": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "openData": {
            "type": [
              "object",
              "null"
            ],
            "description": "Ligne au format du jeu de données public « Fréquentation des musées de France ». null quand le site compte plusieurs établissements.",
            "properties": {
              "museofileId": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "year": {
                "type": "string"
              },
              "paid": {
                "type": "integer"
              },
              "free": {
                "type": "integer"
              },
              "total": {
                "type": "integer"
              }
            }
          },
          "sources": {
            "type": "object",
            "properties": {
              "journal": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "visitors": {
                    "type": "integer"
                  },
                  "paid": {
                    "type": "integer"
                  },
                  "free": {
                    "type": "integer"
                  }
                }
              },
              "manual": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "visitors": {
                    "type": "integer"
                  },
                  "paid": {
                    "type": "integer"
                  },
                  "free": {
                    "type": "integer"
                  }
                }
              },
              "imported": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "visitors": {
                    "type": "integer"
                  },
                  "paid": {
                    "type": "integer"
                  },
                  "free": {
                    "type": "integer"
                  }
                },
                "description": "Historique importé, jamais compté dans les rubriques ; null sans import."
              }
            },
            "description": "Même forme que sources de GET /api/v1/attendance."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "note": {
            "type": "string"
          }
        }
      },
      "TicketCatalog": {
        "type": "object",
        "required": [
          "ok",
          "offers"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "shop": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              }
            }
          },
          "open": {
            "type": "boolean",
            "description": "La vente en ligne est ouverte."
          },
          "today": {
            "type": "string",
            "format": "date"
          },
          "lang": {
            "type": "string"
          },
          "offers": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "offerId",
                "label",
                "tariffs"
              ],
              "properties": {
                "offerId": {
                  "type": "string",
                  "description": "À reprendre dans `#o=` et dans `offer`."
                },
                "label": {
                  "type": "string"
                },
                "desc": {
                  "type": "string"
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "perm",
                    "expo",
                    "activite",
                    "event"
                  ]
                },
                "code": {
                  "type": "string"
                },
                "act": {
                  "type": "string",
                  "description": "Présent quand l'offre se vend par séance."
                },
                "tariffs": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "tariffId": {
                        "type": "string"
                      },
                      "label": {
                        "type": "string"
                      },
                      "unitCents": {
                        "type": "integer"
                      },
                      "needsProof": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              }
            }
          },
          "sessions": {
            "type": "array",
            "description": "Avec `offer`.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "À reprendre dans `#s=`."
                },
                "day": {
                  "type": "string",
                  "format": "date"
                },
                "start": {
                  "type": "string",
                  "example": "20:30"
                },
                "end": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "level": {
                  "type": "string",
                  "enum": [
                    "open",
                    "last",
                    "full",
                    "paused"
                  ]
                },
                "free": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "Places restantes, donné seulement quand il en reste peu."
                },
                "waitlist": {
                  "type": "boolean"
                }
              }
            }
          }
        }
      },
      "ShopCatalog": {
        "type": "object",
        "required": [
          "ok",
          "v",
          "shop",
          "open",
          "categories"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "v": {
            "type": "integer",
            "const": 1
          },
          "shop": {
            "type": "object",
            "properties": {
              "slug": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "tagline": {
                "type": "string"
              },
              "address": {
                "type": "string"
              },
              "phone": {
                "type": "string"
              },
              "origin": {
                "type": "string",
                "enum": [
                  "https://caisse.bzh",
                  "https://pos.inklura.fr"
                ]
              },
              "url": {
                "type": "string",
                "format": "uri"
              },
              "demo": {
                "type": "boolean"
              }
            }
          },
          "open": {
            "type": "boolean"
          },
          "payment": {
            "type": "string",
            "enum": [
              "pickup",
              "online"
            ],
            "description": "pickup : paiement au retrait."
          },
          "training": {
            "type": "boolean"
          },
          "currency": {
            "type": "string",
            "const": "EUR"
          },
          "symbol": {
            "type": "string"
          },
          "pickup": {
            "type": "string",
            "description": "Consignes de retrait."
          },
          "stockKnown": {
            "type": "boolean"
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "icon": {
                  "type": "string"
                },
                "items": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": [
                      "id",
                      "name",
                      "price",
                      "url"
                    ],
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "name": {
                        "type": "string"
                      },
                      "desc": {
                        "type": "string"
                      },
                      "image": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "format": "uri"
                      },
                      "price": {
                        "type": "number",
                        "description": "Euros TTC, le plus bas des déclinaisons."
                      },
                      "priceMax": {
                        "type": "number"
                      },
                      "soldOut": {
                        "type": "boolean"
                      },
                      "left": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "Ce qu'il reste (article sans déclinaison) ; null quand il n'est pas compté."
                      },
                      "url": {
                        "type": "string",
                        "format": "uri",
                        "description": "Page de l'article dans la boutique."
                      },
                      "variants": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            },
                            "price": {
                              "type": "number"
                            },
                            "left": {
                              "type": [
                                "integer",
                                "null"
                              ]
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 100
        },
        "description": "Clé libre (un UUID par exemple). Une requête renvoyée avec la même clé dans les 24 heures reçoit la première réponse (en-tête `Idempotent-Replayed: true`) au lieu de créer un doublon. Même clé pour une requête différente : 422 `idempotency_key_reused` ; requête identique encore en cours : 409 `idempotency_in_progress`."
      }
    }
  }
}
