{
  "openapi": "3.1.0",
  "info": {
    "title": "API Zen595",
    "version": "1.0.0",
    "description": "Factures Tarif 595 (generalInvoice XML 5.0, PDF avec QR-facture) depuis votre logiciel : site, caisse, outil de réservation. Base de données en Suisse ; numéros AVS et d'assuré·e chiffrés.",
    "contact": {
      "email": "contact@zen595.ch"
    },
    "license": {
      "name": "Conditions générales Zen595",
      "url": "https://zen595.ch/conditions"
    }
  },
  "servers": [
    {
      "url": "https://api.zen595.ch"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Élèves",
      "description": "Les personnes facturées (patients au sens du XML 5.0)."
    },
    {
      "name": "Prestations",
      "description": "Vos cours ou abonnements, chacun avec sa position du Tarif 595 et son prix."
    },
    {
      "name": "Factures",
      "description": "Émission, PDF, XML, envoi par e-mail, annulation."
    },
    {
      "name": "Cours",
      "description": "Facturer tous les élèves d'un cours en une requête."
    },
    {
      "name": "Référentiels",
      "description": "Positions du Tarif 595 et caisses (sans authentification)."
    }
  ],
  "paths": {
    "/v1/participants": {
      "get": {
        "tags": [
          "Élèves"
        ],
        "summary": "Lister les élèves",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Recherche nom, prénom, e-mail"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "participants": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Participant"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "listParticipants"
      },
      "post": {
        "tags": [
          "Élèves"
        ],
        "summary": "Créer un élève",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255
            },
            "description": "Rejouer la requête sans doublon pendant 24 h (recommandé pour toute création)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "familyName",
                  "givenName"
                ],
                "properties": {
                  "familyName": {
                    "type": "string",
                    "description": "Nom",
                    "maxLength": 35
                  },
                  "givenName": {
                    "type": "string",
                    "description": "Prénom",
                    "maxLength": 35
                  },
                  "sex": {
                    "enum": [
                      "female",
                      "male",
                      null
                    ],
                    "description": "Exigé par le XML 5.0 pour facturer"
                  },
                  "birthdate": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Date de naissance",
                    "format": "date",
                    "example": "2027-01-01"
                  },
                  "street": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "houseNo": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "zip": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "NPA"
                  },
                  "city": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Localité"
                  },
                  "canton": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Canton (VD, GE…)"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Pour l'envoi des factures",
                    "format": "email"
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "language": {
                    "enum": [
                      "fr",
                      "de",
                      "it",
                      null
                    ]
                  },
                  "insurerId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Identifiant d'une caisse de GET /v1/insurers"
                  },
                  "insuredId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Numéro d'assuré·e (chiffré au repos)"
                  },
                  "ahvNumber": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Numéro AVS 756.XXXX.XXXX.XX, facultatif (chiffré, jamais renvoyé en entier)"
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "externalRef": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Votre identifiant (unique par compte) : l'import met à jour au lieu de doubler"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Créé",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "participant": {
                      "$ref": "#/components/schemas/Participant"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "createParticipant"
      }
    },
    "/v1/participants/{id}": {
      "get": {
        "tags": [
          "Élèves"
        ],
        "summary": "Lire un élève",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "par_…"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "participant": {
                      "$ref": "#/components/schemas/Participant"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Introuvable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "getParticipant"
      },
      "patch": {
        "tags": [
          "Élèves"
        ],
        "summary": "Modifier un élève",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "par_…"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [],
                "properties": {
                  "familyName": {
                    "type": "string",
                    "description": "Nom",
                    "maxLength": 35
                  },
                  "givenName": {
                    "type": "string",
                    "description": "Prénom",
                    "maxLength": 35
                  },
                  "sex": {
                    "enum": [
                      "female",
                      "male",
                      null
                    ],
                    "description": "Exigé par le XML 5.0 pour facturer"
                  },
                  "birthdate": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Date de naissance",
                    "format": "date",
                    "example": "2027-01-01"
                  },
                  "street": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "houseNo": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "zip": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "NPA"
                  },
                  "city": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Localité"
                  },
                  "canton": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Canton (VD, GE…)"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Pour l'envoi des factures",
                    "format": "email"
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "language": {
                    "enum": [
                      "fr",
                      "de",
                      "it",
                      null
                    ]
                  },
                  "insurerId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Identifiant d'une caisse de GET /v1/insurers"
                  },
                  "insuredId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Numéro d'assuré·e (chiffré au repos)"
                  },
                  "ahvNumber": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Numéro AVS 756.XXXX.XXXX.XX, facultatif (chiffré, jamais renvoyé en entier)"
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "externalRef": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Votre identifiant (unique par compte) : l'import met à jour au lieu de doubler"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "participant": {
                      "$ref": "#/components/schemas/Participant"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "updateParticipant"
      },
      "delete": {
        "tags": [
          "Élèves"
        ],
        "summary": "Archiver un élève",
        "description": "Ses factures restent consultables.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "par_…"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "archiveParticipant"
      }
    },
    "/v1/participants/import": {
      "post": {
        "tags": [
          "Élèves"
        ],
        "summary": "Importer des élèves en masse",
        "description": "Jusqu'à 2000 lignes. Dates en `YYYY-MM-DD` ou `JJ.MM.AAAA`, sexe `F`/`M`, caisse par nom (`insurerName`) ou GLN (`insurerGln`). Avec `?dryRun=1`, contrôle sans enregistrer. Une ligne dont `externalRef` existe déjà met la fiche à jour.",
        "parameters": [
          {
            "name": "dryRun",
            "in": "query",
            "schema": {
              "enum": [
                "1"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255
            },
            "description": "Rejouer la requête sans doublon pendant 24 h (recommandé pour toute création)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rows"
                ],
                "properties": {
                  "rows": {
                    "type": "array",
                    "maxItems": 2000,
                    "items": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dryRun": {
                      "type": "boolean"
                    },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "created": {
                          "type": "integer"
                        },
                        "updated": {
                          "type": "integer"
                        },
                        "invalid": {
                          "type": "integer"
                        }
                      }
                    },
                    "rows": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "importParticipants"
      }
    },
    "/v1/services": {
      "get": {
        "tags": [
          "Prestations"
        ],
        "summary": "Lister les prestations",
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "services": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Service"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "listServices"
      },
      "post": {
        "tags": [
          "Prestations"
        ],
        "summary": "Créer une prestation",
        "description": "L'unité (séance ou mois) est déduite de la position.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255
            },
            "description": "Rejouer la requête sans doublon pendant 24 h (recommandé pour toute création)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "tarifPosition",
                  "unitPrice"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "tarifPosition": {
                    "type": "string",
                    "description": "Position du Tarif 595, ex. 3029"
                  },
                  "unitPrice": {
                    "type": "number",
                    "description": "CHF par unité"
                  },
                  "vatRate": {
                    "type": [
                      "number",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Créée",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "service": {
                      "$ref": "#/components/schemas/Service"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "createService"
      }
    },
    "/v1/invoices": {
      "get": {
        "tags": [
          "Factures"
        ],
        "summary": "Lister les factures",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "enum": [
                "issued",
                "cancelled"
              ]
            }
          },
          {
            "name": "participantId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Pagination : issuedAt de la dernière facture reçue"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invoices": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Invoice"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "listInvoices"
      },
      "post": {
        "tags": [
          "Factures"
        ],
        "summary": "Émettre une facture",
        "description": "Contrôle complet avant émission : s'il manque une donnée exigée par le Tarif 595, réponse 422 avec la liste `missing` (rien n'est créé). Avec `?dryRun=1`, contrôle seulement. Numérotation continue par compte.",
        "parameters": [
          {
            "name": "dryRun",
            "in": "query",
            "schema": {
              "enum": [
                "1"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255
            },
            "description": "Rejouer la requête sans doublon pendant 24 h (recommandé pour toute création)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InvoiceInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Émise",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invoice": {
                      "$ref": "#/components/schemas/Invoice"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Offre Découverte : 10 factures par mois atteintes (code PLAN_LIMIT).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Données manquantes pour le Tarif 595",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Missing"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "createInvoice"
      }
    },
    "/v1/invoices/{id}": {
      "get": {
        "tags": [
          "Factures"
        ],
        "summary": "Lire une facture",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "inv_…"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invoice": {
                      "$ref": "#/components/schemas/Invoice"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Introuvable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "getInvoice"
      }
    },
    "/v1/invoices/{id}/pdf": {
      "get": {
        "tags": [
          "Factures"
        ],
        "summary": "PDF de la facture",
        "description": "Facture avec QR-facture, justificatif de remboursement et feuille de codes QR. Régénéré à l'identique et vérifié contre l'empreinte du XML.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "inv_…"
          }
        ],
        "responses": {
          "200": {
            "description": "PDF",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "getInvoicePdf"
      }
    },
    "/v1/invoices/{id}/xml": {
      "get": {
        "tags": [
          "Factures"
        ],
        "summary": "XML generalInvoice 5.0",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "inv_…"
          }
        ],
        "responses": {
          "200": {
            "description": "XML",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "getInvoiceXml"
      }
    },
    "/v1/invoices/{id}/send": {
      "post": {
        "tags": [
          "Factures"
        ],
        "summary": "Envoyer la facture à l'élève par e-mail",
        "description": "PDF en pièce jointe, au nom de votre studio ; les réponses arrivent sur votre e-mail.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "inv_…"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255
            },
            "description": "Rejouer la requête sans doublon pendant 24 h (recommandé pour toute création)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "to": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Remplace l'e-mail de la fiche",
                    "format": "email"
                  },
                  "message": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Message personnel",
                    "maxLength": 1000
                  },
                  "saveEmail": {
                    "type": "boolean",
                    "description": "Enregistrer `to` sur une fiche sans e-mail"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invoiceId": {
                      "type": "string"
                    },
                    "status": {
                      "const": "sent"
                    },
                    "to": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Facture annulée",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Pas d'adresse e-mail (code NO_EMAIL)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "sendInvoice"
      }
    },
    "/v1/invoices/{id}/cancel": {
      "post": {
        "tags": [
          "Factures"
        ],
        "summary": "Annuler une facture",
        "description": "Une facture émise ne se modifie pas : annulez-la, puis émettez la facture corrigée avec `cancelsId`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "inv_…"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255
            },
            "description": "Rejouer la requête sans doublon pendant 24 h (recommandé pour toute création)."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invoice": {
                      "$ref": "#/components/schemas/Invoice"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "cancelInvoice"
      }
    },
    "/v1/courses/{id}/invoices": {
      "post": {
        "tags": [
          "Cours"
        ],
        "summary": "Facturer les élèves d'un cours",
        "description": "Une facture par élève (100 au plus par requête). Les élèves déjà facturés pour ce cours sont ignorés ; un élève incomplet est signalé sans bloquer les autres. `?dryRun=1` pour contrôler.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "crs_…"
          },
          {
            "name": "dryRun",
            "in": "query",
            "schema": {
              "enum": [
                "1"
              ]
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255
            },
            "description": "Rejouer la requête sans doublon pendant 24 h (recommandé pour toute création)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "participantIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 100,
                    "description": "Par défaut : tous les élèves non facturés"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "dryRun": {
                      "type": "boolean"
                    },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "ready": {
                          "type": "integer"
                        },
                        "issued": {
                          "type": "integer"
                        },
                        "already": {
                          "type": "integer"
                        },
                        "missing": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        }
                      }
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "participantId": {
                            "type": "string"
                          },
                          "status": {
                            "enum": [
                              "ready",
                              "issued",
                              "already",
                              "missing",
                              "limit"
                            ]
                          },
                          "invoiceId": {
                            "type": "string"
                          },
                          "invoiceNumber": {
                            "type": "string"
                          },
                          "missing": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "operationId": "invoiceCourse"
      }
    },
    "/v1/positions": {
      "get": {
        "tags": [
          "Référentiels"
        ],
        "summary": "Positions du Tarif 595",
        "description": "Catalogue officiel (FR, DE, IT) avec dates de validité.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [],
        "operationId": "listPositions"
      }
    },
    "/v1/insurers": {
      "get": {
        "tags": [
          "Référentiels"
        ],
        "summary": "Caisses (liste MediData)",
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "insurers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "gln": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Corps invalide : `fields` liste les champs en erreur.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Clé API absente ou invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Plus de 120 requêtes par minute pour cette clé (en-tête Retry-After).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [],
        "operationId": "listInsurers"
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Clé API `z595_live_…` créée dans Paramètres → Clés API (offres Solo et Studio)."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Message"
          },
          "code": {
            "type": "string",
            "description": "Code stable, ex. PLAN_LIMIT"
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "path": {
                  "type": "string",
                  "description": "Champ"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Missing": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "participant.insuredId",
              "issuer.rcc"
            ]
          }
        }
      },
      "Participant": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "par_…"
          },
          "familyName": {
            "type": "string",
            "description": "Nom",
            "maxLength": 35
          },
          "givenName": {
            "type": "string",
            "description": "Prénom",
            "maxLength": 35
          },
          "sex": {
            "enum": [
              "female",
              "male",
              null
            ],
            "description": "Exigé par le XML 5.0 pour facturer"
          },
          "birthdate": {
            "type": [
              "string",
              "null"
            ],
            "description": "Date de naissance",
            "format": "date",
            "example": "2027-01-01"
          },
          "street": {
            "type": [
              "string",
              "null"
            ]
          },
          "houseNo": {
            "type": [
              "string",
              "null"
            ]
          },
          "zip": {
            "type": [
              "string",
              "null"
            ],
            "description": "NPA"
          },
          "city": {
            "type": [
              "string",
              "null"
            ],
            "description": "Localité"
          },
          "canton": {
            "type": [
              "string",
              "null"
            ],
            "description": "Canton (VD, GE…)"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pour l'envoi des factures",
            "format": "email"
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "language": {
            "enum": [
              "fr",
              "de",
              "it",
              null
            ]
          },
          "insurerId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Identifiant d'une caisse de GET /v1/insurers"
          },
          "insuredId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Numéro d'assuré·e (chiffré au repos)"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "externalRef": {
            "type": [
              "string",
              "null"
            ],
            "description": "Votre identifiant (unique par compte) : l'import met à jour au lieu de doubler"
          },
          "ahvNumberEnd": {
            "type": [
              "string",
              "null"
            ],
            "description": "3 derniers chiffres du n° AVS"
          },
          "archivedAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Service": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "svc_…"
          },
          "name": {
            "type": "string"
          },
          "tarifPosition": {
            "type": "string"
          },
          "unit": {
            "enum": [
              "session",
              "month"
            ]
          },
          "unitPrice": {
            "type": "string",
            "example": "390.00",
            "pattern": "^\\d+\\.\\d{2}$"
          },
          "vatRate": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "InvoiceInput": {
        "type": "object",
        "required": [
          "participantId",
          "lines",
          "prepaid"
        ],
        "properties": {
          "participantId": {
            "type": "string",
            "description": "par_…"
          },
          "lines": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "required": [
                "serviceId",
                "quantity",
                "dateBegin",
                "dateEnd"
              ],
              "properties": {
                "serviceId": {
                  "type": "string",
                  "description": "svc_…"
                },
                "quantity": {
                  "type": "number",
                  "description": "Séances ou mois"
                },
                "unitPrice": {
                  "type": "number",
                  "description": "CHF ; défaut : prix de la prestation"
                },
                "amount": {
                  "type": "number",
                  "description": "Ou le total de la ligne, réparti au centime"
                },
                "dateBegin": {
                  "type": "string",
                  "format": "date",
                  "example": "2027-01-01"
                },
                "dateEnd": {
                  "type": "string",
                  "format": "date",
                  "example": "2027-01-01"
                }
              }
            }
          },
          "prepaid": {
            "type": "boolean",
            "description": "Déjà payé : facture acquittée (quittance pour le remboursement)"
          },
          "canton": {
            "type": [
              "string",
              "null"
            ],
            "description": "Lieu des cours ; défaut : canton du compte"
          },
          "remark": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 350
          },
          "cancelsId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Facture annulée que celle-ci remplace"
          },
          "billingProfileId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Lieu ou praticien·ne (offre Studio)"
          }
        }
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "inv_…"
          },
          "invoiceNumber": {
            "type": "string",
            "example": "Z595-2027-0001"
          },
          "status": {
            "enum": [
              "issued",
              "cancelled"
            ]
          },
          "participantId": {
            "type": "string"
          },
          "courseId": {
            "type": [
              "string",
              "null"
            ]
          },
          "amount": {
            "type": "string",
            "example": "390.00",
            "pattern": "^\\d+\\.\\d{2}$"
          },
          "amountPrepaid": {
            "type": "string",
            "example": "390.00",
            "pattern": "^\\d+\\.\\d{2}$"
          },
          "periodStart": {
            "type": "string",
            "format": "date",
            "example": "2027-01-01"
          },
          "periodEnd": {
            "type": "string",
            "format": "date",
            "example": "2027-01-01"
          },
          "tarifPositions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "insurerGln": {
            "type": [
              "string",
              "null"
            ]
          },
          "xmlSha256": {
            "type": "string",
            "description": "Empreinte du XML émis"
          },
          "sentAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "cancelsId": {
            "type": [
              "string",
              "null"
            ]
          },
          "issuedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  }
}