{
  "openapi": "3.1.0",
  "info": {
    "title": "Treats — API partenaires",
    "version": "v1",
    "description": "API des éditeurs partenaires de Treats. Treats enregistre l'application de l'éditeur ; chaque organisation (une clinique ou un groupe) la connecte en choisissant ses sites et ses droits. Jeton : `POST /api/partner/v1/oauth/token` (client_credentials + organization_id), valable 60 minutes.\n\nConventions : identifiants UUID, instants ISO 8601 UTC, jours `AAAA-MM-JJ` (Europe/Paris), montants en centimes, listes `{items, nextCursor}`, PATCH « absent = inchangé, null = vidé », `Idempotency-Key` obligatoire sur tout POST, erreurs `{error: {code, message, requestId, details, meta}}`, 300 appels par minute et par connexion (`RateLimit-*`). Liste des routes et droits lus sur le routeur du serveur ; descriptions vérifiées contre des réponses réelles."
  },
  "servers": [
    {
      "url": "https://app.treats.vet",
      "description": "Production"
    },
    {
      "url": "https://preprod.treats.vet",
      "description": "Préproduction (recette)"
    }
  ],
  "components": {
    "securitySchemes": {
      "partner": {
        "type": "oauth2",
        "description": "Jeton d'une application partenaire pour UNE organisation qui l'a connectée.",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "/api/partner/v1/oauth/token",
            "scopes": {
              "agenda:read": "agenda:read",
              "agenda:write": "agenda:write",
              "billing:read": "billing:read",
              "catalog:read": "catalog:read",
              "clients:read": "clients:read",
              "clients:write": "clients:write",
              "consultations:notes": "consultations:notes",
              "consultations:read": "consultations:read",
              "consultations:write": "consultations:write",
              "documents:read": "documents:read",
              "documents:write": "documents:write",
              "imaging:write": "imaging:write",
              "lab:write": "lab:write",
              "org:read": "org:read",
              "patients:read": "patients:read",
              "patients:write": "patients:write",
              "prescriptions:read": "prescriptions:read",
              "webhooks": "webhooks"
            }
          }
        }
      }
    },
    "schemas": {
      "Appointment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du rendez-vous."
          },
          "publicId": {
            "type": "string",
            "description": "Identifiant public lisible (`apt_…`), partageable. Nul pour un rendez-vous qui n'en a pas reçu.",
            "nullable": true,
            "example": "apt_7KQ2M9"
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site du rendez-vous."
          },
          "clientId": {
            "type": "string",
            "format": "uuid",
            "description": "Client (propriétaire). Nul pour un rendez-vous sans client rattaché.",
            "nullable": true
          },
          "patientId": {
            "type": "string",
            "format": "uuid",
            "description": "Animal. Nul pour un rendez-vous sans animal rattaché.",
            "nullable": true
          },
          "vetId": {
            "type": "string",
            "format": "uuid",
            "description": "Vétérinaire du rendez-vous. Nul si aucun n'est désigné.",
            "nullable": true
          },
          "appointmentTypeId": {
            "type": "string",
            "format": "uuid",
            "description": "Type de rendez-vous (voir `GET /appointment-types`). Nul sur certains rendez-vous anciens ou repris d'un autre logiciel.",
            "nullable": true
          },
          "appointmentSubtypeId": {
            "type": "string",
            "format": "uuid",
            "description": "Motif, précision du type (voir `GET /appointment-subtypes`). Nul sans motif.",
            "nullable": true
          },
          "roomId": {
            "type": "string",
            "format": "uuid",
            "description": "Salle réservée. Nulle sans salle.",
            "nullable": true
          },
          "startAt": {
            "type": "string",
            "format": "date-time",
            "description": "Début du rendez-vous."
          },
          "endAt": {
            "type": "string",
            "format": "date-time",
            "description": "Fin du rendez-vous. Nulle sur certains rendez-vous anciens.",
            "nullable": true
          },
          "durationMinutes": {
            "type": "integer",
            "description": "Durée en minutes : la durée enregistrée, sinon l'écart entre `startAt` et `endAt`. Nulle si ni l'une ni l'autre n'est connue.",
            "nullable": true
          },
          "workflow": {
            "type": "string",
            "enum": [
              "PLANNED",
              "CONFIRMED",
              "WAITING",
              "IN_PROGRESS",
              "TO_BILL",
              "COMPLETED"
            ],
            "description": "Avancement de la visite. `PLANNED` : prévu ; `CONFIRMED` : confirmé ; `WAITING` : animal arrivé, en salle d'attente ; `IN_PROGRESS` : en consultation ; `TO_BILL` : visite faite, à facturer ; `COMPLETED` : terminé."
          },
          "category": {
            "type": "string",
            "enum": [
              "EMERGENCY",
              "WALK_IN",
              "HOSPITALIZATION",
              "CANCELLED",
              "NO_SHOW",
              "DECEASED"
            ],
            "description": "Nature particulière du rendez-vous, nulle pour un rendez-vous ordinaire. `EMERGENCY` : urgence ; `WALK_IN` : sans rendez-vous ; `HOSPITALIZATION` : hospitalisation ; `CANCELLED` : annulé ; `NO_SHOW` : non honoré ; `DECEASED` : animal décédé. Les trois dernières valeurs sont définitives : le rendez-vous ne se modifie plus.",
            "nullable": true
          },
          "urgent": {
            "type": "boolean",
            "description": "`true` : marqué urgent par la clinique."
          },
          "note": {
            "type": "string",
            "description": "Note du rendez-vous (motif libre, précisions). Nulle sans note.",
            "nullable": true,
            "maxLength": 5000
          },
          "noteAuthor": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Identifiant de la personne."
              },
              "name": {
                "type": "string",
                "description": "Prénom et nom. Nul si la personne n'est plus lisible.",
                "nullable": true
              }
            },
            "required": [
              "id",
              "name"
            ],
            "description": "Auteur de la dernière modification de la note. Nul sans note, ou si l'auteur n'est pas connu. C'est l'utilisateur d'intégration de l'application quand la note vient de l'API.",
            "nullable": true
          },
          "noteUpdatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de la dernière modification de la note. Nulle sans note.",
            "nullable": true
          },
          "arrivedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Arrivée de l'animal à la clinique. Nulle tant qu'il n'est pas arrivé.",
            "nullable": true
          },
          "calledAt": {
            "type": "string",
            "format": "date-time",
            "description": "Appel de l'animal en consultation. Nul tant qu'il n'a pas été appelé.",
            "nullable": true
          },
          "cancellation": {
            "allOf": [
              {
                "$ref": "#/components/schemas/AppointmentCancellation"
              }
            ],
            "description": "Annulation. Renseignée seulement pour un rendez-vous annulé (`category: CANCELLED`), nulle sinon.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de création. Nulle sur certains rendez-vous repris d'un autre logiciel.",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de la dernière modification. C'est la clé de synchronisation des listes : mémorisez la plus grande valeur vue pour `updated_since`."
          }
        },
        "required": [
          "id",
          "publicId",
          "siteId",
          "clientId",
          "patientId",
          "vetId",
          "appointmentTypeId",
          "appointmentSubtypeId",
          "roomId",
          "startAt",
          "endAt",
          "durationMinutes",
          "workflow",
          "category",
          "urgent",
          "note",
          "noteAuthor",
          "noteUpdatedAt",
          "arrivedAt",
          "calledAt",
          "cancellation",
          "createdAt",
          "updatedAt"
        ],
        "description": "Un rendez-vous de l'agenda d'un site."
      },
      "AppointmentCancellation": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "enum": [
              "CLIENT_REQUEST",
              "VET_UNAVAILABLE",
              "PATIENT_RECOVERED",
              "NO_SHOW",
              "EMERGENCY",
              "DUPLICATE",
              "OTHER"
            ],
            "description": "Motif (voir `GET /cancellation-reasons`). Nul si l'équipe de la clinique a annulé sans motif ; toujours renseigné pour une annulation faite par l'API.",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Précision libre. Nulle sans précision.",
            "nullable": true
          },
          "cancelledAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de l'annulation.",
            "nullable": true
          },
          "clientNotified": {
            "type": "boolean",
            "description": "`true` : le client a été prévenu de l'annulation. Nul si l'information n'a pas été enregistrée.",
            "nullable": true
          }
        },
        "required": [
          "reason",
          "notes",
          "cancelledAt",
          "clientNotified"
        ],
        "description": "L'annulation d'un rendez-vous."
      },
      "AppointmentSubtype": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du motif, à passer en `appointmentSubtypeId`."
          },
          "appointmentTypeId": {
            "type": "string",
            "format": "uuid",
            "description": "Type auquel le motif appartient."
          },
          "name": {
            "type": "string",
            "description": "Nom du motif."
          },
          "durationMinutes": {
            "type": "integer",
            "description": "Durée du motif, en minutes. Une prestation rattachée au motif peut imposer une durée différente à la création du rendez-vous."
          },
          "defaultRoomId": {
            "type": "string",
            "format": "uuid",
            "description": "Salle réservée par défaut pour ce motif quand la création ne précise pas de salle. Nulle sans salle par défaut.",
            "nullable": true
          },
          "species": {
            "type": "string",
            "description": "Code de l'espèce à laquelle le motif est réservé (voir `GET /species`). Nul : motif valable pour toutes les espèces.",
            "nullable": true,
            "example": "DOG"
          }
        },
        "required": [
          "id",
          "appointmentTypeId",
          "name",
          "durationMinutes",
          "defaultRoomId",
          "species"
        ],
        "description": "Un motif de rendez-vous : précision d'un type, avec sa propre durée."
      },
      "AppointmentType": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du type, à passer en `appointmentTypeId`."
          },
          "name": {
            "type": "string",
            "description": "Nom du type, tel que la clinique l'affiche."
          },
          "durationMinutes": {
            "type": "integer",
            "description": "Durée par défaut d'un rendez-vous de ce type, en minutes."
          }
        },
        "required": [
          "id",
          "name",
          "durationMinutes"
        ],
        "description": "Un type de rendez-vous d'un site (consultation, vaccination, chirurgie…)."
      },
      "AvailabilityEvaluation": {
        "type": "object",
        "properties": {
          "available": {
            "type": "boolean",
            "description": "`true` seulement si aucune contrainte n'est violée : la création d'un rendez-vous sur ce créneau serait acceptée à cet instant."
          },
          "startAt": {
            "type": "string",
            "format": "date-time",
            "description": "Début du créneau jugé."
          },
          "endAt": {
            "type": "string",
            "format": "date-time",
            "description": "Fin du créneau jugé, telle que retenue (donnée, calculée depuis la durée, ou depuis le type et le motif)."
          },
          "durationMinutes": {
            "type": "integer",
            "description": "Durée du créneau jugé, en minutes."
          },
          "violations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AvailabilityViolation"
            },
            "description": "Toutes les contraintes non respectées. Vide si `available` vaut `true`."
          }
        },
        "required": [
          "available",
          "startAt",
          "endAt",
          "durationMinutes",
          "violations"
        ],
        "description": "Le jugement d'un créneau."
      },
      "AvailabilitySlot": {
        "type": "object",
        "properties": {
          "vetId": {
            "type": "string",
            "format": "uuid",
            "description": "Vétérinaire libre sur ce créneau, à passer en `vetId` à la création."
          },
          "vetName": {
            "type": "string",
            "description": "Prénom et nom du vétérinaire."
          },
          "startAt": {
            "type": "string",
            "format": "date-time",
            "description": "Début du créneau."
          },
          "endAt": {
            "type": "string",
            "format": "date-time",
            "description": "Fin du créneau."
          },
          "durationMinutes": {
            "type": "integer",
            "description": "Durée du créneau, en minutes."
          }
        },
        "required": [
          "vetId",
          "vetName",
          "startAt",
          "endAt",
          "durationMinutes"
        ],
        "description": "Un créneau libre pour un vétérinaire."
      },
      "AvailabilityViolation": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "enum": [
              "PAST_DATE",
              "HOLIDAY",
              "CLINIC_CLOSED",
              "OUT_OF_BUSINESS_HOURS",
              "VET_OUT_OF_SHIFT",
              "VET_ABSENT",
              "VET_BUSY",
              "VET_EXTERNAL_BUSY",
              "ROOM_BUSY"
            ],
            "description": "`PAST_DATE` : créneau déjà commencé ou passé ; `HOLIDAY` : jour férié ou de fermeture ; `CLINIC_CLOSED` : site fermé ce jour-là ou à cette heure ; `OUT_OF_BUSINESS_HOURS` : hors des heures d'ouverture ; `VET_OUT_OF_SHIFT` : hors du planning du vétérinaire ; `VET_ABSENT` : vétérinaire absent (congé, formation…) ; `VET_BUSY` : vétérinaire déjà pris par un autre rendez-vous ; `VET_EXTERNAL_BUSY` : vétérinaire occupé dans son agenda personnel synchronisé ; `ROOM_BUSY` : salle déjà réservée."
          },
          "message": {
            "type": "string",
            "description": "Explication en français, affichable telle quelle. Ne nomme jamais un autre site de l'organisation."
          }
        },
        "required": [
          "reason",
          "message"
        ],
        "description": "Une contrainte non respectée par un créneau."
      },
      "Breed": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la race. C'est la valeur à envoyer dans `breedId` d'un animal."
          },
          "code": {
            "type": "string",
            "description": "Code de la race, en majuscules."
          },
          "label": {
            "type": "string",
            "description": "Libellé dans la langue de l'organisation, renommé si la clinique l'a renommé."
          },
          "speciesCode": {
            "type": "string",
            "description": "Code de l'espèce de la race."
          },
          "icadCode": {
            "type": "string",
            "description": "Code de la race dans la nomenclature de l'I-CAD (fichier national d'identification), quand il existe.",
            "nullable": true
          }
        },
        "required": [
          "id",
          "code",
          "label",
          "speciesCode",
          "icadCode"
        ],
        "description": "Une race d'une espèce."
      },
      "CallNote": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la note."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site de la note."
          },
          "clientId": {
            "type": "string",
            "format": "uuid",
            "description": "Client qui a appelé."
          },
          "patientId": {
            "type": "string",
            "format": "uuid",
            "description": "Animal concerné. Nul si l'appel ne porte pas sur un animal précis.",
            "nullable": true
          },
          "appointmentId": {
            "type": "string",
            "format": "uuid",
            "description": "Rendez-vous relié à la note (voir `POST /call-notes/{id}/link-appointment`). Nul sans rendez-vous.",
            "nullable": true
          },
          "category": {
            "type": "string",
            "enum": [
              "APPOINTMENT_REQUEST",
              "MEDICAL_QUESTION",
              "COMPLAINT",
              "RESULTS_INQUIRY",
              "OTHER"
            ],
            "description": "Objet de l'appel. `APPOINTMENT_REQUEST` : demande de rendez-vous ; `MEDICAL_QUESTION` : question médicale ; `COMPLAINT` : réclamation ; `RESULTS_INQUIRY` : demande de résultats ; `OTHER` : autre."
          },
          "content": {
            "type": "string",
            "description": "Contenu de la note. Toujours renseigné pour une note créée par l'API.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "IN_PROGRESS",
              "RESOLVED"
            ],
            "description": "`PENDING` : à traiter ; `IN_PROGRESS` : prise en charge par l'équipe ; `RESOLVED` : traitée, la note ne se modifie plus.",
            "nullable": true
          },
          "reminderAt": {
            "type": "string",
            "format": "date-time",
            "description": "Rappel demandé à l'équipe. Nul sans rappel.",
            "nullable": true
          },
          "createdBy": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Identifiant de la personne."
              },
              "name": {
                "type": "string",
                "description": "Prénom et nom. Nul si la personne n'est plus lisible.",
                "nullable": true
              }
            },
            "required": [
              "id",
              "name"
            ],
            "description": "Auteur de la note : l'utilisateur d'intégration de l'application pour une note créée par l'API. Nul si l'auteur n'est pas connu.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de création.",
            "nullable": true
          },
          "resolvedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de traitement. Nulle tant que la note n'est pas traitée.",
            "nullable": true
          },
          "resolvedBy": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Identifiant de la personne."
              },
              "name": {
                "type": "string",
                "description": "Prénom et nom. Nul si la personne n'est plus lisible.",
                "nullable": true
              }
            },
            "required": [
              "id",
              "name"
            ],
            "description": "Personne qui a traité la note. Nulle tant que la note n'est pas traitée.",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de la dernière modification, clé de synchronisation de `GET /call-notes`."
          }
        },
        "required": [
          "id",
          "siteId",
          "clientId",
          "patientId",
          "appointmentId",
          "category",
          "content",
          "status",
          "reminderAt",
          "createdBy",
          "createdAt",
          "resolvedAt",
          "resolvedBy",
          "updatedAt"
        ],
        "description": "Une note d'appel : la trace d'un appel d'un client, à traiter par l'équipe."
      },
      "CancellationReason": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "CLIENT_REQUEST",
              "VET_UNAVAILABLE",
              "PATIENT_RECOVERED",
              "NO_SHOW",
              "EMERGENCY",
              "DUPLICATE",
              "OTHER"
            ],
            "description": "Code à passer en `reason` de `POST /appointments/{id}/cancel`."
          },
          "label": {
            "type": "string",
            "description": "Libellé en français (« Demande du client », « Vétérinaire indisponible », « Patient rétabli », « Client absent », « Urgence », « Doublon », « Autre »)."
          },
          "requiresNotes": {
            "type": "boolean",
            "description": "`true` : une précision (`notes`) est obligatoire avec ce motif. Seul `OTHER` l'exige."
          }
        },
        "required": [
          "code",
          "label",
          "requiresNotes"
        ],
        "description": "Un motif d'annulation."
      },
      "CatalogCategory": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la catégorie."
          },
          "code": {
            "type": "string",
            "description": "Code de la catégorie.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Nom de la catégorie."
          },
          "parentId": {
            "type": "string",
            "format": "uuid",
            "description": "Catégorie parente ; `null` pour une racine.",
            "nullable": true
          },
          "itemType": {
            "type": "string",
            "enum": [
              "PRODUCT",
              "SERVICE"
            ],
            "description": "Type des articles rangés dans la catégorie.",
            "nullable": true
          },
          "active": {
            "type": "boolean",
            "description": "`false` : catégorie désactivée par la clinique."
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CatalogCategory"
            },
            "description": "Sous-catégories, triées comme les racines. Liste vide pour une feuille."
          }
        },
        "description": "Une catégorie du catalogue et ses sous-catégories."
      },
      "CatalogItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de l'article."
          },
          "type": {
            "type": "string",
            "enum": [
              "PRODUCT",
              "SERVICE"
            ],
            "description": "`PRODUCT` : produit (médicament, aliment, accessoire). `SERVICE` : acte."
          },
          "code": {
            "type": "string",
            "description": "Code de l'article dans le catalogue de la clinique.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Nom de l'article."
          },
          "categoryId": {
            "type": "string",
            "format": "uuid",
            "description": "Catégorie de l'article (voir `GET /catalog/categories`).",
            "nullable": true
          },
          "categoryName": {
            "type": "string",
            "description": "Nom de cette catégorie.",
            "nullable": true
          },
          "vatRatePercent": {
            "type": "number",
            "description": "Taux de TVA en pourcentage (`20.00`, `5.50`…).",
            "nullable": true
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site dont le prix est rendu."
          },
          "priceHtCents": {
            "type": "integer",
            "description": "Prix effectif hors taxes pour ce site, en centimes. `null` si aucun prix n'est saisi.",
            "nullable": true
          },
          "priceTtcCents": {
            "type": "integer",
            "description": "Prix effectif toutes taxes comprises pour ce site, en centimes. `null` si aucun prix n'est saisi.",
            "nullable": true
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "description": "Devise des montants, toujours `EUR`."
          },
          "priceScope": {
            "type": "string",
            "enum": [
              "SITE",
              "ORGANIZATION"
            ],
            "description": "Origine du prix : `SITE` si le site a un prix propre pour cet article, `ORGANIZATION` s'il applique le prix commun de l'organisation."
          },
          "gtin": {
            "type": "string",
            "description": "Code GTIN (code-barres) d'un produit.",
            "nullable": true
          },
          "prescriptionOnly": {
            "type": "boolean",
            "description": "`true` : délivrance sur ordonnance seulement."
          },
          "active": {
            "type": "boolean",
            "description": "`false` : article désactivé par la clinique."
          },
          "billable": {
            "type": "boolean",
            "description": "`false` : article qui ne se facture jamais (prescription libre, consigne)."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de synchronisation : dernière modification de l'article ou d'un prix qui le concerne pour ce site, y compris l'entrée en vigueur d'un prix programmé (minuit, heure de Paris, du jour d'effet). Sert de clé à `updated_since` et au curseur."
          }
        },
        "description": "Un article du catalogue de la clinique (acte ou produit), avec le prix effectif du site demandé. Montants en centimes entiers."
      },
      "Client": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du client."
          },
          "publicId": {
            "type": "string",
            "description": "Identifiant public partageable (`cli_…`), affiché dans Treats. Peut être nul sur une fiche à qui aucun n'a encore été attribué.",
            "nullable": true,
            "example": "cli_8K2M4P"
          },
          "organizationId": {
            "type": "string",
            "format": "uuid",
            "description": "Organisation du client."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site d'origine : celui où la fiche a été créée. Le client reste celui de toute l'organisation ; ce site décide de sa visibilité quand l'organisation ne vous a ouvert que certains sites."
          },
          "type": {
            "type": "string",
            "enum": [
              "INDIVIDUAL",
              "PROFESSIONAL"
            ],
            "description": "`INDIVIDUAL` : particulier. `PROFESSIONAL` : éleveur, refuge, entreprise…",
            "nullable": true
          },
          "civility": {
            "type": "string",
            "enum": [
              "MR",
              "MRS",
              "MS",
              "DR"
            ],
            "description": "Civilité : `MR` (Monsieur), `MRS` (Madame), `MS` (Mademoiselle), `DR` (Docteur).",
            "nullable": true
          },
          "firstName": {
            "type": "string",
            "description": "Prénom. Toujours rempli pour un client créé par l'API ; peut manquer sur une fiche reprise d'un ancien logiciel.",
            "nullable": true
          },
          "lastName": {
            "type": "string",
            "description": "Nom."
          },
          "companyName": {
            "type": "string",
            "description": "Raison sociale.",
            "nullable": true
          },
          "siren": {
            "type": "string",
            "description": "Numéro SIREN, en lecture seule (saisi par la clinique).",
            "nullable": true
          },
          "email": {
            "type": "string",
            "description": "Adresse email.",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "description": "Téléphone principal, tel qu'il a été saisi. Unique dans l'organisation.",
            "nullable": true
          },
          "mobilePhone": {
            "type": "string",
            "description": "Téléphone mobile, tel qu'il a été saisi.",
            "nullable": true
          },
          "address": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ClientAddress"
              }
            ],
            "description": "Adresse postale. L'objet est toujours présent ; ses champs sont nuls quand l'adresse n'est pas renseignée."
          },
          "birthDate": {
            "type": "string",
            "format": "date",
            "description": "Date de naissance du client.",
            "nullable": true
          },
          "consents": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ClientConsents"
              }
            ],
            "description": "Accords de contact donnés par le client."
          },
          "preferredChannel": {
            "type": "string",
            "enum": [
              "EMAIL",
              "SMS",
              "PHONE"
            ],
            "description": "Canal préféré pour les envois manuels de la clinique.",
            "nullable": true
          },
          "active": {
            "type": "boolean",
            "description": "`false` : fiche désactivée par la clinique (plus aucune communication)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de création de la fiche. Peut être nulle sur une fiche reprise d'un ancien logiciel.",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Clé de synchronisation, à la microseconde : date de dernière modification, à défaut date de création. C'est la valeur que lit `updated_since` et qui ordonne la liste."
          }
        },
        "required": [
          "id",
          "organizationId",
          "siteId",
          "lastName",
          "address",
          "consents",
          "active",
          "updatedAt"
        ],
        "description": "Un client (propriétaire) de l'organisation. Ne contient ni notes internes, ni typologie tarifaire, ni vétérinaire traitant."
      },
      "ClientAddress": {
        "type": "object",
        "properties": {
          "line": {
            "type": "string",
            "description": "Adresse composée sur une ligne, telle qu'elle s'imprime sur les documents. Lecture seule : recomposée à chaque modification des autres champs.",
            "nullable": true
          },
          "street": {
            "type": "string",
            "description": "Voie (numéro et rue).",
            "nullable": true
          },
          "postalCode": {
            "type": "string",
            "description": "Code postal.",
            "nullable": true
          },
          "city": {
            "type": "string",
            "description": "Ville.",
            "nullable": true
          },
          "country": {
            "type": "string",
            "description": "Pays.",
            "nullable": true
          }
        },
        "description": "Adresse postale d'un client."
      },
      "ClientConsents": {
        "type": "object",
        "properties": {
          "email": {
            "type": "boolean",
            "description": "Le client accepte d'être contacté par email."
          },
          "sms": {
            "type": "boolean",
            "description": "Le client accepte d'être contacté par SMS."
          },
          "mail": {
            "type": "boolean",
            "description": "Le client accepte d'être contacté par courrier."
          }
        },
        "required": [
          "email",
          "sms",
          "mail"
        ],
        "description": "Accords de contact du client. Un accord se recueille par la clinique ou par le propriétaire lui-même, jamais par l'API ; chaque changement est journalisé par Treats."
      },
      "ClientDuplicateCandidate": {
        "type": "object",
        "properties": {
          "clientId": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du client existant (à lire par `GET /clients/{id}`)."
          },
          "publicId": {
            "type": "string",
            "description": "Identifiant public (`cli_…`).",
            "nullable": true
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site d'origine du client existant."
          },
          "firstName": {
            "type": "string",
            "description": "Prénom du client existant.",
            "nullable": true
          },
          "lastName": {
            "type": "string",
            "description": "Nom du client existant."
          },
          "match": {
            "type": "string",
            "enum": [
              "PHONE",
              "NAME"
            ],
            "description": "`PHONE` : même téléphone principal. `NAME` : même nom et même prénom, casse et accents ignorés. Une fiche qui correspond aux deux n'apparaît qu'une fois, en `PHONE`."
          }
        },
        "required": [
          "clientId",
          "siteId",
          "lastName",
          "match"
        ],
        "description": "Une fiche existante proche de celle que vous voulez créer."
      },
      "ClientDuplicateCheck": {
        "type": "object",
        "properties": {
          "phoneAvailable": {
            "type": "boolean",
            "description": "`false` : un client de l'organisation porte déjà ce téléphone principal, et `POST /clients` le refuserait (`409 CLIENT_DUPLICATE`). Vaut aussi `false` quand cette fiche est sur un site qui ne vous est pas ouvert, sans rien en dire de plus. `true` : téléphone libre. `null` : aucun téléphone demandé.",
            "nullable": true
          },
          "candidates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClientDuplicateCandidate"
            },
            "description": "Fiches proches que vous pouvez voir, d'abord celles au même téléphone puis celles au même nom. Une fiche d'un site qui ne vous est pas ouvert n'y figure jamais. Liste vide si aucune."
          }
        },
        "required": [
          "candidates"
        ],
        "description": "Résultat d'un contrôle de doublon avant création."
      },
      "Consultation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la consultation."
          },
          "publicId": {
            "type": "string",
            "description": "Identifiant public partageable, préfixé `cns_`.",
            "nullable": true,
            "example": "cns_8K2M4Q"
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site de la consultation."
          },
          "patientId": {
            "type": "string",
            "format": "uuid",
            "description": "Animal examiné."
          },
          "appointmentId": {
            "type": "string",
            "format": "uuid",
            "description": "Rendez-vous d'origine. Nul pour une consultation ouverte sans rendez-vous.",
            "nullable": true
          },
          "vetId": {
            "type": "string",
            "format": "uuid",
            "description": "Membre de l'équipe qui porte la consultation (voir `GET /staff`).",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "DRAFT",
              "VALIDATED"
            ],
            "description": "`DRAFT` : brouillon, modifiable. `VALIDATED` : validée par le vétérinaire, plus rien ne s'y écrit par l'API sauf une note d'équipe."
          },
          "category": {
            "type": "string",
            "enum": [
              "CONSULTATION",
              "EMERGENCY",
              "FOLLOW_UP",
              "SURGERY",
              "HOSPITALIZATION",
              "ANALYSIS",
              "PREVENTION",
              "TECHNICAL_ACT",
              "ADMINISTRATIVE",
              "EUTHANASIA"
            ],
            "description": "Type d'événement : consultation, urgence, suivi, chirurgie, hospitalisation, analyse, prévention, acte technique, administratif, euthanasie. Déduit du modèle de consultation.",
            "nullable": true
          },
          "reason": {
            "type": "string",
            "description": "Motif affiché de la consultation.",
            "nullable": true
          },
          "motifId": {
            "type": "string",
            "format": "uuid",
            "description": "Motif du référentiel de l'organisation, s'il a été choisi.",
            "nullable": true
          },
          "templateSlug": {
            "type": "string",
            "description": "Modèle de consultation utilisé à l'ouverture.",
            "nullable": true
          },
          "startedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Début de la consultation.",
            "nullable": true
          },
          "validatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de validation. Nulle sur un brouillon.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de création."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Clé de synchronisation, la même que dans la liste : dernière modification de l'en-tête ou d'un module.",
            "nullable": true
          },
          "ownerReport": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ConsultationOwnerReport"
              }
            ],
            "description": "Synthèse remise au propriétaire, posée par le vétérinaire à la validation. Nulle tant qu'aucune de ses trois parties n'est remplie (toujours nulle sur un brouillon ouvert par l'API).",
            "nullable": true
          }
        },
        "required": [
          "id",
          "publicId",
          "siteId",
          "patientId",
          "appointmentId",
          "vetId",
          "status",
          "category",
          "reason",
          "motifId",
          "templateSlug",
          "startedAt",
          "validatedAt",
          "createdAt",
          "updatedAt",
          "ownerReport"
        ],
        "description": "Une consultation : l'en-tête, son type d'événement et la synthèse remise au propriétaire."
      },
      "ConsultationListItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la consultation."
          },
          "publicId": {
            "type": "string",
            "description": "Identifiant public partageable, préfixé `cns_`.",
            "nullable": true,
            "example": "cns_8K2M4Q"
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site de la consultation."
          },
          "patientId": {
            "type": "string",
            "format": "uuid",
            "description": "Animal examiné."
          },
          "appointmentId": {
            "type": "string",
            "format": "uuid",
            "description": "Rendez-vous d'origine. Nul pour une consultation ouverte sans rendez-vous.",
            "nullable": true
          },
          "vetId": {
            "type": "string",
            "format": "uuid",
            "description": "Membre de l'équipe qui porte la consultation (voir `GET /staff`).",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "DRAFT",
              "VALIDATED"
            ],
            "description": "`DRAFT` : brouillon, modifiable. `VALIDATED` : validée par le vétérinaire, plus rien ne s'y écrit par l'API sauf une note d'équipe."
          },
          "reason": {
            "type": "string",
            "description": "Motif affiché de la consultation.",
            "nullable": true
          },
          "motifId": {
            "type": "string",
            "format": "uuid",
            "description": "Motif du référentiel de l'organisation, s'il a été choisi.",
            "nullable": true
          },
          "templateSlug": {
            "type": "string",
            "description": "Modèle de consultation utilisé à l'ouverture.",
            "nullable": true
          },
          "startedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Début de la consultation.",
            "nullable": true
          },
          "validatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de validation. Nulle sur un brouillon.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de création."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Clé de synchronisation : dernière modification de l'en-tête ou d'un module. Pour une consultation jamais modifiée depuis l'ouverture de l'API, la plus récente de sa création, de son début et de sa validation."
          }
        },
        "required": [
          "id",
          "publicId",
          "siteId",
          "patientId",
          "appointmentId",
          "vetId",
          "status",
          "reason",
          "motifId",
          "templateSlug",
          "startedAt",
          "validatedAt",
          "createdAt",
          "updatedAt"
        ],
        "description": "Une consultation dans une liste : l'en-tête, sans le type d'événement ni la synthèse remise au propriétaire (lire `GET /consultations/{id}` pour les avoir)."
      },
      "ConsultationModule": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du module dans cette consultation."
          },
          "slug": {
            "type": "string",
            "description": "Identifiant stable du module (`anamnese`, `examen-general`, `soins`…). C'est la valeur de `{slug}` dans `PUT /consultations/{id}/modules/{slug}`.",
            "nullable": true,
            "example": "anamnese"
          },
          "name": {
            "type": "string",
            "description": "Nom du module, tel que la clinique le voit.",
            "nullable": true
          },
          "category": {
            "type": "string",
            "description": "Famille du module, en minuscules : `history`, `examination`, `diagnosis`, `procedure`, `treatment`, `prevention`, `vitals`, `lab`, `imaging`, `nutrition`, `billing`, `files`, `administrative`.",
            "nullable": true
          },
          "position": {
            "type": "integer",
            "description": "Rang du module dans la consultation (0 si non renseigné)."
          },
          "writable": {
            "type": "boolean",
            "description": "`true` : module écrivable par l'API (anamnese, commemoratifs, examen-general, diagnostic, comportement), `data` suit son contrat. `false` : lecture seule, `data` est nul et seul `text` est rendu."
          },
          "data": {
            "type": "object",
            "description": "Contenu du module selon son contrat publié (`GET /module-definitions/{slug}`), pour un module écrivable : tous les champs du contrat, à `null` quand rien n'est saisi. Formes par module : `ModuleDataAnamnese`, `ModuleDataCommemoratifs`, `ModuleDataExamenGeneral`, `ModuleDataDiagnostic`, `ModuleDataComportement`. Nul pour un module en lecture seule. Une valeur reprise d'un ancien logiciel hors de la liste d'un champ à valeurs fixes est rendue telle quelle.",
            "nullable": true
          },
          "text": {
            "type": "string",
            "description": "Rendu lisible du module, une ligne par constat (séparées par `\\n`), le même que le compte rendu de consultation, valeurs traduites en français. Jamais de prix. Nul pour un module vide, pour la note interne et pour l'anesthésie (que le compte rendu n'imprime pas).",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Dernière modification du module.",
            "nullable": true
          }
        },
        "required": [
          "id",
          "slug",
          "name",
          "category",
          "position",
          "writable",
          "data",
          "text",
          "updatedAt"
        ],
        "description": "Un module de la consultation, dans l'ordre de l'écran."
      },
      "ConsultationNote": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la note."
          },
          "consultationId": {
            "type": "string",
            "format": "uuid",
            "description": "Consultation de la note."
          },
          "content": {
            "type": "string",
            "description": "Texte de la note."
          },
          "authorUserId": {
            "type": "string",
            "format": "uuid",
            "description": "Auteur : un membre de l'équipe, ou le compte d'intégration de l'application pour une note écrite par l'API.",
            "nullable": true
          },
          "createdByThisApplication": {
            "type": "boolean",
            "description": "`true` : note écrite par votre application."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date d'écriture.",
            "nullable": true
          }
        },
        "required": [
          "id",
          "consultationId",
          "content",
          "authorUserId",
          "createdByThisApplication",
          "createdAt"
        ],
        "description": "Une note d'équipe d'une consultation. Jamais une note confidentielle."
      },
      "ConsultationOwnerReport": {
        "type": "object",
        "properties": {
          "summary": {
            "type": "string",
            "description": "Résumé de la consultation.",
            "nullable": true
          },
          "diagnosis": {
            "type": "string",
            "description": "Diagnostic, formulé pour le propriétaire.",
            "nullable": true
          },
          "instructions": {
            "type": "string",
            "description": "Consignes au propriétaire.",
            "nullable": true
          }
        },
        "required": [
          "summary",
          "diagnosis",
          "instructions"
        ],
        "description": "Synthèse pour le propriétaire : ce que la clinique lui remet, pas le dossier médical."
      },
      "CreditNote": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de l'avoir."
          },
          "number": {
            "type": "string",
            "description": "Numéro de l'avoir."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site émetteur."
          },
          "invoiceId": {
            "type": "string",
            "format": "uuid",
            "description": "Facture corrigée par l'avoir."
          },
          "invoiceNumber": {
            "type": "string",
            "description": "Numéro de cette facture."
          },
          "clientId": {
            "type": "string",
            "format": "uuid",
            "description": "Client concerné."
          },
          "type": {
            "type": "string",
            "enum": [
              "FULL",
              "PARTIAL"
            ],
            "description": "`FULL` : avoir sur toute la facture. `PARTIAL` : sur une partie."
          },
          "reason": {
            "type": "string",
            "enum": [
              "CLIENT_ERROR",
              "DUPLICATE",
              "SERVICE_NOT_PROVIDED",
              "PRICING_ERROR",
              "OTHER"
            ],
            "description": "Motif : erreur de client, doublon, prestation non réalisée, erreur de prix, autre."
          },
          "reasonDetails": {
            "type": "string",
            "description": "Précision saisie par la clinique.",
            "nullable": true
          },
          "totalHtCents": {
            "type": "integer",
            "description": "Total hors taxes de l'avoir, en centimes."
          },
          "totalVatCents": {
            "type": "integer",
            "description": "TVA de l'avoir, en centimes."
          },
          "totalTtcCents": {
            "type": "integer",
            "description": "Total toutes taxes comprises de l'avoir, en centimes."
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "description": "Devise des montants, toujours `EUR`."
          },
          "issuedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date d'émission de l'avoir."
          },
          "amountDueToClientCents": {
            "type": "integer",
            "description": "Au détail : ce que la clinique doit encore rendre au client au titre de cet avoir (trop-perçu non restitué), en centimes ; 0 une fois restitué ou si rien n'avait été payé en trop. `null` en liste.",
            "nullable": true
          },
          "settlement": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CreditNoteSettlement"
              }
            ],
            "description": "Au détail : la restitution faite au client. `null` tant qu'elle n'est pas faite, et toujours `null` en liste.",
            "nullable": true
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreditNoteLine"
            },
            "description": "Au détail : les lignes de l'avoir. `null` en liste.",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de synchronisation : la date d'émission (un avoir ne se modifie pas). Sert de clé à `updated_since` et au curseur."
          }
        },
        "description": "Un avoir. Un avoir émis ne change plus. Montants positifs : ils se retranchent de la facture d'origine."
      },
      "CreditNoteLine": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la ligne."
          },
          "invoiceLineId": {
            "type": "string",
            "format": "uuid",
            "description": "Ligne de la facture d'origine que celle-ci corrige.",
            "nullable": true
          },
          "label": {
            "type": "string",
            "description": "Libellé."
          },
          "quantity": {
            "type": "number",
            "description": "Quantité."
          },
          "unitPriceHtCents": {
            "type": "integer",
            "description": "Prix unitaire hors taxes, en centimes."
          },
          "vatRatePercent": {
            "type": "number",
            "description": "Taux de TVA, en pourcentage."
          },
          "totalHtCents": {
            "type": "integer",
            "description": "Total hors taxes de la ligne, en centimes."
          }
        },
        "description": "Une ligne d'avoir."
      },
      "CreditNoteSettlement": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "REFUND",
              "DEPOSIT"
            ],
            "description": "`REFUND` : remboursé au client. `DEPOSIT` : porté en acompte sur son compte client."
          },
          "method": {
            "type": "string",
            "enum": [
              "CARD",
              "CARD_CONTACTLESS",
              "CASH",
              "CHECK",
              "TRANSFER",
              "CLIENT_ACCOUNT",
              "ALMA",
              "AVOIR",
              "CERTIFICATE_UNRECOVERABLE",
              "CONTENTIOUS",
              "ONEY_BANK",
              "OXANE",
              "PAYVET",
              "LOSS",
              "PROFIT_AND_LOSS",
              "FLOA"
            ],
            "description": "Moyen du remboursement (voir `Payment.method`). `null` pour un acompte.",
            "nullable": true
          },
          "amountCents": {
            "type": "integer",
            "description": "Montant restitué, en centimes."
          },
          "settledAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de la restitution."
          },
          "paymentId": {
            "type": "string",
            "format": "uuid",
            "description": "Pour un remboursement : le mouvement négatif correspondant dans `GET /payments` (`kind` = `CREDIT_NOTE_REFUND`). `null` pour un acompte.",
            "nullable": true
          }
        },
        "description": "La restitution d'un avoir au client."
      },
      "Document": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du document."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site du document."
          },
          "patientId": {
            "type": "string",
            "format": "uuid",
            "description": "Animal du document. Nul pour un document du seul dossier client.",
            "nullable": true
          },
          "consultationId": {
            "type": "string",
            "format": "uuid",
            "description": "Consultation à laquelle le document est rattaché, s'il l'est.",
            "nullable": true
          },
          "type": {
            "type": "string",
            "enum": [
              "SUMMARY",
              "PRESCRIPTION",
              "INVOICE",
              "CERTIFICATE",
              "MEDICAL_RECORD",
              "VACCINATION_CARD",
              "QUOTE",
              "CONSENT_FORM",
              "DELIVERY_NOTE",
              "UPLOAD",
              "CREDIT_NOTE",
              "HOSPITALIZATION_REPORT",
              "CONSULTATION_REPORT"
            ],
            "description": "Nature : synthèse, ordonnance, facture, certificat, dossier médical, carnet de vaccination, devis, consentement, bon de livraison, fichier déposé (`UPLOAD`, dont tout dépôt par l'API), avoir, compte rendu d'hospitalisation, compte rendu de consultation."
          },
          "category": {
            "type": "string",
            "enum": [
              "IMAGERIE",
              "CHIRURGIE",
              "CERTIFICATS",
              "CONSENTEMENTS",
              "INFORMATION",
              "RESULTATS_LABO",
              "ADMINISTRATIF",
              "AUTRE"
            ],
            "description": "Rubrique du dossier : imagerie, chirurgie, certificats, consentements, information, résultats de laboratoire, administratif, autre.",
            "nullable": true
          },
          "title": {
            "type": "string",
            "description": "Titre du document.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "GENERATED",
              "DRAFT",
              "VALIDATED",
              "SIGNED",
              "SENT"
            ],
            "description": "`GENERATED` : produit ou déposé, sans circuit de validation (tout fichier déposé). `DRAFT` : brouillon. `VALIDATED` : validé. `SIGNED` : signé. `SENT` : envoyé."
          },
          "visibility": {
            "type": "string",
            "enum": [
              "INTERNAL",
              "OWNER",
              "ASV_ONLY"
            ],
            "description": "`INTERNAL` : réservé à l'équipe (tout dépôt par l'API). `OWNER` : montré au propriétaire. `ASV_ONLY` : réservé aux assistants."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de création ou de dépôt."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Dernière modification.",
            "nullable": true
          },
          "validatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de validation, s'il a été validé.",
            "nullable": true
          },
          "signedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de signature, s'il a été signé.",
            "nullable": true
          }
        },
        "required": [
          "id",
          "siteId",
          "patientId",
          "consultationId",
          "type",
          "category",
          "title",
          "status",
          "visibility",
          "createdAt",
          "updatedAt",
          "validatedAt",
          "signedAt"
        ],
        "description": "Les métadonnées d'un document du dossier. Le contenu se télécharge par `GET /documents/{id}/download`."
      },
      "DocumentDownload": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Adresse du contenu. `requiresAuthorization: false` : adresse pré-signée du stockage, à suivre sans en-tête d'autorisation. `requiresAuthorization: true` : la route `GET /api/partner/v1/documents/{id}/content`, à appeler avec le jeton."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "Fin de validité de l'adresse pré-signée (15 minutes après l'appel). Nulle pour la route de l'API, qui ne périme pas.",
            "nullable": true
          },
          "requiresAuthorization": {
            "type": "boolean",
            "description": "`true` : passer `Authorization: Bearer …` en suivant `url`. `false` : ne PAS le passer (adresse pré-signée)."
          }
        },
        "required": [
          "url",
          "expiresAt",
          "requiresAuthorization"
        ],
        "description": "Où télécharger le contenu d'un document."
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Code stable, en majuscules (`CLIENT_NOT_FOUND`, `SLOT_CONFLICT`, `INVALID_FIELD`…). Le tester, jamais le message.",
                "example": "INVALID_FIELD"
              },
              "message": {
                "type": "string",
                "description": "Explication en français, lisible par l'utilisateur final."
              },
              "requestId": {
                "type": "string",
                "description": "Identifiant de l'appel, à communiquer au support Treats. Aussi rendu dans l'en-tête `X-Request-ID`."
              },
              "details": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "field": {
                      "type": "string",
                      "description": "Champ en cause, avec son chemin pour un objet imbriqué (`address.city`)."
                    },
                    "message": {
                      "type": "string",
                      "description": "Pourquoi ce champ est refusé."
                    }
                  }
                },
                "description": "Un détail par champ refusé, sur les erreurs de validation."
              },
              "meta": {
                "type": "object",
                "description": "Informations propres à l'erreur (identifiant d'un doublon, créneau en conflit…)."
              }
            },
            "required": [
              "code",
              "message",
              "requestId"
            ],
            "description": "L'erreur."
          }
        },
        "required": [
          "error"
        ],
        "description": "Enveloppe de toute erreur de l'API (sauf la demande de jeton, au format OAuth). Le message est en français et peut être affiché tel quel ; le code est stable et se teste."
      },
      "HistoryEntry": {
        "type": "object",
        "properties": {
          "consultationId": {
            "type": "string",
            "format": "uuid",
            "description": "Consultation (à lire par `GET /consultations/{id}`)."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site de la consultation, toujours un site ouvert à l'application."
          },
          "date": {
            "type": "string",
            "format": "date-time",
            "description": "Date de la consultation."
          },
          "category": {
            "type": "string",
            "enum": [
              "CONSULTATION",
              "EMERGENCY",
              "FOLLOW_UP",
              "SURGERY",
              "HOSPITALIZATION",
              "ANALYSIS",
              "PREVENTION",
              "TECHNICAL_ACT",
              "ADMINISTRATIVE",
              "EUTHANASIA"
            ],
            "description": "Type d'événement : consultation, urgence, suivi, chirurgie, hospitalisation, analyse, prévention, acte technique, administratif, euthanasie."
          },
          "title": {
            "type": "string",
            "description": "Titre affiché dans l'historique de la clinique.",
            "nullable": true
          },
          "reason": {
            "type": "string",
            "description": "Motif de la consultation.",
            "nullable": true
          },
          "vetName": {
            "type": "string",
            "description": "Nom du praticien.",
            "nullable": true
          },
          "validated": {
            "type": "boolean",
            "description": "`true` : consultation validée."
          },
          "hasDocuments": {
            "type": "boolean",
            "description": "`true` : au moins un document est rattaché à la consultation."
          },
          "hasPrescription": {
            "type": "boolean",
            "description": "`true` : au moins une ordonnance est rattachée à la consultation."
          }
        },
        "required": [
          "consultationId",
          "siteId",
          "date",
          "category",
          "title",
          "reason",
          "vetName",
          "validated",
          "hasDocuments",
          "hasPrescription"
        ],
        "description": "Une consultation dans l'historique d'un animal, résumée."
      },
      "ImagingExam": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la demande, transmis au fournisseur quand Treats lui pousse la demande. C'est l'identifiant du chemin de dépôt des résultats."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site de la clinique qui a demandé l'examen."
          },
          "provider": {
            "type": "string",
            "enum": [
              "ANIMA_CARE",
              "IDEXX",
              "VETERNITY",
              "CENTRAVET",
              "ICAD",
              "NEPHTIS",
              "PENNYLANE",
              "PICOXIA",
              "MYPACS"
            ],
            "description": "Fournisseur à qui la demande est adressée (l'un de ceux que représente l'application)."
          },
          "status": {
            "type": "string",
            "enum": [
              "REQUESTED",
              "RECEIVED",
              "CANCELLED"
            ],
            "description": "`REQUESTED` : résultats attendus (demande créée, transmise, ou dont la transmission a échoué). `RECEIVED` : des résultats ont été reçus. `CANCELLED` : annulée par la clinique, plus aucun dépôt accepté."
          },
          "modality": {
            "type": "string",
            "enum": [
              "RADIO",
              "ECHO",
              "SCANNER",
              "IRM",
              "ENDOSCOPIE",
              "AUTRE"
            ],
            "description": "Type d'examen : radiographie, échographie, scanner, IRM, endoscopie, autre.",
            "nullable": true
          },
          "accessionNumber": {
            "type": "string",
            "description": "Numéro d'accession DICOM de la demande (liste de travail de l'appareil).",
            "nullable": true
          },
          "examAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de l'examen.",
            "nullable": true
          },
          "requestedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de la demande."
          },
          "receivedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de la première réception de résultats ; `null` tant que rien n'est reçu.",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Dernière modification ; clé de synchronisation des listes."
          },
          "veterinarian": {
            "type": "string",
            "description": "Vétérinaire demandeur : prénom et nom, précédés de « Dr » pour un vétérinaire.",
            "nullable": true
          },
          "patient": {
            "allOf": [
              {
                "$ref": "#/components/schemas/LabExamPatient"
              }
            ],
            "description": "L'animal à examiner ; `null` s'il n'est plus lisible.",
            "nullable": true
          },
          "results": {
            "type": "object",
            "properties": {
              "imageCount": {
                "type": "integer",
                "description": "Nombre d'images reçues (tous canaux confondus)."
              },
              "reportReceived": {
                "type": "boolean",
                "description": "Un compte rendu PDF a été reçu."
              },
              "studyLink": {
                "type": "string",
                "description": "Lien vers l'étude chez le fournisseur (visionneuse), s'il en a envoyé un.",
                "nullable": true
              }
            },
            "description": "Ce que Treats a déjà reçu pour cet examen."
          }
        },
        "description": "Une demande d'examen d'imagerie adressée par une clinique au fournisseur que représente l'application."
      },
      "ImagingResultsReceipt": {
        "type": "object",
        "properties": {
          "examId": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la demande."
          },
          "status": {
            "type": "string",
            "enum": [
              "REQUESTED",
              "RECEIVED",
              "CANCELLED"
            ],
            "description": "Statut de la demande après le dépôt (`RECEIVED` dès qu'un fichier a été reçu)."
          },
          "imageCount": {
            "type": "integer",
            "description": "Nombre total d'images reçues pour l'examen."
          },
          "reportReceived": {
            "type": "boolean",
            "description": "Un compte rendu PDF a été reçu pour l'examen, par ce dépôt ou un précédent."
          },
          "alreadyReceived": {
            "type": "boolean",
            "description": "`true` : tous les fichiers envoyés l'avaient déjà été, rien n'a été ajouté ni modifié."
          }
        },
        "description": "Accusé de réception d'un dépôt de résultats d'imagerie."
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la facture."
          },
          "number": {
            "type": "string",
            "description": "Numéro de la facture."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site émetteur."
          },
          "clientId": {
            "type": "string",
            "format": "uuid",
            "description": "Client facturé."
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "PARTIAL",
              "PAID",
              "CANCELLED"
            ],
            "description": "Statut, comme en liste."
          },
          "issuedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date d'émission (validation) ; à défaut, date de création."
          },
          "dueDate": {
            "type": "string",
            "format": "date",
            "description": "Date d'échéance.",
            "nullable": true
          },
          "patientIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Animaux concernés : celui de l'en-tête puis ceux des lignes (encaissement groupé de plusieurs animaux). Liste vide pour une vente au comptoir."
          },
          "consultationIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Consultations liées à la facture (en-tête et lignes). Liste vide si aucune."
          },
          "globalDiscountPercent": {
            "type": "number",
            "description": "Remise globale de la facture, en pourcentage. `null` s'il n'y en a pas.",
            "nullable": true
          },
          "subtotalHtCents": {
            "type": "integer",
            "description": "Total hors taxes des lignes avant toute remise, en centimes."
          },
          "discountHtCents": {
            "type": "integer",
            "description": "Total des remises (remises de ligne et remise globale), hors taxes, en centimes."
          },
          "totalHtCents": {
            "type": "integer",
            "description": "Total hors taxes, en centimes. Égal à la somme des bases de `vatBreakdown`."
          },
          "totalVatCents": {
            "type": "integer",
            "description": "Total de TVA, en centimes. Égal à la somme des TVA de `vatBreakdown`."
          },
          "totalTtcCents": {
            "type": "integer",
            "description": "Total toutes taxes comprises, en centimes."
          },
          "paidCents": {
            "type": "integer",
            "description": "Montant déjà encaissé, en centimes."
          },
          "creditedCents": {
            "type": "integer",
            "description": "Somme TTC des avoirs émis sur cette facture, en centimes."
          },
          "balanceDueCents": {
            "type": "integer",
            "description": "Reste à payer, en centimes, jamais négatif."
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "description": "Devise des montants, toujours `EUR`."
          },
          "vatBreakdown": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceVatLine"
            },
            "description": "Ventilation de la TVA par taux : c'est elle qui fait foi."
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceLine"
            },
            "description": "Lignes telles qu'imprimées sur la facture remise au client. Les lignes internes ou incluses masquées n'y figurent pas."
          },
          "cancelledAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date d'annulation, pour une facture annulée.",
            "nullable": true
          },
          "replacedByInvoiceId": {
            "type": "string",
            "format": "uuid",
            "description": "Facture qui remplace celle-ci, quand elle a été annulée puis refaite.",
            "nullable": true
          },
          "replacesInvoiceId": {
            "type": "string",
            "format": "uuid",
            "description": "Facture annulée que celle-ci remplace.",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de synchronisation, comme en liste."
          }
        },
        "description": "Le détail d'une facture émise : en-tête, lignes imprimées, ventilation de TVA et totaux."
      },
      "InvoiceLine": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la ligne."
          },
          "label": {
            "type": "string",
            "description": "Libellé imprimé sur la facture."
          },
          "type": {
            "type": "string",
            "enum": [
              "ACT",
              "PRODUCT",
              "SERVICE"
            ],
            "description": "`ACT` : acte médical. `PRODUCT` : produit vendu. `SERVICE` : autre prestation.",
            "nullable": true
          },
          "catalogItemId": {
            "type": "string",
            "format": "uuid",
            "description": "Article du catalogue facturé (voir `GET /catalog/items/{id}`). `null` pour une ligne libre.",
            "nullable": true
          },
          "quantity": {
            "type": "number",
            "description": "Quantité."
          },
          "unit": {
            "type": "string",
            "description": "Unité de vente (comprimé, boîte…).",
            "nullable": true
          },
          "unitPriceHtCents": {
            "type": "integer",
            "description": "Prix unitaire hors taxes, arrondi au centime (le prix saisi peut porter quatre décimales). Pour recompter, partir des totaux de ligne, pas de ce prix.",
            "nullable": true
          },
          "unitPriceTtcCents": {
            "type": "integer",
            "description": "Prix unitaire toutes taxes comprises, en centimes.",
            "nullable": true
          },
          "vatRatePercent": {
            "type": "number",
            "description": "Taux de TVA de la ligne, en pourcentage.",
            "nullable": true
          },
          "discountPercent": {
            "type": "number",
            "description": "Remise de la ligne, en pourcentage.",
            "nullable": true
          },
          "totalHtCents": {
            "type": "integer",
            "description": "Total hors taxes de la ligne après sa remise, en centimes.",
            "nullable": true
          },
          "totalVatCents": {
            "type": "integer",
            "description": "TVA de la ligne, en centimes.",
            "nullable": true
          },
          "totalTtcCents": {
            "type": "integer",
            "description": "Total toutes taxes comprises de la ligne tel qu'il se lit sur la facture. Sans remise globale, la somme des lignes égale `totalTtcCents` de la facture au centime ; avec une remise globale, les lignes sont rendues avant elle et leur somme dépasse le total du montant de la remise. Sur une ligne, `totalHtCents + totalVatCents` peut différer d'un centime de ce montant.",
            "nullable": true
          },
          "patientId": {
            "type": "string",
            "format": "uuid",
            "description": "Animal concerné par la ligne.",
            "nullable": true
          },
          "consultationId": {
            "type": "string",
            "format": "uuid",
            "description": "Consultation d'où vient la ligne.",
            "nullable": true
          }
        },
        "description": "Une ligne imprimée d'une facture."
      },
      "InvoiceSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la facture."
          },
          "number": {
            "type": "string",
            "description": "Numéro de la facture (numérotation légale de la clinique)."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site émetteur."
          },
          "clientId": {
            "type": "string",
            "format": "uuid",
            "description": "Client facturé."
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "PARTIAL",
              "PAID",
              "CANCELLED"
            ],
            "description": "`PENDING` : à régler. `PARTIAL` : réglée en partie. `PAID` : soldée. `CANCELLED` : annulée. Une facture réglée puis créditée garde `PAID` : `creditedCents` égal à `totalTtcCents` dit qu'elle est annulée par avoir."
          },
          "issuedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date d'émission (validation). Pour une facture reprise d'un ancien logiciel sans date de validation : sa date de création."
          },
          "dueDate": {
            "type": "string",
            "format": "date",
            "description": "Date d'échéance.",
            "nullable": true
          },
          "totalHtCents": {
            "type": "integer",
            "description": "Total hors taxes, en centimes."
          },
          "totalVatCents": {
            "type": "integer",
            "description": "Total de TVA, en centimes."
          },
          "totalTtcCents": {
            "type": "integer",
            "description": "Total toutes taxes comprises, en centimes : ce que le client doit payer. Sur quelques factures anciennes, `totalHtCents + totalVatCents` en diffère d'un centime ; les montants sont rendus tels qu'émis."
          },
          "paidCents": {
            "type": "integer",
            "description": "Montant déjà encaissé, en centimes."
          },
          "creditedCents": {
            "type": "integer",
            "description": "Somme TTC des avoirs émis sur cette facture, en centimes."
          },
          "balanceDueCents": {
            "type": "integer",
            "description": "Reste à payer, en centimes : TTC moins avoirs moins encaissé, jamais négatif."
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "description": "Devise des montants, toujours `EUR`."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de synchronisation : dernière modification de la facture (règlement, avoir, annulation…), à défaut sa création. Sert de clé à `updated_since` et au curseur."
          }
        },
        "description": "Une facture émise, telle que rendue en liste."
      },
      "InvoiceVatLine": {
        "type": "object",
        "properties": {
          "ratePercent": {
            "type": "number",
            "description": "Taux de TVA en pourcentage (`20.00`, `5.50`…)."
          },
          "baseHtCents": {
            "type": "integer",
            "description": "Base hors taxes nette (après remises) à ce taux, en centimes."
          },
          "vatCents": {
            "type": "integer",
            "description": "TVA à ce taux, en centimes."
          }
        },
        "description": "Une ligne de la ventilation de TVA."
      },
      "LabExamPatient": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de l'animal."
          },
          "publicId": {
            "type": "string",
            "description": "Identifiant public de l'animal (`pat_…`). C'est l'identifiant à renvoyer dans `patient.publicId` avec un résultat.",
            "nullable": true,
            "example": "pat_a8kx2mpqr"
          },
          "name": {
            "type": "string",
            "description": "Nom de l'animal."
          },
          "species": {
            "type": "string",
            "description": "Code d'espèce du référentiel Treats, en majuscules (`DOG`, `CAT`, `RABBIT`, `NAC`…). Vocabulaire ouvert : de nouveaux codes peuvent apparaître.",
            "nullable": true,
            "example": "CAT"
          },
          "breed": {
            "type": "string",
            "description": "Race, en texte libre.",
            "nullable": true
          },
          "sex": {
            "type": "string",
            "enum": [
              "MALE",
              "FEMALE",
              "MALE_NEUTERED",
              "FEMALE_SPAYED",
              "UNKNOWN"
            ],
            "description": "Sexe et statut de stérilisation.",
            "nullable": true
          },
          "birthDate": {
            "type": "string",
            "format": "date",
            "description": "Date de naissance.",
            "nullable": true
          },
          "microchipNumber": {
            "type": "string",
            "description": "Numéro de puce électronique.",
            "nullable": true
          },
          "owner": {
            "type": "object",
            "properties": {
              "lastName": {
                "type": "string",
                "description": "Nom de famille du client, ou raison sociale pour une personne morale.",
                "nullable": true
              },
              "firstName": {
                "type": "string",
                "description": "Prénom du client.",
                "nullable": true
              }
            },
            "description": "Le client propriétaire : nom seulement.",
            "nullable": true
          }
        },
        "description": "L'animal d'une commande d'analyses ou d'une demande d'examen d'imagerie : de quoi l'identifier sur l'appareil ou à la paillasse, rien d'autre (aucune coordonnée du client)."
      },
      "LabOrder": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la commande Treats. À renvoyer dans `orderId` avec le résultat : c'est le rattachement le plus sûr."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site de la clinique qui a passé la commande."
          },
          "provider": {
            "type": "string",
            "enum": [
              "ANIMA_CARE",
              "IDEXX",
              "VETERNITY",
              "CENTRAVET",
              "ICAD",
              "NEPHTIS",
              "PENNYLANE",
              "PICOXIA",
              "MYPACS"
            ],
            "description": "Fournisseur à qui la commande est adressée (l'un de ceux que représente l'application)."
          },
          "providerOrderId": {
            "type": "string",
            "description": "Identifiant de la commande chez le laboratoire, quand Treats la lui a transmise par son connecteur.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "COMPLETED",
              "CANCELLED"
            ],
            "description": "`PENDING` : à réaliser (créée, transmise ou en cours, y compris après un résultat partiel). `COMPLETED` : résultat final reçu. `CANCELLED` : annulée par la clinique."
          },
          "modality": {
            "type": "string",
            "enum": [
              "IN_HOUSE",
              "REFERENCE_LAB",
              "DIGITAL",
              "OTHER"
            ],
            "description": "Mode d'analyse : au cabinet (`IN_HOUSE`), en laboratoire extérieur (`REFERENCE_LAB`), dématérialisé (`DIGITAL`), autre.",
            "nullable": true
          },
          "testCodes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Codes d'analyses demandés, au catalogue du laboratoire (`CBC`, `CHEM10`…). Liste vide si la commande n'en porte pas."
          },
          "notes": {
            "type": "string",
            "description": "Remarques de la clinique pour le laboratoire (« À jeun »…).",
            "nullable": true
          },
          "veterinarian": {
            "type": "string",
            "description": "Vétérinaire demandeur, en texte.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de la commande."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Dernière modification ; clé de synchronisation des listes."
          },
          "patient": {
            "allOf": [
              {
                "$ref": "#/components/schemas/LabExamPatient"
              }
            ],
            "description": "L'animal concerné ; `null` si la commande n'est rattachée à aucun animal lisible.",
            "nullable": true
          }
        },
        "description": "Une commande d'analyses adressée par une clinique au laboratoire que représente l'application."
      },
      "LabResult": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du résultat dans Treats (pour `GET /lab/results/{id}` et le dépôt du PDF)."
          },
          "externalResultId": {
            "type": "string",
            "description": "Identifiant du résultat chez le laboratoire, tel qu'envoyé."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site de la clinique qui a reçu le résultat."
          },
          "provider": {
            "type": "string",
            "enum": [
              "ANIMA_CARE",
              "IDEXX",
              "VETERNITY",
              "CENTRAVET",
              "ICAD",
              "NEPHTIS",
              "PENNYLANE",
              "PICOXIA",
              "MYPACS"
            ],
            "description": "Laboratoire d'origine du résultat."
          },
          "matched": {
            "type": "boolean",
            "description": "`true` : rattaché à un animal (dossier médical). `false` : en attente dans la file « Résultats à rattacher » de la clinique."
          },
          "matchedBy": {
            "type": "string",
            "enum": [
              "ORDER",
              "PATIENT_ID",
              "MICROCHIP"
            ],
            "description": "Ce qui a permis le rattachement automatique : la commande (`ORDER`), l'identifiant de l'animal (`PATIENT_ID`) ou la puce (`MICROCHIP`). `null` si le résultat n'est pas rattaché, ou s'il l'a été ensuite à la main par l'équipe de la clinique.",
            "nullable": true
          },
          "unmatchedReason": {
            "type": "string",
            "enum": [
              "NO_IDENTIFIER",
              "PATIENT_NOT_FOUND",
              "MICROCHIP_AMBIGUOUS",
              "CONFLICTING_IDENTIFIERS"
            ],
            "description": "Pourquoi le résultat n'a pas été rattaché : aucun identifiant d'animal (`NO_IDENTIFIER`), identifiant ou puce introuvable, ou animal hors des sites ouverts à l'application (`PATIENT_NOT_FOUND`), puce portée par plusieurs animaux (`MICROCHIP_AMBIGUOUS`), commande, identifiant et puce qui désignent des animaux différents (`CONFLICTING_IDENTIFIERS`). `null` si le résultat est rattaché.",
            "nullable": true
          },
          "orderId": {
            "type": "string",
            "format": "uuid",
            "description": "Commande Treats d'origine, si le résultat a été envoyé avec `orderId`.",
            "nullable": true
          },
          "patientId": {
            "type": "string",
            "format": "uuid",
            "description": "Animal auquel le résultat est rattaché ; `null` tant qu'il ne l'est pas.",
            "nullable": true
          },
          "patientPublicId": {
            "type": "string",
            "description": "Identifiant public (`pat_…`) de cet animal.",
            "nullable": true
          },
          "resultType": {
            "type": "string",
            "enum": [
              "BLOOD_PANEL",
              "XRAY",
              "ULTRASOUND",
              "URINE",
              "BIOPSY",
              "OTHER"
            ],
            "description": "Type de résultat."
          },
          "modality": {
            "type": "string",
            "enum": [
              "IN_HOUSE",
              "REFERENCE_LAB",
              "DIGITAL",
              "OTHER"
            ],
            "description": "Mode d'analyse.",
            "nullable": true
          },
          "collectedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de prélèvement envoyée dans `collectedAt` ; à défaut, date de réception par Treats.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de la première réception par Treats."
          },
          "pdfReceived": {
            "type": "boolean",
            "description": "Un compte rendu PDF accompagne le résultat."
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LabResultLine"
            },
            "description": "Lignes d'analyses, regroupées par rubrique (`category`) dans l'ordre où chaque rubrique est apparue, puis dans l'ordre d'envoi. Liste vide pour un résultat en compte rendu PDF seul."
          }
        },
        "description": "Un résultat d'analyses reçu du laboratoire que représente l'application."
      },
      "LabResultCreate": {
        "type": "object",
        "properties": {
          "externalResultId": {
            "type": "string",
            "description": "Identifiant du résultat chez le laboratoire, 100 caractères au plus. Unique par organisation et par laboratoire : le renvoyer remplace le résultat déjà reçu (voir la route).",
            "example": "DX-2026-0042",
            "maxLength": 100
          },
          "orderId": {
            "type": "string",
            "format": "uuid",
            "description": "Commande Treats à laquelle répond le résultat (`id` lu sur `GET /lab/orders`). Elle doit être adressée à un fournisseur de l'application, sur un site ouvert, sinon `404 LAB_ORDER_NOT_FOUND`. Elle donne le site de réception, le fournisseur et l'animal."
          },
          "provider": {
            "type": "string",
            "enum": [
              "ANIMA_CARE",
              "IDEXX",
              "VETERNITY",
              "CENTRAVET",
              "ICAD",
              "NEPHTIS",
              "PENNYLANE",
              "PICOXIA",
              "MYPACS"
            ],
            "description": "Laboratoire d'origine, insensible à la casse. Doit être l'un des fournisseurs que représente l'application et, avec `orderId`, celui de la commande. Facultatif avec `orderId` ou si l'application ne représente qu'un fournisseur ; obligatoire sinon (`PROVIDER_REQUIRED`)."
          },
          "patient": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Identifiant de l'animal dans Treats."
              },
              "publicId": {
                "type": "string",
                "description": "Identifiant public de l'animal (`pat_…`), tel que lu dans `patient.publicId` d'une commande ou d'une demande d'examen.",
                "example": "pat_a8kx2mpqr",
                "maxLength": 64
              },
              "microchip": {
                "type": "string",
                "description": "Numéro de puce : 6 à 23 caractères alphanumériques, espaces ignorés. Ne rattache que si un seul animal de l'organisation porte cette puce."
              },
              "name": {
                "type": "string",
                "description": "Nom de l'animal chez le laboratoire. Affiché à l'équipe dans « Résultats à rattacher » ; jamais utilisé pour rattacher.",
                "maxLength": 200
              },
              "ownerLastName": {
                "type": "string",
                "description": "Nom du propriétaire chez le laboratoire. Affiché à l'équipe dans « Résultats à rattacher » ; jamais utilisé pour rattacher.",
                "maxLength": 200
              }
            },
            "description": "Identification de l'animal, quand le résultat ne passe pas par une commande (ou pour la confirmer). Tous les identifiants fournis (y compris l'animal de la commande) doivent désigner le même animal, sinon le résultat n'est pas rattaché : `CONFLICTING_IDENTIFIERS`, ou `PATIENT_NOT_FOUND` quand `id` et `publicId` se contredisent.",
            "nullable": true
          },
          "resultType": {
            "type": "string",
            "enum": [
              "BLOOD_PANEL",
              "XRAY",
              "ULTRASOUND",
              "URINE",
              "BIOPSY",
              "OTHER"
            ],
            "description": "Type de résultat, insensible à la casse. `BIOPSY` pour un compte rendu d'anatomopathologie.",
            "default": "OTHER"
          },
          "modality": {
            "type": "string",
            "enum": [
              "IN_HOUSE",
              "REFERENCE_LAB",
              "DIGITAL",
              "OTHER"
            ],
            "description": "Mode d'analyse, insensible à la casse.",
            "default": "REFERENCE_LAB"
          },
          "collectedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de prélèvement (instant ISO 8601). À défaut, la date de réception par Treats en tient lieu."
          },
          "final": {
            "type": "boolean",
            "description": "`false` : résultat partiel. Ne joue que pour un résultat rattaché par sa commande : la commande reste `PENDING` (en cours) au lieu de passer `COMPLETED`.",
            "default": true
          },
          "veterinarian": {
            "type": "string",
            "description": "Biologiste ou vétérinaire signataire, enregistré avec le résultat.",
            "maxLength": 200
          },
          "lines": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "analyte": {
                  "type": "string",
                  "description": "Paramètre mesuré (`GLU`, `HCT`…).",
                  "maxLength": 200
                },
                "value": {
                  "type": "string",
                  "description": "Valeur telle qu'à afficher (« 12.31 », « < 10 », « positif »). `value` ou `numericValue` obligatoire ; sans `value`, l'écriture décimale de `numericValue` en tient lieu.",
                  "maxLength": 500
                },
                "numericValue": {
                  "type": "number",
                  "description": "Valeur numérique, en nombre JSON (jamais un texte converti) : 6 décimales et 12 chiffres entiers au plus (valeur absolue jusqu'à 999 999 999 999)."
                },
                "unit": {
                  "type": "string",
                  "description": "Unité (`mmol/L`, `%`…).",
                  "maxLength": 50
                },
                "low": {
                  "type": "number",
                  "description": "Borne basse de l'intervalle de référence, mêmes limites que `numericValue`. Doit être inférieure ou égale à `high`."
                },
                "high": {
                  "type": "number",
                  "description": "Borne haute de l'intervalle de référence, mêmes limites que `numericValue`."
                },
                "referenceRange": {
                  "type": "string",
                  "description": "Intervalle de référence en texte (« 3.9 - 7.8 »).",
                  "maxLength": 100
                },
                "flag": {
                  "type": "string",
                  "enum": [
                    "NORMAL",
                    "LOW",
                    "HIGH",
                    "CRITICAL_LOW",
                    "CRITICAL_HIGH",
                    "ABNORMAL"
                  ],
                  "description": "Position de la valeur, insensible à la casse. Une valeur hors `NORMAL` est signalée à la clinique comme hors norme."
                },
                "category": {
                  "type": "string",
                  "description": "Rubrique (« Biochimie »…). Les lignes sont regroupées par rubrique.",
                  "maxLength": 200
                },
                "testCode": {
                  "type": "string",
                  "description": "Code de l'analyse au catalogue du laboratoire.",
                  "maxLength": 50
                },
                "testName": {
                  "type": "string",
                  "description": "Nom de l'analyse.",
                  "maxLength": 200
                },
                "notes": {
                  "type": "string",
                  "description": "Commentaire sur la ligne.",
                  "maxLength": 2000
                }
              },
              "required": [
                "analyte"
              ]
            },
            "description": "Lignes d'analyses, 500 au plus. Au moins une ligne ou un compte rendu (`pdfBase64`) est exigé. Les textes vides valent absents."
          },
          "pdfBase64": {
            "type": "string",
            "description": "Compte rendu PDF encodé en base64 (espaces et retours à la ligne ignorés), 20 Mo au plus une fois décodé. Le contenu doit commencer par la signature `%PDF`."
          }
        },
        "required": [
          "externalResultId"
        ],
        "description": "Résultat d'analyses envoyé par le laboratoire. Tout champ hors de cette liste est refusé (`INVALID_FIELD`), de même dans `patient`. Toutes les erreurs de champ sont rendues ensemble, chacune avec son chemin (`lines[0].flag`)."
      },
      "LabResultLine": {
        "type": "object",
        "properties": {
          "category": {
            "type": "string",
            "description": "Rubrique (« Biochimie », « Hématologie »…).",
            "nullable": true
          },
          "testCode": {
            "type": "string",
            "description": "Code de l'analyse au catalogue du laboratoire.",
            "nullable": true
          },
          "testName": {
            "type": "string",
            "description": "Nom de l'analyse.",
            "nullable": true
          },
          "analyte": {
            "type": "string",
            "description": "Paramètre mesuré (`GLU`, `HCT`…). Toujours rempli pour un résultat déposé par l'API ; peut manquer sur un résultat que Treats a reçu de ce laboratoire par un autre canal.",
            "nullable": true
          },
          "value": {
            "type": "string",
            "description": "Valeur telle qu'affichée (« 12.31 », « < 10 », « positif »). Quand seule `numericValue` a été envoyée, son écriture décimale.",
            "nullable": true
          },
          "numericValue": {
            "type": "number",
            "description": "Valeur numérique exacte, sans zéros superflus (`12.3`).",
            "nullable": true
          },
          "unit": {
            "type": "string",
            "description": "Unité (`mmol/L`, `%`, `U/L`…).",
            "nullable": true
          },
          "low": {
            "type": "number",
            "description": "Borne basse de l'intervalle de référence.",
            "nullable": true
          },
          "high": {
            "type": "number",
            "description": "Borne haute de l'intervalle de référence.",
            "nullable": true
          },
          "referenceRange": {
            "type": "string",
            "description": "Intervalle de référence en texte (« 3.9 - 7.8 »).",
            "nullable": true
          },
          "flag": {
            "type": "string",
            "enum": [
              "NORMAL",
              "LOW",
              "HIGH",
              "CRITICAL_LOW",
              "CRITICAL_HIGH",
              "ABNORMAL"
            ],
            "description": "Position de la valeur par rapport à l'intervalle. `null` si le laboratoire n'en a pas donné.",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Commentaire du laboratoire sur la ligne.",
            "nullable": true
          }
        },
        "description": "Une ligne d'analyse d'un résultat, relue telle qu'elle a été envoyée."
      },
      "Me": {
        "type": "object",
        "properties": {
          "app": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Identifiant de l'application."
              },
              "name": {
                "type": "string",
                "description": "Nom de l'application, tel que les cliniques le voient."
              },
              "company": {
                "type": "string",
                "description": "Éditeur.",
                "nullable": true
              },
              "environment": {
                "type": "string",
                "enum": [
                  "SANDBOX",
                  "LIVE"
                ],
                "description": "`SANDBOX` : application de recette, connectable seulement à une organisation de test qui n'envoie aucun email ni SMS. `LIVE` : production."
              }
            },
            "description": "L'application partenaire."
          },
          "organization": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Identifiant de l'organisation."
              },
              "name": {
                "type": "string",
                "description": "Nom de l'organisation.",
                "nullable": true
              }
            },
            "description": "L'organisation qui a connecté l'application."
          },
          "sites": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid",
                  "description": "Identifiant du site."
                },
                "name": {
                  "type": "string",
                  "description": "Nom du site."
                }
              }
            },
            "description": "Sites ouverts à l'application. Toute lecture se limite à ces sites ; une ressource d'un autre site répond 404."
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Droits effectifs : ceux que l'organisation a accordés, dans la limite de ceux que Treats a ouverts à l'application."
          },
          "grant": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Identifiant de l'autorisation."
              },
              "allSites": {
                "type": "boolean",
                "description": "`true` : tous les sites de l'organisation, y compris ceux ouverts plus tard."
              },
              "grantedAt": {
                "type": "string",
                "format": "date-time",
                "description": "Date de l'autorisation."
              }
            },
            "description": "L'autorisation donnée par l'organisation."
          }
        },
        "description": "Ce que le jeton ouvre."
      },
      "ModuleAllergy": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "FOOD",
              "MEDICATION",
              "OTHER"
            ],
            "description": "Type d'allergie : alimentaire, médicamenteuse, autre.",
            "nullable": true
          },
          "allergen": {
            "type": "string",
            "description": "Allergène.",
            "nullable": true,
            "maxLength": 500
          }
        },
        "required": [
          "type",
          "allergen"
        ],
        "description": "Une allergie (module `commemoratifs`)."
      },
      "ModuleDataAnamnese": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "description": "Motif de consultation, dans les mots du propriétaire.",
            "nullable": true,
            "maxLength": 20000
          },
          "symptomOnset": {
            "type": "string",
            "enum": [
              "LESS_24H",
              "ONE_THREE_DAYS",
              "MORE_THREE_DAYS",
              "CHRONIC"
            ],
            "description": "Début des symptômes : moins de 24 h, 1 à 3 jours, plus de 3 jours, chronique.",
            "nullable": true
          },
          "evolution": {
            "type": "string",
            "enum": [
              "IMPROVEMENT",
              "STABLE",
              "WORSENING"
            ],
            "description": "Évolution : amélioration, stable, aggravation.",
            "nullable": true
          }
        },
        "required": [
          "reason",
          "symptomOnset",
          "evolution"
        ],
        "description": "`data` du module `anamnese` (lecture), et champs acceptés par `PUT …/modules/anamnese`."
      },
      "ModuleDataCommemoratifs": {
        "type": "object",
        "properties": {
          "medicalHistory": {
            "type": "string",
            "description": "Antécédents médicaux.",
            "nullable": true,
            "maxLength": 20000
          },
          "previousSurgeries": {
            "type": "string",
            "description": "Chirurgies antérieures.",
            "nullable": true,
            "maxLength": 20000
          },
          "currentTreatment": {
            "type": "string",
            "description": "Traitement en cours.",
            "nullable": true,
            "maxLength": 20000
          },
          "allergies": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ModuleAllergy"
              }
            ],
            "description": "Allergie connue. Objet remplacé en entier à l'écriture ; `null` l'efface.",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Notes du module. Réservées à l'équipe : jamais imprimées sur le compte rendu remis au propriétaire.",
            "nullable": true,
            "maxLength": 20000
          }
        },
        "required": [
          "medicalHistory",
          "previousSurgeries",
          "currentTreatment",
          "allergies",
          "notes"
        ],
        "description": "`data` du module `commemoratifs` (lecture), et champs acceptés par `PUT …/modules/commemoratifs`."
      },
      "ModuleDataComportement": {
        "type": "object",
        "properties": {
          "attitude": {
            "type": "string",
            "enum": [
              "NORMAL",
              "DEPRESSED",
              "AGGRESSIVE",
              "ANXIOUS"
            ],
            "description": "Attitude : normale, abattue, agressive, anxieuse.",
            "nullable": true
          },
          "interaction": {
            "type": "string",
            "enum": [
              "NORMAL",
              "DIMINISHED",
              "ABSENT"
            ],
            "description": "Interaction : normale, diminuée, absente.",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Notes du module (imprimées sur le compte rendu).",
            "nullable": true,
            "maxLength": 20000
          }
        },
        "required": [
          "attitude",
          "interaction",
          "notes"
        ],
        "description": "`data` du module `comportement` (lecture), et champs acceptés par `PUT …/modules/comportement`."
      },
      "ModuleDataDiagnostic": {
        "type": "object",
        "properties": {
          "primary": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ModuleDiagnosis"
            },
            "description": "Diagnostic principal : 30 éléments au plus. Liste remplacée en entier à l'écriture ; `null` ou `[]` l'efface (rendue `null`).",
            "nullable": true
          },
          "differential": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ModuleDiagnosis"
            },
            "description": "Diagnostic différentiel : 30 éléments au plus. Liste remplacée en entier à l'écriture ; `null` ou `[]` l'efface (rendue `null`).",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Notes du module (imprimées sur le compte rendu).",
            "nullable": true,
            "maxLength": 20000
          }
        },
        "required": [
          "primary",
          "differential",
          "notes"
        ],
        "description": "`data` du module `diagnostic` (lecture), et champs acceptés par `PUT …/modules/diagnostic`."
      },
      "ModuleDataExamenGeneral": {
        "type": "object",
        "properties": {
          "normal": {
            "type": "boolean",
            "description": "`true` : examen sans particularité (RAS).",
            "nullable": true
          },
          "generalState": {
            "type": "string",
            "enum": [
              "GOOD",
              "CORRECT",
              "ALTERED",
              "BAD"
            ],
            "description": "État général : bon, correct, altéré, mauvais.",
            "nullable": true
          },
          "mucousMembranes": {
            "type": "string",
            "enum": [
              "PINK",
              "PALE",
              "CONGESTED",
              "ICTERIC",
              "CYANOTIC"
            ],
            "description": "Muqueuses : roses, pâles, congestionnées, ictériques, cyanosées.",
            "nullable": true
          },
          "capillaryRefillSeconds": {
            "type": "number",
            "description": "Temps de recoloration capillaire, en secondes. Son interprétation (normal, allongé) est calculée par Treats.",
            "nullable": true,
            "minimum": 0,
            "maximum": 20
          },
          "dehydration": {
            "type": "string",
            "enum": [
              "NONE",
              "FIVE_PERCENT",
              "SIX_EIGHT_PERCENT",
              "TEN_TWELVE_PERCENT",
              "TWELVE_FIFTEEN_PERCENT"
            ],
            "description": "Déshydratation : aucune, 5 %, 6-8 %, 10-12 %, 12-15 %.",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Notes du module. Réservées à l'équipe : jamais imprimées sur le compte rendu remis au propriétaire.",
            "nullable": true,
            "maxLength": 20000
          }
        },
        "required": [
          "normal",
          "generalState",
          "mucousMembranes",
          "capillaryRefillSeconds",
          "dehydration",
          "notes"
        ],
        "description": "`data` du module `examen-general` (lecture), et champs acceptés par `PUT …/modules/examen-general`."
      },
      "ModuleDefinition": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "Identifiant stable du module.",
            "example": "examen-general"
          },
          "name": {
            "type": "string",
            "description": "Nom du module, tel que la clinique le voit."
          },
          "category": {
            "type": "string",
            "description": "Famille du module, en minuscules (`history`, `examination`, `diagnosis`, `procedure`, `treatment`, `prevention`, `vitals`, `lab`, `imaging`, `nutrition`, `billing`, `files`, `administrative`).",
            "nullable": true
          },
          "writable": {
            "type": "boolean",
            "description": "`true` : module écrivable par `PUT /consultations/{id}/modules/{slug}` (anamnese, commemoratifs, examen-general, diagnostic, comportement). `false` : lecture seule."
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ModuleField"
            },
            "description": "Champs du contrat, dans l'ordre de l'écran. Vide pour un module en lecture seule."
          }
        },
        "required": [
          "slug",
          "name",
          "category",
          "writable",
          "fields"
        ],
        "description": "Un module de consultation proposé par le site et, s'il est écrivable par l'API, le contrat de son `data`."
      },
      "ModuleDiagnosis": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "description": "Libellé du diagnostic. Obligatoire à l'écriture, sauf si `diagnosisId` est donné : le nom du diagnostic du référentiel est alors repris.",
            "maxLength": 500
          },
          "diagnosisId": {
            "type": "string",
            "format": "uuid",
            "description": "Diagnostic du référentiel de l'organisation (actif, commun ou propre à l'organisation). Absent ou nul : diagnostic en texte libre. Un identifiant hors du référentiel est refusé.",
            "nullable": true
          }
        },
        "required": [
          "label",
          "diagnosisId"
        ],
        "description": "Un diagnostic (module `diagnostic`)."
      },
      "ModuleEnumValue": {
        "type": "object",
        "properties": {
          "value": {
            "type": "string",
            "description": "Valeur à envoyer, telle quelle (sensible à la casse).",
            "example": "ONE_THREE_DAYS"
          },
          "label": {
            "type": "string",
            "description": "Libellé français, celui de l'écran.",
            "example": "1-3 jours"
          }
        },
        "required": [
          "value",
          "label"
        ],
        "description": "Une valeur permise d'un champ `ENUM`."
      },
      "ModuleField": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Nom du champ dans le corps et dans `data`.",
            "example": "symptomOnset"
          },
          "type": {
            "type": "string",
            "enum": [
              "TEXT",
              "ENUM",
              "BOOLEAN",
              "DECIMAL",
              "OBJECT",
              "ARRAY"
            ],
            "description": "`TEXT` : texte. `ENUM` : une valeur de `values`. `BOOLEAN`. `DECIMAL` : nombre entre `min` et `max`. `OBJECT` : objet dont les champs sont dans `properties`. `ARRAY` : liste d'objets dont les champs sont dans `items`."
          },
          "label": {
            "type": "string",
            "description": "Libellé français du champ."
          },
          "description": {
            "type": "string",
            "description": "Précision sur le champ.",
            "nullable": true
          },
          "maxLength": {
            "type": "integer",
            "description": "`TEXT` : nombre de caractères au plus (20 000 pour un texte libre, 500 pour un libellé). `ARRAY` : nombre d'éléments au plus (30).",
            "nullable": true
          },
          "min": {
            "type": "number",
            "description": "`DECIMAL` : valeur minimale.",
            "nullable": true
          },
          "max": {
            "type": "number",
            "description": "`DECIMAL` : valeur maximale.",
            "nullable": true
          },
          "unit": {
            "type": "string",
            "description": "`DECIMAL` : unité de la valeur.",
            "nullable": true,
            "example": "secondes"
          },
          "values": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ModuleEnumValue"
            },
            "description": "`ENUM` : valeurs permises, dans l'ordre de l'échelle. Nul pour les autres types.",
            "nullable": true
          },
          "properties": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ModuleField"
            },
            "description": "`OBJECT` : champs de l'objet. Nul pour les autres types.",
            "nullable": true
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ModuleField"
            },
            "description": "`ARRAY` : champs d'un élément de la liste. Nul pour les autres types.",
            "nullable": true
          },
          "teamOnly": {
            "type": "boolean",
            "description": "`true` : réservé à l'équipe, jamais imprimé sur le compte rendu remis au propriétaire."
          }
        },
        "required": [
          "name",
          "type",
          "label",
          "description",
          "maxLength",
          "min",
          "max",
          "unit",
          "values",
          "properties",
          "items",
          "teamOnly"
        ],
        "description": "Un champ du contrat d'un module : nom à utiliser dans le corps, type, bornes, valeurs permises."
      },
      "Patient": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de l'animal."
          },
          "publicId": {
            "type": "string",
            "description": "Identifiant public partageable (`pat_…`). Peut être nul sur une fiche à qui aucun n'a encore été attribué.",
            "nullable": true,
            "example": "pat_3F9Q7T"
          },
          "clientId": {
            "type": "string",
            "format": "uuid",
            "description": "Propriétaire (client) de l'animal."
          },
          "name": {
            "type": "string",
            "description": "Nom de l'animal."
          },
          "species": {
            "type": "string",
            "description": "Code d'espèce, tel que le rend `GET /species` (par exemple `DOG`, `CAT`).",
            "example": "CAT"
          },
          "breed": {
            "type": "string",
            "description": "Race en texte, telle qu'elle s'affiche dans Treats.",
            "nullable": true
          },
          "breedId": {
            "type": "string",
            "format": "uuid",
            "description": "Race du référentiel de l'organisation, quand la race y est liée.",
            "nullable": true
          },
          "sex": {
            "type": "string",
            "enum": [
              "MALE",
              "FEMALE",
              "MALE_NEUTERED",
              "FEMALE_SPAYED",
              "UNKNOWN"
            ],
            "description": "Sexe. `MALE_NEUTERED` : mâle castré ; `FEMALE_SPAYED` : femelle stérilisée. `null` : non renseigné.",
            "nullable": true
          },
          "sterilized": {
            "type": "boolean",
            "description": "Animal stérilisé. `null` : non renseigné.",
            "nullable": true
          },
          "birthDate": {
            "type": "string",
            "format": "date",
            "description": "Date de naissance. Pour un âge estimé, ancrée au 1er du mois (`MONTH`) ou au 1er janvier (`YEAR`).",
            "nullable": true
          },
          "birthDatePrecision": {
            "type": "string",
            "enum": [
              "DAY",
              "MONTH",
              "YEAR"
            ],
            "description": "`DAY` : date exacte. `MONTH`, `YEAR` : âge estimé au mois ou à l'année. `null` quand `birthDate` est nulle.",
            "nullable": true
          },
          "microchipNumber": {
            "type": "string",
            "description": "Numéro de puce (15 chiffres). Unique dans l'organisation.",
            "nullable": true
          },
          "tattooNumber": {
            "type": "string",
            "description": "Numéro de tatouage.",
            "nullable": true
          },
          "passportNumber": {
            "type": "string",
            "description": "Numéro de passeport.",
            "nullable": true
          },
          "coatColor": {
            "type": "string",
            "description": "Robe (couleur du pelage).",
            "nullable": true
          },
          "deceased": {
            "type": "boolean",
            "description": "`true` : décès déclaré par la clinique (jamais par l'API)."
          },
          "deceasedDate": {
            "type": "string",
            "format": "date",
            "description": "Date du décès déclaré par la clinique.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de création de la fiche. Peut être nulle sur une fiche reprise d'un ancien logiciel.",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Clé de synchronisation, à la microseconde : date de dernière modification, à défaut date de création. C'est la valeur que lit `updated_since` et qui ordonne la liste."
          }
        },
        "required": [
          "id",
          "clientId",
          "name",
          "species",
          "deceased",
          "updatedAt"
        ],
        "description": "Un animal : son identité. Ne contient ni mémo, ni alertes, ni assurance, ni photo, ni poids (voir les pesées)."
      },
      "PatientWeight": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la pesée."
          },
          "weightKg": {
            "type": "number",
            "description": "Poids en kilogrammes (nombre décimal JSON, jamais une chaîne).",
            "example": 12.4
          },
          "measuredAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de la pesée."
          }
        },
        "required": [
          "id",
          "weightKg",
          "measuredAt"
        ],
        "description": "Une pesée de l'animal. Ni note ni score d'état corporel."
      },
      "Payment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du mouvement."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site de l'encaissement (celui de la facture réglée quand le paiement n'en porte pas)."
          },
          "kind": {
            "type": "string",
            "enum": [
              "INVOICE",
              "DEPOSIT",
              "DEPOSIT_REFUND",
              "CREDIT_NOTE_REFUND"
            ],
            "description": "`INVOICE` : règlement d'une facture (`invoiceId` renseigné). `DEPOSIT` : acompte versé au compte client. `DEPOSIT_REFUND` : remboursement d'un acompte. `CREDIT_NOTE_REFUND` : remboursement d'un avoir (`creditNoteId` renseigné). Les remboursements ont un montant négatif."
          },
          "method": {
            "type": "string",
            "enum": [
              "CARD",
              "CARD_CONTACTLESS",
              "CASH",
              "CHECK",
              "TRANSFER",
              "CLIENT_ACCOUNT",
              "ALMA",
              "AVOIR",
              "CERTIFICATE_UNRECOVERABLE",
              "CONTENTIOUS",
              "ONEY_BANK",
              "OXANE",
              "PAYVET",
              "LOSS",
              "PROFIT_AND_LOSS",
              "FLOA"
            ],
            "description": "Moyen de paiement : carte, carte sans contact, espèces, chèque, virement, prélèvement sur le compte client (acompte), avoir, organismes de paiement ou de financement (`ALMA`, `FLOA`, `ONEY_BANK`, `OXANE`, `PAYVET`), et soldes comptables d'une créance (`CERTIFICATE_UNRECOVERABLE` : certificat d'irrécouvrabilité, `CONTENTIOUS` : contentieux, `LOSS` et `PROFIT_AND_LOSS` : passage en perte)."
          },
          "amountCents": {
            "type": "integer",
            "description": "Montant en centimes. Négatif pour un remboursement (`DEPOSIT_REFUND`, `CREDIT_NOTE_REFUND`)."
          },
          "refundedCents": {
            "type": "integer",
            "description": "Part de ce paiement déjà rendue au client, en centimes (0 si rien)."
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "description": "Devise des montants, toujours `EUR`."
          },
          "receivedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date d'encaissement."
          },
          "invoiceId": {
            "type": "string",
            "format": "uuid",
            "description": "Facture réglée, pour un `INVOICE`.",
            "nullable": true
          },
          "clientId": {
            "type": "string",
            "format": "uuid",
            "description": "Client payeur (à défaut, celui de la facture réglée).",
            "nullable": true
          },
          "creditNoteId": {
            "type": "string",
            "format": "uuid",
            "description": "Avoir remboursé, pour un `CREDIT_NOTE_REFUND`.",
            "nullable": true
          },
          "reference": {
            "type": "string",
            "description": "Référence saisie avec le paiement (numéro de chèque, de transaction…).",
            "nullable": true
          },
          "groupId": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant commun aux mouvements d'un même encaissement réparti sur plusieurs factures.",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de synchronisation : dernière modification du paiement (remboursement partiel…), à défaut sa date d'encaissement. Sert de clé à `updated_since` et au curseur."
          }
        },
        "description": "Un mouvement d'encaissement d'un site."
      },
      "Prescription": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de l'ordonnance."
          },
          "number": {
            "type": "string",
            "description": "Numéro de l'ordonnance, attribué à la signature.",
            "nullable": true
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site où l'ordonnance a été établie."
          },
          "patientId": {
            "type": "string",
            "format": "uuid",
            "description": "Animal concerné.",
            "nullable": true
          },
          "consultationId": {
            "type": "string",
            "format": "uuid",
            "description": "Consultation d'où vient l'ordonnance.",
            "nullable": true
          },
          "type": {
            "type": "string",
            "enum": [
              "SIMPLE",
              "SECURED"
            ],
            "description": "`SIMPLE` : ordonnance ordinaire. `SECURED` : ordonnance sécurisée (stupéfiants), à validité courte."
          },
          "status": {
            "type": "string",
            "enum": [
              "SIGNED",
              "CANCELLED"
            ],
            "description": "`SIGNED` : signée par le vétérinaire. `CANCELLED` : annulée après signature (son numéro reste réservé)."
          },
          "issuedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date d'établissement."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de fin de validité."
          },
          "signedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de signature par le vétérinaire. Peut manquer sur une ordonnance reprise d'un ancien logiciel.",
            "nullable": true
          },
          "valid": {
            "type": "boolean",
            "description": "Au détail : `true` si la date de fin de validité n'est pas passée au moment de la lecture. Ne tient pas compte de l'annulation : lire aussi `status`. `null` en liste.",
            "nullable": true
          },
          "prescriberName": {
            "type": "string",
            "description": "Au détail : nom du vétérinaire prescripteur. `null` en liste.",
            "nullable": true
          },
          "recommendations": {
            "type": "string",
            "description": "Au détail : recommandations imprimées sur l'ordonnance. `null` en liste.",
            "nullable": true
          },
          "renewalPolicy": {
            "type": "string",
            "enum": [
              "NOT_ALLOWED",
              "ALLOWED_1_YEAR",
              "WITH_CONDITIONS"
            ],
            "description": "Au détail : renouvellement non autorisé, autorisé pendant un an, ou sous conditions. `null` en liste ou si non renseigné.",
            "nullable": true
          },
          "renewedFromId": {
            "type": "string",
            "format": "uuid",
            "description": "Au détail : ordonnance dont celle-ci est le renouvellement. `null` en liste.",
            "nullable": true
          },
          "cancelledAt": {
            "type": "string",
            "format": "date-time",
            "description": "Au détail : date d'annulation d'une ordonnance annulée. `null` en liste, même pour une ordonnance annulée.",
            "nullable": true
          },
          "cancellationReason": {
            "type": "string",
            "description": "Au détail : motif d'annulation. `null` en liste.",
            "nullable": true
          },
          "lineCount": {
            "type": "integer",
            "description": "Nombre de lignes de l'ordonnance."
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PrescriptionLine"
            },
            "description": "Au détail : les lignes, telles qu'imprimées. `null` en liste.",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de synchronisation : dernière modification de l'ordonnance (signature, annulation…), à défaut sa création. Sert de clé à `updated_since` et au curseur."
          }
        },
        "description": "Une ordonnance signée (ou annulée après signature). En liste, sans ses lignes ; au détail, avec elles."
      },
      "PrescriptionLine": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la ligne."
          },
          "catalogItemId": {
            "type": "string",
            "format": "uuid",
            "description": "Article du catalogue prescrit (voir `GET /catalog/items/{id}`). `null` pour une prescription libre.",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Nom du médicament ou du produit prescrit.",
            "nullable": true
          },
          "substance": {
            "type": "string",
            "description": "Substance active.",
            "nullable": true
          },
          "dosage": {
            "type": "string",
            "description": "Posologie, telle qu'imprimée (« 1 comprimé matin et soir »).",
            "nullable": true
          },
          "duration": {
            "type": "string",
            "description": "Durée du traitement, telle qu'imprimée.",
            "nullable": true
          },
          "quantity": {
            "type": "integer",
            "description": "Quantité délivrée.",
            "nullable": true
          },
          "conditioning": {
            "type": "string",
            "description": "Conditionnement.",
            "nullable": true
          },
          "route": {
            "type": "string",
            "description": "Voie d'administration.",
            "nullable": true
          },
          "instructions": {
            "type": "string",
            "description": "Instructions complémentaires.",
            "nullable": true
          },
          "computedDoseValue": {
            "type": "number",
            "description": "Dose calculée pour le poids de l'animal, quand le vétérinaire l'a calculée.",
            "nullable": true
          },
          "computedDoseUnit": {
            "type": "string",
            "description": "Unité de cette dose (`mg`, `mL`…).",
            "nullable": true
          },
          "offLabelUse": {
            "type": "boolean",
            "description": "`true` : prescription hors des conditions de l'autorisation de mise sur le marché (cascade)."
          },
          "freeText": {
            "type": "boolean",
            "description": "`true` : ligne saisie librement, sans article du catalogue."
          }
        },
        "description": "Une ligne d'ordonnance : le médicament prescrit et sa posologie."
      },
      "Room": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la salle."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site de la salle."
          },
          "name": {
            "type": "string",
            "description": "Nom de la salle, tel que la clinique l'affiche."
          },
          "type": {
            "type": "string",
            "enum": [
              "CONSULTATION_ROOM",
              "OPERATING_THEATER",
              "IMAGING_ROOM",
              "LABORATORY",
              "HOSPITALIZATION",
              "GROOMING",
              "WAITING_AREA",
              "ISOLATION_ROOM",
              "EQUIPMENT",
              "OTHER"
            ],
            "description": "Nature : salle de consultation, bloc opératoire, imagerie, laboratoire, hospitalisation, toilettage, salle d'attente, isolement, équipement, autre."
          },
          "capacity": {
            "type": "integer",
            "description": "Capacité renseignée par la clinique (1 par défaut). Nulle si elle a été effacée.",
            "nullable": true
          }
        },
        "required": [
          "id",
          "siteId",
          "name",
          "type",
          "capacity"
        ],
        "description": "Une salle ou une ressource active d'un site."
      },
      "Site": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du site. C'est la valeur à passer en `site_id` ou dans l'en-tête `Treats-Site-Id`."
          },
          "organizationId": {
            "type": "string",
            "format": "uuid",
            "description": "Organisation du site."
          },
          "name": {
            "type": "string",
            "description": "Nom du site."
          },
          "code": {
            "type": "string",
            "description": "Code court du site (3 à 5 caractères), stable. Préfixe ses numéros d'ordonnance.",
            "nullable": true,
            "example": "MAM",
            "maxLength": 5
          },
          "phone": {
            "type": "string",
            "description": "Téléphone du site.",
            "nullable": true
          },
          "email": {
            "type": "string",
            "description": "Adresse email du site.",
            "nullable": true
          },
          "siret": {
            "type": "string",
            "description": "Numéro SIRET du site.",
            "nullable": true
          },
          "address": {
            "allOf": [
              {
                "$ref": "#/components/schemas/SiteAddress"
              }
            ],
            "description": "Adresse postale. L'objet est toujours présent, ses champs peuvent être nuls."
          },
          "timezone": {
            "type": "string",
            "enum": [
              "Europe/Paris"
            ],
            "description": "Fuseau des jours calendaires et des horaires du site. Toujours `Europe/Paris` aujourd'hui ; lisez-le plutôt que de le supposer."
          },
          "dormant": {
            "type": "boolean",
            "description": "`true` : site en sommeil. Il reste visible, mais ne reçoit ni rendez-vous en ligne ni envoi automatique."
          }
        },
        "required": [
          "id",
          "organizationId",
          "name",
          "code",
          "phone",
          "email",
          "siret",
          "address",
          "timezone",
          "dormant"
        ],
        "description": "Un site de l'organisation ouvert à l'application."
      },
      "SiteAddress": {
        "type": "object",
        "properties": {
          "line": {
            "type": "string",
            "description": "Adresse sur une ligne, telle qu'elle figure sur les documents du site.",
            "nullable": true
          },
          "street": {
            "type": "string",
            "description": "Numéro et voie.",
            "nullable": true
          },
          "postalCode": {
            "type": "string",
            "description": "Code postal.",
            "nullable": true
          },
          "city": {
            "type": "string",
            "description": "Ville.",
            "nullable": true
          },
          "country": {
            "type": "string",
            "description": "Pays.",
            "nullable": true
          }
        },
        "required": [
          "line",
          "street",
          "postalCode",
          "city",
          "country"
        ],
        "description": "Adresse postale d'un site : composantes séparées et ligne composée."
      },
      "SiteHoliday": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du jour."
          },
          "date": {
            "type": "string",
            "format": "date",
            "description": "Jour (`AAAA-MM-JJ`, Europe/Paris). Pour un jour qui revient chaque année, c'est la date enregistrée : reportez le jour et le mois sur l'année voulue."
          },
          "label": {
            "type": "string",
            "description": "Libellé (« 1er mai », « Inventaire »…)."
          },
          "recurringYearly": {
            "type": "boolean",
            "description": "`true` : revient chaque année à la même date (1er mai, 25 décembre…)."
          },
          "scope": {
            "type": "string",
            "enum": [
              "NATIONAL",
              "SITE"
            ],
            "description": "`NATIONAL` : jour férié commun à tous les sites. `SITE` : fermeture propre à ce site."
          }
        },
        "required": [
          "id",
          "date",
          "label",
          "recurringYearly",
          "scope"
        ],
        "description": "Un jour férié ou un jour de fermeture d'un site."
      },
      "SiteOpeningHours": {
        "type": "object",
        "properties": {
          "dayOfWeek": {
            "type": "string",
            "enum": [
              "MONDAY",
              "TUESDAY",
              "WEDNESDAY",
              "THURSDAY",
              "FRIDAY",
              "SATURDAY",
              "SUNDAY"
            ],
            "description": "Jour de la semaine."
          },
          "closed": {
            "type": "boolean",
            "description": "`true` : site fermé ce jour-là. Les heures sont alors toutes nulles."
          },
          "open": {
            "type": "string",
            "description": "Heure d'ouverture (`HH:mm`). Nulle un jour fermé ou si la clinique ne l'a pas renseignée.",
            "nullable": true,
            "example": "08:30"
          },
          "close": {
            "type": "string",
            "description": "Heure de fermeture (`HH:mm`). Nulle un jour fermé ou si la clinique ne l'a pas renseignée.",
            "nullable": true,
            "example": "19:00"
          },
          "breakStart": {
            "type": "string",
            "description": "Début de la pause de midi (`HH:mm`). Nul s'il n'y a pas de pause ou un jour fermé.",
            "nullable": true,
            "example": "12:30"
          },
          "breakEnd": {
            "type": "string",
            "description": "Fin de la pause de midi (`HH:mm`). Nulle s'il n'y a pas de pause ou un jour fermé.",
            "nullable": true,
            "example": "14:00"
          },
          "slotMinutes": {
            "type": "integer",
            "description": "Durée par défaut d'un créneau de rendez-vous, en minutes. Nulle si le site n'en fixe pas. Rendue aussi un jour fermé.",
            "nullable": true,
            "example": 30
          }
        },
        "required": [
          "dayOfWeek",
          "closed",
          "open",
          "close",
          "breakStart",
          "breakEnd",
          "slotMinutes"
        ],
        "description": "Horaires d'ouverture d'un site pour un jour de la semaine, en heure locale du site (`HH:mm`)."
      },
      "Species": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Code de l'espèce, en majuscules. C'est la valeur à envoyer dans `species` d'un animal et dans le chemin de `GET /species/{code}/breeds`.",
            "example": "DOG"
          },
          "label": {
            "type": "string",
            "description": "Libellé dans la langue de l'organisation, renommé si la clinique l'a renommé."
          },
          "group": {
            "type": "string",
            "enum": [
              "DOG",
              "CAT",
              "EQUINE",
              "BOVINE",
              "OVINE",
              "CAPRINE",
              "PORCINE",
              "EXOTIC",
              "FARM",
              "OTHER"
            ],
            "description": "Famille fixe, stable d'une organisation à l'autre : à utiliser pour faire correspondre l'espèce à votre propre nomenclature. `EXOTIC` : nouveaux animaux de compagnie (lapin, furet, oiseau, reptile, rongeur…). `FARM` : animaux de rente hors ruminants classiques (volailles, camélidés…)."
          },
          "parentCode": {
            "type": "string",
            "description": "Pour une espèce créée par la clinique, le code de l'espèce Treats à laquelle elle se rattache. Nul pour une espèce Treats.",
            "nullable": true
          }
        },
        "required": [
          "code",
          "label",
          "group",
          "parentCode"
        ],
        "description": "Une espèce que l'accueil de la clinique peut choisir."
      },
      "StaffMember": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant du membre de l'équipe."
          },
          "firstName": {
            "type": "string",
            "description": "Prénom."
          },
          "lastName": {
            "type": "string",
            "description": "Nom."
          },
          "role": {
            "type": "string",
            "enum": [
              "VET",
              "ASV",
              "ADMIN",
              "SUPER_ADMIN",
              "VIEWER",
              "CLUSTER_MANAGER",
              "REGIONAL_DIRECTOR",
              "GROUP_ADMIN"
            ],
            "description": "Rôle du compte dans Treats. `VET` : vétérinaire ; `ASV` : auxiliaire spécialisé vétérinaire ; `ADMIN` : administration de la clinique ; `SUPER_ADMIN` : administration de toute l'organisation ; `VIEWER` : consultation seule ; `CLUSTER_MANAGER`, `REGIONAL_DIRECTOR`, `GROUP_ADMIN` : direction d'un groupe de sites."
          },
          "primarySiteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site principal de la personne. Nul s'il n'est pas renseigné ou si ce site n'est pas ouvert à l'application.",
            "nullable": true
          },
          "siteIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Sites où la personne exerce, parmi les sites lus par l'appel (tous les sites ouverts, ou le seul site demandé par `site_id`). Jamais vide."
          }
        },
        "required": [
          "id",
          "firstName",
          "lastName",
          "role",
          "primarySiteId",
          "siteIds"
        ],
        "description": "Un membre actif de l'équipe, de quoi désigner le praticien d'un rendez-vous. Ni email, ni téléphone, ni droits."
      },
      "Task": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la tâche."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site de la tâche."
          },
          "title": {
            "type": "string",
            "description": "Titre."
          },
          "description": {
            "type": "string",
            "description": "Description. Nulle sans description.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "TODO",
              "IN_PROGRESS",
              "DONE",
              "CANCELLED"
            ],
            "description": "`TODO` : à faire ; `IN_PROGRESS` : en cours ; `DONE` : faite ; `CANCELLED` : annulée par l'équipe.",
            "nullable": true
          },
          "priority": {
            "type": "string",
            "enum": [
              "LOW",
              "MEDIUM",
              "HIGH",
              "URGENT"
            ],
            "description": "Priorité : basse, moyenne, haute, urgente.",
            "nullable": true
          },
          "dueAt": {
            "type": "string",
            "format": "date-time",
            "description": "Échéance. Seul le jour compte si `dateOnly` vaut `true`. Nulle sans échéance.",
            "nullable": true
          },
          "dateOnly": {
            "type": "boolean",
            "description": "`true` : l'échéance est un jour, sans heure."
          },
          "durationMinutes": {
            "type": "integer",
            "description": "Durée prévue en minutes. Nulle pour une tâche à la journée créée sans durée.",
            "nullable": true
          },
          "assignees": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid",
                  "description": "Identifiant de la personne."
                },
                "name": {
                  "type": "string",
                  "description": "Prénom et nom. Nul si la personne n'est plus lisible.",
                  "nullable": true
                }
              },
              "required": [
                "id",
                "name"
              ]
            },
            "description": "Personnes assignées, dans l'ordre. Vide : tâche du pool de l'équipe, que n'importe qui peut prendre."
          },
          "patientId": {
            "type": "string",
            "format": "uuid",
            "description": "Animal concerné. Nul sans animal.",
            "nullable": true
          },
          "clientId": {
            "type": "string",
            "format": "uuid",
            "description": "Client concerné. Nul sans client.",
            "nullable": true
          },
          "createdBy": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Identifiant de la personne."
              },
              "name": {
                "type": "string",
                "description": "Prénom et nom. Nul si la personne n'est plus lisible.",
                "nullable": true
              }
            },
            "required": [
              "id",
              "name"
            ],
            "description": "Auteur de la tâche : l'utilisateur d'intégration de l'application pour une tâche créée par l'API. Nul si l'auteur n'est pas connu.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de création.",
            "nullable": true
          },
          "completedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date à laquelle la tâche a été faite. Nulle tant qu'elle ne l'est pas.",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de la dernière modification, clé de synchronisation de `GET /tasks`."
          }
        },
        "required": [
          "id",
          "siteId",
          "title",
          "description",
          "status",
          "priority",
          "dueAt",
          "dateOnly",
          "durationMinutes",
          "assignees",
          "patientId",
          "clientId",
          "createdBy",
          "createdAt",
          "completedAt",
          "updatedAt"
        ],
        "description": "Une tâche d'équipe d'un site."
      },
      "TokenError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "invalid_request",
              "invalid_client",
              "unauthorized_client",
              "unsupported_grant_type",
              "slow_down"
            ],
            "description": "Code OAuth. `unauthorized_client` : l'organisation n'a pas connecté l'application (ou l'a déconnectée)."
          },
          "error_description": {
            "type": "string",
            "description": "Explication en français."
          },
          "treats_code": {
            "type": "string",
            "description": "Code Treats précis : `INVALID_CLIENT`, `NO_GRANT`, `PARTNER_APP_SUSPENDED`, `INVALID_ORGANIZATION_ID`, `RATE_LIMITED`…"
          }
        },
        "required": [
          "error",
          "error_description",
          "treats_code"
        ],
        "description": "Refus d'une demande de jeton, au format OAuth 2 (RFC 6749 §5.2), lisible par les bibliothèques OAuth."
      },
      "WaitingRoomEntry": {
        "type": "object",
        "properties": {
          "appointmentId": {
            "type": "string",
            "format": "uuid",
            "description": "Rendez-vous de la visite."
          },
          "patientId": {
            "type": "string",
            "format": "uuid",
            "description": "Animal.",
            "nullable": true
          },
          "patientName": {
            "type": "string",
            "description": "Nom de l'animal.",
            "nullable": true
          },
          "clientId": {
            "type": "string",
            "format": "uuid",
            "description": "Client.",
            "nullable": true
          },
          "clientName": {
            "type": "string",
            "description": "Nom du client, tel que la clinique l'affiche.",
            "nullable": true
          },
          "vetId": {
            "type": "string",
            "format": "uuid",
            "description": "Vétérinaire du rendez-vous.",
            "nullable": true
          },
          "vetName": {
            "type": "string",
            "description": "Nom du vétérinaire.",
            "nullable": true
          },
          "appointmentTypeId": {
            "type": "string",
            "format": "uuid",
            "description": "Type de rendez-vous.",
            "nullable": true
          },
          "startAt": {
            "type": "string",
            "format": "date-time",
            "description": "Heure prévue du rendez-vous."
          },
          "arrivedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Heure d'arrivée de l'animal."
          },
          "waitingMinutes": {
            "type": "integer",
            "description": "Minutes écoulées depuis l'arrivée, au moment de l'appel."
          },
          "urgent": {
            "type": "boolean",
            "description": "`true` : rendez-vous marqué urgent."
          }
        },
        "required": [
          "appointmentId",
          "patientId",
          "patientName",
          "clientId",
          "clientName",
          "vetId",
          "vetName",
          "appointmentTypeId",
          "startAt",
          "arrivedAt",
          "waitingMinutes",
          "urgent"
        ],
        "description": "Un animal en salle d'attente."
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de la livraison, égal à l'`id` du corps envoyé et à l'en-tête `Treats-Event-Id`."
          },
          "type": {
            "type": "string",
            "enum": [
              "appointment.created",
              "appointment.updated",
              "appointment.cancelled",
              "appointment.no_show",
              "appointment.completed",
              "patient.checked_in",
              "client.created",
              "patient.created",
              "patient.deceased",
              "consultation.started",
              "consultation.validated",
              "invoice.issued",
              "invoice.paid",
              "payment.recorded",
              "credit_note.issued",
              "prescription.signed",
              "lab.result.received",
              "ping"
            ],
            "description": "Type de l'événement."
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time",
            "description": "Instant où le fait s'est produit dans Treats."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site de l'événement (`null` pour un ping ou un événement sans site).",
            "nullable": true
          },
          "data": {
            "type": "object",
            "properties": {
              "object": {
                "type": "string",
                "enum": [
                  "appointment",
                  "client",
                  "patient",
                  "consultation",
                  "invoice",
                  "payment",
                  "credit_note",
                  "prescription",
                  "lab_result",
                  "ping"
                ],
                "description": "Nature de la ressource."
              },
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Identifiant de la ressource. `null` pour un ping.",
                "nullable": true
              },
              "publicId": {
                "type": "string",
                "description": "Identifiant public (`apt_…`, `cli_…`, `pat_…`, `cns_…`) quand la ressource en a un et qu'il est connu.",
                "nullable": true
              }
            },
            "description": "La ressource désignée."
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "SENDING",
              "DELIVERED",
              "FAILED",
              "DEAD",
              "MUTED",
              "CANCELLED"
            ],
            "description": "`PENDING` : en file. `SENDING` : envoi en cours. `DELIVERED` : votre endpoint a répondu 2xx. `FAILED` : échec, nouvelle tentative prévue à `nextAttemptAt`. `DEAD` : six tentatives en échec (ou ping en échec), plus de reprise automatique. `MUTED` : organisation de test et application de production, rien n'est envoyé. `CANCELLED` : rien n'est envoyé, `lastError` dit pourquoi (droit, site ou endpoint retiré entre-temps, application de recette face à une organisation réelle…)."
          },
          "attempts": {
            "type": "integer",
            "description": "Tentatives comptées depuis la production ou le dernier rejeu (remis à 0 par un rejeu)."
          },
          "nextAttemptAt": {
            "type": "string",
            "format": "date-time",
            "description": "Prochaine tentative prévue. Renseigné seulement pour `PENDING` et `FAILED`.",
            "nullable": true
          },
          "lastAttemptAt": {
            "type": "string",
            "format": "date-time",
            "description": "Dernière requête envoyée à l'endpoint.",
            "nullable": true
          },
          "lastStatusCode": {
            "type": "integer",
            "description": "Code HTTP de la dernière réponse reçue. `null` si aucune réponse n'est arrivée (délai dépassé, connexion impossible, adresse refusée).",
            "nullable": true
          },
          "lastError": {
            "type": "string",
            "description": "Dernière erreur, en français : `HTTP 503`, `Délai dépassé (10 s)`, `Connexion impossible : …`, `WEBHOOK_URL_FORBIDDEN : …`, ou une raison d'annulation préfixée par son code (`GRANT_REVOKED : …`, `SCOPE_WITHDRAWN : …`, `SITE_NOT_COVERED : …`, `TYPE_UNSUBSCRIBED : …`, `ORGANIZATION_MUTED : …`, `SANDBOX_ORG_LIVE : …`…). `null` si tout s'est bien passé.",
            "nullable": true
          },
          "deliveredAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de la livraison réussie.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Production de la livraison."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Dernier changement de la livraison (clé du tri et de `updated_since`)."
          }
        },
        "description": "Une livraison : l'envoi d'un événement à l'endpoint de l'application, pour une connexion (une organisation)."
      },
      "WebhookEndpoint": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de l'endpoint."
          },
          "url": {
            "type": "string",
            "description": "Adresse HTTPS qui reçoit les événements."
          },
          "types": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Types d'événement abonnés, dans l'ordre de `GET /webhooks/event-types`. Tous les types quand aucun n'a été choisi."
          },
          "active": {
            "type": "boolean",
            "description": "`false` : endpoint désactivé par Treats après trois jours d'échecs continus. Plus rien n'est produit pour lui ; `PUT /webhooks/endpoint` le réactive."
          },
          "disabledAt": {
            "type": "string",
            "format": "date-time",
            "description": "Date de la désactivation. `null` si l'endpoint est actif.",
            "nullable": true
          },
          "disabledReason": {
            "type": "string",
            "description": "Raison de la désactivation, en français (début de la série d'échecs et dernière erreur). `null` si l'endpoint est actif.",
            "nullable": true
          },
          "failingSince": {
            "type": "string",
            "format": "date-time",
            "description": "Début de la série d'échecs en cours. Remis à `null` à la première livraison réussie (ping compris) et par `PUT /webhooks/endpoint`.",
            "nullable": true
          },
          "previousSecretValidUntil": {
            "type": "string",
            "format": "date-time",
            "description": "Après une rotation du secret : fin de la période pendant laquelle l'ancien secret signe encore. `null` hors de cette période.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Création de l'endpoint."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Dernière modification (adresse, types, rotation du secret, désactivation)."
          }
        },
        "description": "Le point de réception des webhooks de l'application. Un seul par application, commun à toutes les organisations connectées. Le secret de signature n'y est jamais rendu."
      },
      "WebhookEndpointConfigured": {
        "type": "object",
        "properties": {
          "endpoint": {
            "allOf": [
              {
                "$ref": "#/components/schemas/WebhookEndpoint"
              }
            ],
            "description": "L'endpoint tel qu'il est enregistré."
          },
          "secret": {
            "type": "string",
            "description": "Secret de signature (`whsec_…`), rendu UNE seule fois, à la création (201). `null` sur une modification (200). À conserver côté serveur : Treats ne le rend plus jamais.",
            "nullable": true
          },
          "created": {
            "type": "boolean",
            "description": "`true` : l'endpoint vient d'être créé (201, secret rendu). `false` : endpoint existant modifié (200)."
          }
        },
        "description": "Réponse de `PUT /webhooks/endpoint`."
      },
      "WebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifiant de l'événement pour cette connexion (une organisation et votre application). Égal à l'en-tête `Treats-Event-Id` et à l'`id` de la livraison dans `GET /webhooks/deliveries`. Stable entre les tentatives et les rejeux : c'est la clé de dédoublonnage."
          },
          "type": {
            "type": "string",
            "enum": [
              "appointment.created",
              "appointment.updated",
              "appointment.cancelled",
              "appointment.no_show",
              "appointment.completed",
              "patient.checked_in",
              "client.created",
              "patient.created",
              "patient.deceased",
              "consultation.started",
              "consultation.validated",
              "invoice.issued",
              "invoice.paid",
              "payment.recorded",
              "credit_note.issued",
              "prescription.signed",
              "lab.result.received",
              "ping"
            ],
            "description": "Type de l'événement, égal à l'en-tête `Treats-Event-Type`. `ping` : envoyé seulement par `POST /webhooks/test`."
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time",
            "description": "Instant où le fait s'est produit dans Treats (pas l'instant de l'envoi). Précision variable, jusqu'à la nanoseconde."
          },
          "organizationId": {
            "type": "string",
            "format": "uuid",
            "description": "Organisation de l'événement : celle dont vous demandez le jeton pour relire la ressource."
          },
          "siteId": {
            "type": "string",
            "format": "uuid",
            "description": "Site de l'événement. Pour `patient.created` et `patient.deceased` : le site d'origine du propriétaire de l'animal. `null` pour un `ping` et pour un événement sans site (animal sans propriétaire).",
            "nullable": true
          },
          "data": {
            "type": "object",
            "properties": {
              "object": {
                "type": "string",
                "enum": [
                  "appointment",
                  "client",
                  "patient",
                  "consultation",
                  "invoice",
                  "payment",
                  "credit_note",
                  "prescription",
                  "lab_result",
                  "ping"
                ],
                "description": "Nature de la ressource. `ping` pour un ping."
              },
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Identifiant de la ressource, à relire sur l'API. Absent pour un `ping`."
              },
              "publicId": {
                "type": "string",
                "description": "Identifiant public de la ressource (`apt_…`, `cli_…`, `pat_…`, `cns_…`), présent seulement pour un rendez-vous, un client, un animal ou une consultation qui en a un. Il peut manquer à une tentative et apparaître à la suivante."
              }
            },
            "required": [
              "object"
            ],
            "description": "La ressource désignée."
          }
        },
        "required": [
          "id",
          "type",
          "occurredAt",
          "organizationId",
          "siteId",
          "data"
        ],
        "description": "Corps JSON que Treats envoie en `POST` à l'endpoint de l'application, pour chaque événement. JSON compact, champs dans cet ordre. La signature porte sur ce corps brut, octet pour octet : ne le re-sérialisez pas avant de la vérifier."
      },
      "WebhookEventType": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "appointment.created",
              "appointment.updated",
              "appointment.cancelled",
              "appointment.no_show",
              "appointment.completed",
              "patient.checked_in",
              "client.created",
              "patient.created",
              "patient.deceased",
              "consultation.started",
              "consultation.validated",
              "invoice.issued",
              "invoice.paid",
              "payment.recorded",
              "credit_note.issued",
              "prescription.signed",
              "lab.result.received"
            ],
            "description": "Nom du type."
          },
          "object": {
            "type": "string",
            "enum": [
              "appointment",
              "client",
              "patient",
              "consultation",
              "invoice",
              "payment",
              "credit_note",
              "prescription",
              "lab_result"
            ],
            "description": "Nature de la ressource désignée par `data.object`."
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Droits dont UN suffit, en plus de `webhooks`, pour recevoir ce type (deux pour `lab.result.received` : `consultations:read` ou `lab:write`)."
          }
        },
        "description": "Un type d'événement auquel un endpoint peut s'abonner."
      },
      "WebhookSecret": {
        "type": "object",
        "properties": {
          "secret": {
            "type": "string",
            "description": "Nouveau secret (`whsec_…`), rendu une seule fois. `null` quand la réponse est rejouée par la même `Idempotency-Key` : le secret n'est jamais conservé en clair ; refaites une rotation avec une nouvelle clé si la réponse a été perdue.",
            "nullable": true
          },
          "previousSecretValidUntil": {
            "type": "string",
            "format": "date-time",
            "description": "L'ancien secret signe encore jusqu'à cet instant (24 heures après la rotation)."
          },
          "secretRedacted": {
            "type": "boolean",
            "description": "Présent et `true` seulement sur une réponse rejouée par la même `Idempotency-Key` : le secret a été masqué."
          }
        },
        "description": "Réponse de la rotation du secret."
      }
    }
  },
  "tags": [
    {
      "name": "Accès",
      "description": "Obtenir un jeton pour une organisation qui a connecté l'application, puis lire ce que ce jeton ouvre : l'organisation, les sites et les droits effectifs. Le jeton vaut 60 minutes ; réutilisez-le jusqu'à son expiration plutôt que d'en demander un à chaque appel.",
      "x-treats-slug": "acces",
      "x-treats-order": 0
    },
    {
      "name": "Organisation",
      "description": "Lire les référentiels de l'organisation connectée : ses sites (coordonnées, horaires d'ouverture, jours fériés), son équipe, ses salles, les espèces et races qu'elle propose. Toutes ces routes exigent le droit `org:read` et sont en lecture seule. Ce sont des listes courtes, rendues en une seule réponse : `nextCursor` vaut toujours `null`. Elles ne voient que les sites que l'organisation a ouverts à l'application ; un autre site répond `404 SITE_NOT_FOUND`, jamais `403`.",
      "x-treats-slug": "organisation",
      "x-treats-order": 10
    },
    {
      "name": "Clients",
      "description": "Lire, rechercher, créer et modifier les clients (propriétaires) d'une organisation, et vérifier un doublon avant une création. Un client appartient à l'organisation entière, pas à un site ; il garde son site d'origine (`siteId`), le site où sa fiche a été créée.\n\nRègle de visibilité, commune à toute l'API : une organisation qui a ouvert tous ses sites vous montre tous ses clients. Une organisation qui ne vous a ouvert que certains sites vous montre seulement les clients nés sur ces sites (et leurs animaux), même si un client né ailleurs vient se faire soigner sur un site ouvert. Hors de ce périmètre, tout répond `404 CLIENT_NOT_FOUND`, jamais `403`.\n\nBrouillon seulement : l'API ne fusionne, ne désactive ni ne supprime aucun client, et ne donne jamais d'accord de contact (email, SMS, courrier) ; ces gestes restent à la clinique.",
      "x-treats-slug": "clients",
      "x-treats-order": 20
    },
    {
      "name": "Animaux",
      "description": "Lire, rechercher, créer et modifier l'identité des animaux (patients) d'une organisation, et lire leurs pesées. Un animal suit la visibilité de son propriétaire : vous le voyez si vous voyez son client (organisation entière si tous les sites vous sont ouverts, sinon client né sur un site ouvert) ; sinon `404 PATIENT_NOT_FOUND`.\n\nIdentité seulement : le dossier médical n'est pas exposé ici. L'API ne déclare pas de décès, ne transfère pas un animal à un autre propriétaire, ne fusionne ni ne supprime d'animal ; ces gestes restent à la clinique.",
      "x-treats-slug": "animaux",
      "x-treats-order": 25
    },
    {
      "name": "Agenda",
      "description": "Lire et synchroniser l'agenda des sites ouverts à l'application, chercher des créneaux libres, prendre, déplacer, annuler un rendez-vous et pointer l'arrivée d'un animal. Lectures : droit `agenda:read` ; écritures : `agenda:write`. Toute prise ou tout déplacement de rendez-vous est jugé en politique stricte : la moindre contrainte non respectée (site fermé, vétérinaire hors planning ou absent, créneau pris, date passée…) refuse l'appel en `409 SLOT_CONFLICT`, même si la clinique accepte ce cas en simple avertissement dans son propre agenda. Une clinique qui n'a renseigné ni les horaires de ses sites ni le planning de ses vétérinaires ne peut donc recevoir aucun rendez-vous par l'API. L'application agit sous un utilisateur d'intégration à son nom : il apparaît comme créateur du rendez-vous, auteur de sa note, de son annulation et de l'arrivée.",
      "x-treats-slug": "agenda",
      "x-treats-order": 30
    },
    {
      "name": "Notes d'appel et tâches",
      "description": "Consigner un appel d'un client (demande de rendez-vous, question médicale, réclamation…) pour que l'accueil le traite, et créer des tâches pour l'équipe d'un site. Lectures : droit `agenda:read` ; écritures : `agenda:write`. L'application agit sous son utilisateur d'intégration, auteur des notes et des tâches qu'elle crée. Une tâche créée par l'API est toujours une tâche d'équipe ; les tâches personnelles des membres de l'équipe ne sont jamais visibles. Aucune note d'appel ni tâche ne se supprime par l'API, et ces écritures n'émettent pas de notification.",
      "x-treats-slug": "appels-taches",
      "x-treats-order": 35
    },
    {
      "name": "Consultations",
      "description": "Ouvrir une consultation en brouillon, remplir ses modules cliniques (anamnèse, commémoratifs, examen général, diagnostic, comportement), ajouter une note d'équipe et lire l'historique clinique d'un animal. Un partenaire ne valide jamais une consultation, ne la déverrouille ni ne la supprime, et n'écrit ni acte, ni produit, ni ordonnance : ces gestes restent au vétérinaire, dans Treats. Les modules d'actes, la vaccination et les mesures (température, pression, glycémie) se lisent en texte seulement. Une consultation d'un site non ouvert à l'application répond `404`, jamais `403`.",
      "x-treats-slug": "consultations",
      "x-treats-order": 40
    },
    {
      "name": "Documents",
      "description": "Lire les documents du dossier d'un animal ou d'une consultation (comptes rendus, ordonnances, certificats, fichiers déposés…), les télécharger, et déposer un PDF (compte rendu de laboratoire, d'imagerie…) dans le dossier d'un animal. Un document déposé par l'API naît interne : c'est la clinique qui décide de le montrer au propriétaire. Seuls les documents des sites ouverts à l'application sont visibles ; un autre répond `404`, jamais `403`. Aucun antivirus n'analyse les fichiers déposés : seuls le type déclaré et la signature PDF des octets sont vérifiés.",
      "x-treats-slug": "documents",
      "x-treats-order": 45
    },
    {
      "name": "Catalogue",
      "description": "Lire le catalogue de la clinique (actes et produits, appelés Treats dans l'application) avec le prix pratiqué par un site, l'arbre des catégories, et chercher un acte ou un produit par son nom. Lecture seule : aucune route ne crée ni ne modifie un article, une catégorie ou un prix. Seul le catalogue propre à l'organisation est visible, jamais le référentiel des centrales. Droit exigé : `catalog:read`.",
      "x-treats-slug": "catalogue",
      "x-treats-order": 50
    },
    {
      "name": "Facturation",
      "description": "Lire les factures émises, les avoirs et les encaissements des sites ouverts à l'application, avec leur PDF officiel. Lecture seule : un partenaire ne crée, ne modifie, n'émet, n'annule ni n'encaisse aucune facture, et ne crée aucun avoir ni paiement ; tout cela se fait dans l'application de la clinique. Les brouillons de facture et les pièces reprises d'un ancien logiciel qui ne sont pas des factures (devis, bons de livraison, factures provisoires) ne sont jamais exposés. Montants en centimes entiers, toujours avec leur devise ; les totaux d'une pièce sont ceux du document émis, jamais recalculés. Droit exigé : `billing:read`. Notifications associées : `invoice.issued`, `invoice.paid`, `payment.recorded`, `credit_note.issued`.",
      "x-treats-slug": "facturation",
      "x-treats-order": 60
    },
    {
      "name": "Ordonnances",
      "description": "Lire les ordonnances signées par les vétérinaires des sites ouverts à l'application, leurs lignes et leur PDF officiel. Seules les ordonnances signées sont exposées, y compris celles annulées après signature ; un brouillon n'est pas une ordonnance tant que le vétérinaire ne l'a pas signée (article R.5141-111 du code de la santé publique), même s'il a été préparé ou validé par l'équipe. Lecture seule : l'API ne crée, ne modifie, ne signe ni n'annule aucune ordonnance. Droit exigé : `prescriptions:read`. Notification associée : `prescription.signed`.",
      "x-treats-slug": "ordonnances",
      "x-treats-order": 65
    },
    {
      "name": "Laboratoire",
      "description": "Pour un laboratoire d'analyses : lire les commandes que les cliniques lui adressent, puis déposer les résultats structurés (lignes d'analyses, valeurs, unités, bornes, drapeaux) et leur compte rendu PDF. Treats rattache le résultat à la commande ou à l'animal par un identifiant sûr (commande, identifiant de l'animal, puce), jamais par le nom ; en cas de doute, le résultat attend dans la file « Résultats à rattacher » de la clinique. L'application doit avoir été rattachée par Treats au laboratoire qu'elle représente (droit `lab:write`) : elle ne voit que les commandes et les résultats de ce laboratoire, sur les sites ouverts par l'organisation.",
      "x-treats-slug": "laboratoire",
      "x-treats-order": 70
    },
    {
      "name": "Imagerie",
      "description": "Pour un fournisseur d'imagerie (radiographie, échographie, PACS) : lire les demandes d'examen que les cliniques lui adressent, puis déposer les résultats (images JPEG ou PNG, compte rendu PDF, lien vers l'étude). Les fichiers entrent dans le dossier de l'animal et la consultation est prévenue. L'application doit avoir été rattachée par Treats au fournisseur qu'elle représente (droit `imaging:write`) : elle ne voit que les demandes adressées à ce fournisseur, sur les sites ouverts par l'organisation. L'API ne crée ni n'annule de demande : c'est la clinique qui les émet.",
      "x-treats-slug": "imagerie",
      "x-treats-order": 75
    },
    {
      "name": "Notifications",
      "description": "Recevoir un appel signé (webhook) dès qu'une ressource que l'application a le droit de lire change, au lieu d'interroger l'API en boucle. Chaque application a UN point de réception (endpoint), commun à toutes les organisations qui l'ont connectée ; l'organisation de chaque événement est dans son corps. Le corps est mince (type, organisation, site, identifiant de la ressource) et ne porte aucune donnée personnelle : relisez la ressource sur l'API avec vos droits du moment. Toutes les routes de ce domaine exigent le droit `webhooks` ; recevoir un type exige en plus le droit de lecture de sa ressource.",
      "x-treats-slug": "notifications",
      "x-treats-order": 80
    }
  ],
  "paths": {
    "/api/partner/v1/appointment-subtypes": {
      "get": {
        "tags": [
          "Agenda"
        ],
        "summary": "Lister les motifs de rendez-vous",
        "description": "Rend les motifs actifs d'un site, dans l'ordre d'affichage choisi par la clinique, éventuellement limités à un type. Un motif précise un type et peut fixer sa propre durée, une salle par défaut et une espèce. Les motifs désactivés par la clinique ne sont pas rendus. Une seule page : `nextCursor` vaut toujours `null`.",
        "operationId": "GET_api_partner_v1_appointment_subtypes",
        "security": [
          {
            "partner": [
              "agenda:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:read"
          ]
        },
        "x-treats-controller": "PartnerAppointmentSubtypeController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Site lu. Obligatoire (ici ou dans l'en-tête) si l'organisation a ouvert plusieurs sites à l'application.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "appointment_type_id",
            "in": "query",
            "description": "Seulement les motifs de ce type, qui doit appartenir au site.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les motifs actifs du site.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AppointmentSubtype"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`appointment_type_id` n'est pas un UUID."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND · APPOINTMENT_TYPE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              },
              {
                "code": "APPOINTMENT_TYPE_NOT_FOUND",
                "description": "`appointment_type_id` : type inconnu ou d'un autre site."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/appointment-subtypes?site_id=84b9ad0c-5917-4d16-a75c-d3dc97abe595"
          },
          "response": {
            "body": {
              "items": [
                {
                  "appointmentTypeId": "d2982217-f30f-4d2e-bec2-4bda58da3ca0",
                  "defaultRoomId": null,
                  "durationMinutes": 20,
                  "id": "23af829a-273a-49e6-a24e-d6b33f85a0d2",
                  "name": "motif",
                  "species": null
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/appointment-types": {
      "get": {
        "tags": [
          "Agenda"
        ],
        "summary": "Lister les types de rendez-vous",
        "description": "Rend les types de rendez-vous d'un site (consultation, vaccination, chirurgie…), avec leur durée par défaut. Ce sont les valeurs acceptées en `appointmentTypeId`. Liste courte, sans ordre garanti, en une seule page : `nextCursor` vaut toujours `null`.",
        "operationId": "GET_api_partner_v1_appointment_types",
        "security": [
          {
            "partner": [
              "agenda:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:read"
          ]
        },
        "x-treats-controller": "PartnerAppointmentTypeController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Site lu. Obligatoire (ici ou dans l'en-tête) si l'organisation a ouvert plusieurs sites à l'application.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les types de rendez-vous du site.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AppointmentType"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/appointment-types?site_id=84b9ad0c-5917-4d16-a75c-d3dc97abe595"
          },
          "response": {
            "body": {
              "items": [
                {
                  "durationMinutes": 45,
                  "id": "0a13f61f-52b8-4881-afb0-d63c054104e7",
                  "name": "Urgence"
                },
                {
                  "durationMinutes": 20,
                  "id": "2bda4c9e-123f-47b4-948a-6643681cbcb1",
                  "name": "Suivi"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/appointments": {
      "get": {
        "tags": [
          "Agenda"
        ],
        "summary": "Lister les rendez-vous",
        "description": "Rend les rendez-vous des sites ouverts à l'application, ou du seul site demandé, annulés compris, triés par date de modification (`updatedAt`) puis par identifiant. C'est la route de synchronisation de l'agenda : parcourez les pages avec `cursor`, puis repartez de `updated_since` égal au plus grand `updatedAt` vu, moins 5 minutes, et dédoublonnez par `id`. Une écriture n'apparaît dans cette liste qu'au bout de 60 secondes environ ; `GET /appointments/{id}` la rend tout de suite.\n\nLes filtres se cumulent. `from` et `to` portent sur le début du rendez-vous (`from` inclus, `to` exclu). `client_id` rend aussi les rendez-vous sans client rattaché dont l'animal appartient à ce client. Un filtre sur un client, un animal ou un vétérinaire que l'application ne peut pas lire répond `404`, jamais une liste vide.",
        "operationId": "GET_api_partner_v1_appointments",
        "security": [
          {
            "partner": [
              "agenda:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:read"
          ]
        },
        "x-treats-controller": "PartnerAppointmentController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Limite la lecture à ce site. Sans lui ni l'en-tête : tous les sites ouverts à l'application. Le paramètre l'emporte sur l'en-tête.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Rendez-vous qui commencent à partir de cet instant (inclus). Années 1900 à 9999.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Rendez-vous qui commencent avant cet instant (exclu). Postérieur à `from` quand les deux sont donnés. Années 1900 à 9999.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "vet_id",
            "in": "query",
            "description": "Rendez-vous de ce vétérinaire. Il doit appartenir à l'organisation.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "patient_id",
            "in": "query",
            "description": "Rendez-vous de cet animal.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "client_id",
            "in": "query",
            "description": "Rendez-vous de ce client, ou de l'un de ses animaux quand le rendez-vous n'a pas de client rattaché.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Rendez-vous modifiés à partir de cet instant (inclus). Années 1900 à 9999.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Curseur opaque rendu par la page précédente (`nextCursor`). Absent : première page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Nombre de rendez-vous par page. Une valeur au-delà de 200 est ramenée à 200.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page de rendez-vous, par `updatedAt` puis `id` croissants.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Appointment"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "Paramètre illisible (UUID, instant, `limit` inférieur à 1, `cursor` invalide), date hors des années 1900 à 9999, ou `to` antérieur ou égal à `from`. Un détail par paramètre."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND · CLIENT_NOT_FOUND · PATIENT_NOT_FOUND · VET_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              },
              {
                "code": "CLIENT_NOT_FOUND",
                "description": "`client_id` : client inconnu, d'une autre organisation, ou que l'application ne peut pas lire."
              },
              {
                "code": "PATIENT_NOT_FOUND",
                "description": "`patient_id` : animal inconnu, d'une autre organisation, ou dont le client n'est pas lisible par l'application."
              },
              {
                "code": "VET_NOT_FOUND",
                "description": "`vet_id` : vétérinaire inconnu ou d'une autre organisation."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/appointments?site_id=84b9ad0c-5917-4d16-a75c-d3dc97abe595&patient_id=e7ab5f80-eeeb-4445-8169-774346054354&from=2027-06-20T22:00:00Z&to=2027-06-21T22:00:00Z"
          },
          "response": {
            "body": {
              "items": [
                {
                  "appointmentSubtypeId": null,
                  "appointmentTypeId": "d2982217-f30f-4d2e-bec2-4bda58da3ca0",
                  "arrivedAt": null,
                  "calledAt": null,
                  "cancellation": null,
                  "category": null,
                  "clientId": "31ba4810-584b-4ebb-9f4e-b99cc9007c2d",
                  "createdAt": "2026-10-15T02:04:05.274243Z",
                  "durationMinutes": 30,
                  "endAt": "2027-06-21T08:30:00Z",
                  "id": "8b6d02c4-7ac9-42bd-a9f5-5b251d47f82b",
                  "note": "Note partenaire",
                  "noteAuthor": {
                    "id": "14deba83-08d1-4fd3-98c4-7908063dee3b",
                    "name": "Intégration Agenda"
                  },
                  "noteUpdatedAt": "2026-10-15T02:04:05.274243Z",
                  "patientId": "e7ab5f80-eeeb-4445-8169-774346054354",
                  "publicId": "apt_apwmt2f86",
                  "roomId": "a1ceecfa-cd26-4bd3-aae0-f26be2c8f722",
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "startAt": "2027-06-21T08:00:00Z",
                  "updatedAt": "2026-10-15T02:04:05.275718Z",
                  "urgent": false,
                  "vetId": "bf5c3a90-ea0b-4483-832b-d9fe907fa99e",
                  "workflow": "PLANNED"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      },
      "post": {
        "tags": [
          "Agenda"
        ],
        "summary": "Prendre un rendez-vous",
        "description": "Crée un rendez-vous sur le site donné par `site_id` ou `Treats-Site-Id` (implicite si un seul site est ouvert). Le vétérinaire doit faire partie de l'équipe du site ; le type, le motif et la salle doivent appartenir au site ; l'animal doit appartenir au client et ne pas être décédé. La durée retenue est `durationMinutes`, sinon celle de la prestation rattachée au motif, sinon celle du motif, sinon celle du type ; la salle est `roomId`, sinon la salle par défaut du motif.\n\nAvant d'écrire, le créneau est jugé en politique stricte : toute contrainte non respectée refuse l'appel en `409 SLOT_CONFLICT` (voir `POST /availability/evaluate` pour le jugement sans écriture). Le rendez-vous est créé en `workflow: PLANNED`, au nom de l'utilisateur d'intégration de l'application. Le client n'est prévenu que sur demande (`notifyClient: true`) ; cela ne commande que le message de confirmation immédiat, les rappels programmés par la clinique suivent ses propres réglages. Le jugement du créneau et l'écriture ne sont pas atomiques : deux créations simultanées sur le même créneau peuvent être acceptées si la clinique tolère les chevauchements.",
        "operationId": "POST_api_partner_v1_appointments",
        "security": [
          {
            "partner": [
              "agenda:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:write"
          ]
        },
        "x-treats-controller": "PartnerAppointmentController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Site du rendez-vous. Obligatoire (ici ou dans l'en-tête) si l'organisation a ouvert plusieurs sites à l'application.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "clientId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Client (propriétaire). Il doit être lisible par l'application."
                  },
                  "patientId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Animal. Il doit appartenir à `clientId` et ne pas être décédé."
                  },
                  "appointmentTypeId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Type de rendez-vous du site (voir `GET /appointment-types`)."
                  },
                  "appointmentSubtypeId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Motif, qui doit appartenir au type (voir `GET /appointment-subtypes`). Il peut fixer la durée et la salle par défaut."
                  },
                  "vetId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Vétérinaire, membre de l'équipe du site (voir `GET /staff`)."
                  },
                  "roomId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Salle du site (voir `GET /rooms`). Absente : salle par défaut du motif, ou aucune."
                  },
                  "startAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Début du rendez-vous. Années 1900 à 9999 ; un instant passé est refusé (`SLOT_CONFLICT`, `PAST_DATE`)."
                  },
                  "durationMinutes": {
                    "type": "integer",
                    "description": "Durée en minutes. Absente : durée du motif ou du type.",
                    "minimum": 5,
                    "maximum": 480
                  },
                  "note": {
                    "type": "string",
                    "description": "Note du rendez-vous, visible de l'équipe. Une chaîne vide vaut absence de note.",
                    "maxLength": 5000
                  },
                  "notifyClient": {
                    "type": "boolean",
                    "description": "`true` : prévenir le client de la prise de rendez-vous. Accepté seulement si le client a donné à la clinique son accord pour être contacté par email ou par SMS.",
                    "default": false
                  }
                },
                "required": [
                  "clientId",
                  "patientId",
                  "appointmentTypeId",
                  "vetId",
                  "startAt"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Le rendez-vous créé.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Appointment"
                }
              }
            }
          },
          "400": {
            "description": "BODY_REQUIRED · INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "BODY_REQUIRED",
                "description": "Corps absent."
              },
              {
                "code": "INVALID_FIELD",
                "description": "Champ absent, illisible ou inconnu (un détail par champ) ; `durationMinutes` hors de 5 à 480 ; `note` de plus de 5 000 caractères ; `patientId` qui n'appartient pas à `clientId` ; `appointmentSubtypeId` qui n'appartient pas au type ; `notifyClient: true` pour un client sans accord de contact."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND · CLIENT_NOT_FOUND · PATIENT_NOT_FOUND · VET_NOT_FOUND · APPOINTMENT_TYPE_NOT_FOUND · APPOINTMENT_SUBTYPE_NOT_FOUND · ROOM_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              },
              {
                "code": "CLIENT_NOT_FOUND",
                "description": "Client inconnu, d'une autre organisation, ou que l'application ne peut pas lire."
              },
              {
                "code": "PATIENT_NOT_FOUND",
                "description": "Animal inconnu, d'une autre organisation, ou dont le client n'est pas lisible par l'application."
              },
              {
                "code": "VET_NOT_FOUND",
                "description": "Vétérinaire qui n'est pas dans l'équipe du site."
              },
              {
                "code": "APPOINTMENT_TYPE_NOT_FOUND",
                "description": "Type de rendez-vous inconnu ou d'un autre site."
              },
              {
                "code": "APPOINTMENT_SUBTYPE_NOT_FOUND",
                "description": "Motif inconnu ou d'un autre site."
              },
              {
                "code": "ROOM_NOT_FOUND",
                "description": "Salle inconnue ou d'un autre site."
              }
            ]
          },
          "409": {
            "description": "PATIENT_DECEASED_READONLY · SLOT_CONFLICT",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PATIENT_DECEASED_READONLY",
                "description": "L'animal est décédé : aucun rendez-vous ne peut être pris pour lui."
              },
              {
                "code": "SLOT_CONFLICT",
                "description": "Le créneau ne respecte pas toutes les contraintes. `meta.reason` porte la première raison et `meta.violations` la liste complète (`[{reason, message}]`, raisons de `AvailabilityViolation`). Dans le cas rare d'un créneau pris entre le jugement et l'écriture, seul `meta.reason` est rendu."
              }
            ]
          }
        },
        "x-treats-webhooks": [
          "appointment.created"
        ],
        "x-treats-example": {
          "request": {
            "body": {
              "appointmentTypeId": "d2982217-f30f-4d2e-bec2-4bda58da3ca0",
              "clientId": "31ba4810-584b-4ebb-9f4e-b99cc9007c2d",
              "note": "Note partenaire",
              "notifyClient": false,
              "patientId": "e7ab5f80-eeeb-4445-8169-774346054354",
              "roomId": "a1ceecfa-cd26-4bd3-aae0-f26be2c8f722",
              "startAt": "2027-06-21T08:00:00Z",
              "vetId": "bf5c3a90-ea0b-4483-832b-d9fe907fa99e"
            },
            "method": "POST",
            "url": "/api/partner/v1/appointments?site_id=84b9ad0c-5917-4d16-a75c-d3dc97abe595"
          },
          "response": {
            "body": {
              "appointmentSubtypeId": null,
              "appointmentTypeId": "d2982217-f30f-4d2e-bec2-4bda58da3ca0",
              "arrivedAt": null,
              "calledAt": null,
              "cancellation": null,
              "category": null,
              "clientId": "31ba4810-584b-4ebb-9f4e-b99cc9007c2d",
              "createdAt": "2026-10-15T02:04:05.274243Z",
              "durationMinutes": 30,
              "endAt": "2027-06-21T08:30:00Z",
              "id": "8b6d02c4-7ac9-42bd-a9f5-5b251d47f82b",
              "note": "Note partenaire",
              "noteAuthor": {
                "id": "14deba83-08d1-4fd3-98c4-7908063dee3b",
                "name": "Intégration Agenda"
              },
              "noteUpdatedAt": "2026-10-15T02:04:05.274243Z",
              "patientId": "e7ab5f80-eeeb-4445-8169-774346054354",
              "publicId": "apt_apwmt2f86",
              "roomId": "a1ceecfa-cd26-4bd3-aae0-f26be2c8f722",
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "startAt": "2027-06-21T08:00:00Z",
              "updatedAt": "2026-10-15T02:04:05.275718Z",
              "urgent": false,
              "vetId": "bf5c3a90-ea0b-4483-832b-d9fe907fa99e",
              "workflow": "PLANNED"
            },
            "status": 201
          }
        }
      }
    },
    "/api/partner/v1/appointments/{id}": {
      "get": {
        "tags": [
          "Agenda"
        ],
        "summary": "Lire un rendez-vous",
        "description": "Rend un rendez-vous d'un site ouvert à l'application, quel que soit son état (annulé, terminé…). La lecture est immédiate : un rendez-vous qui vient d'être écrit est rendu tout de suite, sans le délai des listes. C'est la lecture à faire à la réception d'une notification `appointment.*` ou `patient.checked_in`. Un rendez-vous d'un site non ouvert répond `404`, comme un identifiant inconnu.",
        "operationId": "GET_api_partner_v1_appointments_id",
        "security": [
          {
            "partner": [
              "agenda:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:read"
          ]
        },
        "x-treats-controller": "PartnerAppointmentController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant du rendez-vous.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Le rendez-vous.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Appointment"
                }
              }
            }
          },
          "404": {
            "description": "APPOINTMENT_NOT_FOUND · NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "APPOINTMENT_NOT_FOUND",
                "description": "Rendez-vous inconnu, d'une autre organisation, ou d'un site non ouvert à l'application."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/appointments/8b6d02c4-7ac9-42bd-a9f5-5b251d47f82b"
          },
          "response": {
            "body": {
              "appointmentSubtypeId": null,
              "appointmentTypeId": "d2982217-f30f-4d2e-bec2-4bda58da3ca0",
              "arrivedAt": null,
              "calledAt": null,
              "cancellation": null,
              "category": null,
              "clientId": "31ba4810-584b-4ebb-9f4e-b99cc9007c2d",
              "createdAt": "2026-10-15T02:04:05.274243Z",
              "durationMinutes": 30,
              "endAt": "2027-06-21T08:30:00Z",
              "id": "8b6d02c4-7ac9-42bd-a9f5-5b251d47f82b",
              "note": "Note partenaire",
              "noteAuthor": {
                "id": "14deba83-08d1-4fd3-98c4-7908063dee3b",
                "name": "Intégration Agenda"
              },
              "noteUpdatedAt": "2026-10-15T02:04:05.274243Z",
              "patientId": "e7ab5f80-eeeb-4445-8169-774346054354",
              "publicId": "apt_apwmt2f86",
              "roomId": "a1ceecfa-cd26-4bd3-aae0-f26be2c8f722",
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "startAt": "2027-06-21T08:00:00Z",
              "updatedAt": "2026-10-15T02:04:05.275718Z",
              "urgent": false,
              "vetId": "bf5c3a90-ea0b-4483-832b-d9fe907fa99e",
              "workflow": "PLANNED"
            },
            "status": 200
          }
        }
      },
      "patch": {
        "tags": [
          "Agenda"
        ],
        "summary": "Modifier un rendez-vous",
        "description": "Déplace ou modifie un rendez-vous d'un site ouvert à l'application : champ absent = inchangé, champ à `null` = vidé (`roomId`, `appointmentSubtypeId`, `note` seulement). Le site, le client, l'animal, l'avancement (`workflow`) et la catégorie ne se modifient pas par cette route : les envoyer répond `400 INVALID_FIELD`. Pour annuler, utilisez `POST /appointments/{id}/cancel`.\n\nSi le début, la durée, le vétérinaire ou la salle changent, le nouveau créneau est jugé en politique stricte avant l'écriture (`409 SLOT_CONFLICT`), le rendez-vous lui-même étant ignoré dans le calcul des chevauchements. Changer de type ne change pas la durée. Un rendez-vous annulé, non honoré, terminé ou dont l'animal est décédé ne se modifie plus ; un rendez-vous à facturer (`TO_BILL`) ne se déplace plus. Le client n'est pas prévenu d'un déplacement par cette route. Un appel qui ne change rien rend le rendez-vous tel quel, sans notification.",
        "operationId": "PATCH_api_partner_v1_appointments_id",
        "security": [
          {
            "partner": [
              "agenda:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:write"
          ]
        },
        "x-treats-controller": "PartnerAppointmentController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant du rendez-vous.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "startAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Nouveau début. Ne peut pas être `null`."
                  },
                  "durationMinutes": {
                    "type": "integer",
                    "description": "Nouvelle durée en minutes. Ne peut pas être `null`.",
                    "minimum": 5,
                    "maximum": 480
                  },
                  "vetId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Nouveau vétérinaire, membre de l'équipe du site du rendez-vous. Ne peut pas être `null`."
                  },
                  "roomId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Nouvelle salle du site ; `null` retire la salle.",
                    "nullable": true
                  },
                  "appointmentTypeId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Nouveau type, du site du rendez-vous. Ne peut pas être `null`. Si le rendez-vous a un motif, donner aussi `appointmentSubtypeId` (motif du nouveau type, ou `null`)."
                  },
                  "appointmentSubtypeId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Nouveau motif, qui doit appartenir au type (nouveau ou actuel) ; `null` retire le motif.",
                    "nullable": true
                  },
                  "note": {
                    "type": "string",
                    "description": "Nouvelle note ; son auteur devient l'utilisateur d'intégration. `null` vide la note et retire son auteur.",
                    "nullable": true,
                    "maxLength": 5000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Le rendez-vous après modification.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Appointment"
                }
              }
            }
          },
          "400": {
            "description": "BODY_REQUIRED · INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "BODY_REQUIRED",
                "description": "Corps absent."
              },
              {
                "code": "INVALID_FIELD",
                "description": "Champ illisible ou inconnu, ou non modifiable (`workflow`, `category`, `patientId`, `clientId`…), un détail par champ ; `null` sur `startAt`, `durationMinutes`, `vetId` ou `appointmentTypeId` ; `durationMinutes` hors de 5 à 480 ; `note` de plus de 5 000 caractères ; type changé sans `appointmentSubtypeId` alors que le rendez-vous a un motif ; motif qui n'appartient pas au type."
              }
            ]
          },
          "404": {
            "description": "APPOINTMENT_NOT_FOUND · NOT_FOUND · VET_NOT_FOUND · APPOINTMENT_TYPE_NOT_FOUND · APPOINTMENT_SUBTYPE_NOT_FOUND · ROOM_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "APPOINTMENT_NOT_FOUND",
                "description": "Rendez-vous inconnu, d'une autre organisation, ou d'un site non ouvert à l'application."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              },
              {
                "code": "VET_NOT_FOUND",
                "description": "Vétérinaire qui n'est pas dans l'équipe du site du rendez-vous."
              },
              {
                "code": "APPOINTMENT_TYPE_NOT_FOUND",
                "description": "Type de rendez-vous inconnu ou d'un autre site."
              },
              {
                "code": "APPOINTMENT_SUBTYPE_NOT_FOUND",
                "description": "Motif inconnu ou d'un autre site."
              },
              {
                "code": "ROOM_NOT_FOUND",
                "description": "Salle inconnue ou d'un autre site."
              }
            ]
          },
          "409": {
            "description": "SLOT_CONFLICT · APPOINTMENT_NOT_MODIFIABLE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SLOT_CONFLICT",
                "description": "Le nouveau créneau ne respecte pas toutes les contraintes. `meta.reason` : première raison ; `meta.violations` : liste complète (`[{reason, message}]`)."
              },
              {
                "code": "APPOINTMENT_NOT_MODIFIABLE",
                "description": "Rendez-vous annulé, non honoré, terminé ou d'un animal décédé ; ou rendez-vous à facturer (`TO_BILL`) qu'on tente de déplacer. `meta.appointmentId` et `meta.currentStatus` (avancement actuel) sont rendus."
              }
            ]
          }
        },
        "x-treats-webhooks": [
          "appointment.updated"
        ],
        "x-treats-example": {
          "request": {
            "body": {
              "note": "Note modifiée"
            },
            "method": "PATCH",
            "url": "/api/partner/v1/appointments/8b6d02c4-7ac9-42bd-a9f5-5b251d47f82b"
          },
          "response": {
            "body": {
              "appointmentSubtypeId": null,
              "appointmentTypeId": "d2982217-f30f-4d2e-bec2-4bda58da3ca0",
              "arrivedAt": null,
              "calledAt": null,
              "cancellation": null,
              "category": null,
              "clientId": "31ba4810-584b-4ebb-9f4e-b99cc9007c2d",
              "createdAt": "2026-10-15T02:04:05.274243Z",
              "durationMinutes": 30,
              "endAt": "2027-06-21T09:30:00Z",
              "id": "8b6d02c4-7ac9-42bd-a9f5-5b251d47f82b",
              "note": "Note modifiée",
              "noteAuthor": {
                "id": "14deba83-08d1-4fd3-98c4-7908063dee3b",
                "name": "Intégration Agenda"
              },
              "noteUpdatedAt": "2026-10-15T02:04:15.396808Z",
              "patientId": "e7ab5f80-eeeb-4445-8169-774346054354",
              "publicId": "apt_apwmt2f86",
              "roomId": "a1ceecfa-cd26-4bd3-aae0-f26be2c8f722",
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "startAt": "2027-06-21T09:00:00Z",
              "updatedAt": "2026-10-15T02:04:15.397108Z",
              "urgent": false,
              "vetId": "bf5c3a90-ea0b-4483-832b-d9fe907fa99e",
              "workflow": "PLANNED"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/appointments/{id}/cancel": {
      "post": {
        "tags": [
          "Agenda"
        ],
        "summary": "Annuler un rendez-vous",
        "description": "Annule un rendez-vous d'un site ouvert à l'application : il passe en `category: CANCELLED`, libère son créneau et reste visible dans l'agenda et dans les listes, avec son annulation (`cancellation`). Le motif est obligatoire pour l'API, et `OTHER` exige une précision. Le client n'est prévenu que sur demande (`notifyClient: true`). L'annulation est idempotente : annuler un rendez-vous déjà annulé rend `200` et le rendez-vous tel quel, le premier motif est conservé et aucune notification n'est émise. Un rendez-vous ne se supprime jamais par l'API.",
        "operationId": "POST_api_partner_v1_appointments_id_cancel",
        "security": [
          {
            "partner": [
              "agenda:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:write"
          ]
        },
        "x-treats-controller": "PartnerAppointmentController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant du rendez-vous.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "enum": [
                      "CLIENT_REQUEST",
                      "VET_UNAVAILABLE",
                      "PATIENT_RECOVERED",
                      "NO_SHOW",
                      "EMERGENCY",
                      "DUPLICATE",
                      "OTHER"
                    ],
                    "description": "Motif d'annulation (voir `GET /cancellation-reasons`)."
                  },
                  "notes": {
                    "type": "string",
                    "description": "Précision libre. Obligatoire avec `OTHER`.",
                    "maxLength": 5000
                  },
                  "notifyClient": {
                    "type": "boolean",
                    "description": "`true` : prévenir le client de l'annulation.",
                    "default": false
                  }
                },
                "required": [
                  "reason"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Le rendez-vous annulé.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Appointment"
                }
              }
            }
          },
          "400": {
            "description": "BODY_REQUIRED · CANCELLATION_REASON_REQUIRED · CANCELLATION_NOTES_REQUIRED · INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "BODY_REQUIRED",
                "description": "Corps absent : il porte au moins `reason`."
              },
              {
                "code": "CANCELLATION_REASON_REQUIRED",
                "description": "`reason` absent ou vide."
              },
              {
                "code": "CANCELLATION_NOTES_REQUIRED",
                "description": "`reason: OTHER` sans `notes`."
              },
              {
                "code": "INVALID_FIELD",
                "description": "Motif inconnu, `notes` de plus de 5 000 caractères, champ illisible ou inconnu (un détail par champ)."
              }
            ]
          },
          "404": {
            "description": "APPOINTMENT_NOT_FOUND · NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "APPOINTMENT_NOT_FOUND",
                "description": "Rendez-vous inconnu, d'une autre organisation, ou d'un site non ouvert à l'application."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              }
            ]
          }
        },
        "x-treats-webhooks": [
          "appointment.cancelled",
          "appointment.updated"
        ],
        "x-treats-example": {
          "request": {
            "body": {
              "notes": "Annulé par le partenaire",
              "notifyClient": false,
              "reason": "CLIENT_REQUEST"
            },
            "method": "POST",
            "url": "/api/partner/v1/appointments/8b6d02c4-7ac9-42bd-a9f5-5b251d47f82b/cancel"
          },
          "response": {
            "body": {
              "appointmentSubtypeId": null,
              "appointmentTypeId": "d2982217-f30f-4d2e-bec2-4bda58da3ca0",
              "arrivedAt": null,
              "calledAt": null,
              "cancellation": {
                "cancelledAt": "2026-10-15T02:04:20.218245Z",
                "clientNotified": false,
                "notes": "Annulé par le partenaire",
                "reason": "CLIENT_REQUEST"
              },
              "category": "CANCELLED",
              "clientId": "31ba4810-584b-4ebb-9f4e-b99cc9007c2d",
              "createdAt": "2026-10-15T02:04:05.274243Z",
              "durationMinutes": 30,
              "endAt": "2027-06-21T09:30:00Z",
              "id": "8b6d02c4-7ac9-42bd-a9f5-5b251d47f82b",
              "note": null,
              "noteAuthor": null,
              "noteUpdatedAt": null,
              "patientId": "e7ab5f80-eeeb-4445-8169-774346054354",
              "publicId": "apt_apwmt2f86",
              "roomId": null,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "startAt": "2027-06-21T09:00:00Z",
              "updatedAt": "2026-10-15T02:04:20.214912Z",
              "urgent": false,
              "vetId": "bf5c3a90-ea0b-4483-832b-d9fe907fa99e",
              "workflow": "PLANNED"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/appointments/{id}/check-in": {
      "post": {
        "tags": [
          "Agenda"
        ],
        "summary": "Pointer l'arrivée d'un animal",
        "description": "Déclare l'animal arrivé à la clinique : le rendez-vous passe en `workflow: WAITING`, `arrivedAt` prend l'heure de l'appel (ou garde une arrivée déjà enregistrée) et l'animal apparaît dans la salle d'attente du site. Idempotent : un animal déjà en attente n'est pas repointé, le rendez-vous est rendu tel quel, sans notification. Un rendez-vous déjà en consultation (`IN_PROGRESS`) repasse en attente, son heure d'appel effacée. Un rendez-vous annulé, non honoré, terminé, à facturer ou d'un animal décédé est refusé. Aucun corps n'est lu.",
        "operationId": "POST_api_partner_v1_appointments_id_check_in",
        "security": [
          {
            "partner": [
              "agenda:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:write"
          ]
        },
        "x-treats-controller": "PartnerAppointmentController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant du rendez-vous.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "responses": {
          "200": {
            "description": "Le rendez-vous, animal en attente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Appointment"
                }
              }
            }
          },
          "404": {
            "description": "APPOINTMENT_NOT_FOUND · NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "APPOINTMENT_NOT_FOUND",
                "description": "Rendez-vous inconnu, d'une autre organisation, ou d'un site non ouvert à l'application."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              }
            ]
          },
          "409": {
            "description": "APPOINTMENT_NOT_MODIFIABLE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "APPOINTMENT_NOT_MODIFIABLE",
                "description": "Rendez-vous annulé, non honoré, terminé, à facturer (`TO_BILL`) ou d'un animal décédé. `meta.appointmentId` et `meta.currentStatus` sont rendus."
              }
            ]
          }
        },
        "x-treats-webhooks": [
          "patient.checked_in"
        ],
        "x-treats-example": {
          "request": {
            "method": "POST",
            "url": "/api/partner/v1/appointments/27dcb082-4847-4221-89a3-8ec172203eb1/check-in"
          },
          "response": {
            "body": {
              "appointmentSubtypeId": null,
              "appointmentTypeId": "d2982217-f30f-4d2e-bec2-4bda58da3ca0",
              "arrivedAt": "2026-10-15T02:04:19.865132Z",
              "calledAt": null,
              "cancellation": null,
              "category": null,
              "clientId": "79a787b6-2151-4b68-ba30-7969038773f5",
              "createdAt": "2026-10-15T02:04:15.000073Z",
              "durationMinutes": 30,
              "endAt": "2027-06-21T12:30:00Z",
              "id": "27dcb082-4847-4221-89a3-8ec172203eb1",
              "note": null,
              "noteAuthor": null,
              "noteUpdatedAt": null,
              "patientId": "79bc79a5-74b8-472d-a467-e9c1ac2c77c2",
              "publicId": "apt_v8vehz32s",
              "roomId": null,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "startAt": "2027-06-21T12:00:00Z",
              "updatedAt": "2026-10-15T02:04:19.865451Z",
              "urgent": false,
              "vetId": "bf5c3a90-ea0b-4483-832b-d9fe907fa99e",
              "workflow": "WAITING"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/availability/evaluate": {
      "post": {
        "tags": [
          "Agenda"
        ],
        "summary": "Juger un créneau",
        "description": "Juge un créneau avec exactement les contraintes de la prise de rendez-vous par l'API, sans rien écrire : `available: true` si aucune n'est violée, sinon toutes les violations. La fin du créneau est `endAt`, sinon le début plus `durationMinutes`, sinon la durée que prendrait la création pour `appointmentTypeId` et `appointmentSubtypeId` (avec la salle par défaut du motif). Pour juger le déplacement d'un rendez-vous existant, passez-le en `excludeAppointmentId` : il n'entre pas en conflit avec lui-même. Route de lecture : droit `agenda:read`, pas d'en-tête `Idempotency-Key`.",
        "operationId": "POST_api_partner_v1_availability_evaluate",
        "security": [
          {
            "partner": [
              "agenda:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:read"
          ]
        },
        "x-treats-controller": "PartnerAvailabilityController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Site du créneau. Obligatoire (ici ou dans l'en-tête) si l'organisation a ouvert plusieurs sites à l'application.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "startAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Début du créneau."
                  },
                  "endAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Fin du créneau, postérieure à `startAt`. `endAt`, `durationMinutes` ou `appointmentTypeId` obligatoire."
                  },
                  "durationMinutes": {
                    "type": "integer",
                    "description": "Durée en minutes, utilisée sans `endAt`.",
                    "minimum": 5,
                    "maximum": 480
                  },
                  "appointmentTypeId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Type de rendez-vous du site : sans `endAt`, fixe la durée (avec `durationMinutes` s'il est donné)."
                  },
                  "appointmentSubtypeId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Motif du site, qui doit appartenir à `appointmentTypeId` s'il est donné. Pris en compte pour la durée et la salle par défaut seulement avec `appointmentTypeId`."
                  },
                  "vetId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Vétérinaire, membre de l'équipe du site."
                  },
                  "roomId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Salle du site à vérifier."
                  },
                  "excludeAppointmentId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Rendez-vous à ignorer dans le calcul des chevauchements (celui qu'on veut déplacer). Il doit être d'un site ouvert à l'application."
                  }
                },
                "required": [
                  "startAt",
                  "vetId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Le jugement du créneau.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AvailabilityEvaluation"
                }
              }
            }
          },
          "400": {
            "description": "BODY_REQUIRED · INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "BODY_REQUIRED",
                "description": "Corps absent."
              },
              {
                "code": "INVALID_FIELD",
                "description": "`startAt` ou `vetId` absent ; ni `endAt`, ni `durationMinutes`, ni `appointmentTypeId` ; `endAt` antérieur ou égal à `startAt` ; `durationMinutes` hors de 5 à 480 ; motif qui n'appartient pas au type ; champ illisible ou inconnu (un détail par champ)."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND · VET_NOT_FOUND · APPOINTMENT_TYPE_NOT_FOUND · APPOINTMENT_SUBTYPE_NOT_FOUND · ROOM_NOT_FOUND · APPOINTMENT_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              },
              {
                "code": "VET_NOT_FOUND",
                "description": "Vétérinaire qui n'est pas dans l'équipe du site."
              },
              {
                "code": "APPOINTMENT_TYPE_NOT_FOUND",
                "description": "Type de rendez-vous inconnu ou d'un autre site."
              },
              {
                "code": "APPOINTMENT_SUBTYPE_NOT_FOUND",
                "description": "Motif inconnu ou d'un autre site."
              },
              {
                "code": "ROOM_NOT_FOUND",
                "description": "Salle inconnue ou d'un autre site."
              },
              {
                "code": "APPOINTMENT_NOT_FOUND",
                "description": "`excludeAppointmentId` : rendez-vous inconnu ou d'un site non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "body": {
              "durationMinutes": 30,
              "startAt": "2027-06-21T18:00:00Z",
              "vetId": "bf5c3a90-ea0b-4483-832b-d9fe907fa99e"
            },
            "method": "POST",
            "url": "/api/partner/v1/availability/evaluate?site_id=84b9ad0c-5917-4d16-a75c-d3dc97abe595"
          },
          "response": {
            "body": {
              "available": false,
              "durationMinutes": 30,
              "endAt": "2027-06-21T18:30:00Z",
              "startAt": "2027-06-21T18:00:00Z",
              "violations": [
                {
                  "message": "Le créneau est en dehors des heures d'ouverture du site",
                  "reason": "CLINIC_CLOSED"
                },
                {
                  "message": "Le vétérinaire n'est pas planifié sur ce créneau",
                  "reason": "VET_OUT_OF_SHIFT"
                }
              ]
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/availability/slots": {
      "get": {
        "tags": [
          "Agenda"
        ],
        "summary": "Chercher des créneaux libres",
        "description": "Rend les créneaux libres d'un site pour un type ou un motif de rendez-vous : horaires du site, planning des vétérinaires, absences, jours fériés et rendez-vous déjà pris. Sans `vet_id`, tous les vétérinaires actifs de l'équipe du site sont parcourus. Un créneau déjà commencé n'est jamais rendu.\n\nAvec `from` et `to` (31 jours au plus) : tous les créneaux de la fenêtre, triés par début puis par vétérinaire, d'une durée égale à `duration_minutes`, sinon à celle que prendrait la création (prestation du motif, motif, puis type). Sans `to` : le seul prochain créneau libre, tous vétérinaires confondus, cherché sur 30 jours à partir de `from` ou de maintenant ; sa durée est celle du motif, sinon du type, et `duration_minutes` n'est pas utilisé. Les créneaux de la fenêtre partent tous les quarts d'heure et se chevauchent : pour un même vétérinaire, réserver l'un en retire d'autres. La disponibilité des salles n'est pas vérifiée. Liste vide si rien n'est libre. Un créneau rendu n'est pas réservé : la création le rejuge.",
        "operationId": "GET_api_partner_v1_availability_slots",
        "security": [
          {
            "partner": [
              "agenda:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:read"
          ]
        },
        "x-treats-controller": "PartnerAvailabilityController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Site cherché. Obligatoire (ici ou dans l'en-tête) si l'organisation a ouvert plusieurs sites à l'application.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "type_id",
            "in": "query",
            "description": "Type de rendez-vous. `type_id` ou `subtype_id` obligatoire.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "subtype_id",
            "in": "query",
            "description": "Motif ; son type est alors déduit. Avec `type_id`, il doit appartenir à ce type.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "vet_id",
            "in": "query",
            "description": "Limite la recherche à ce vétérinaire, membre de l'équipe du site.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Début de la fenêtre (inclus). Obligatoire avec `to`. Sans `to` : point de départ de la recherche du prochain créneau (maintenant s'il est absent ou passé). Années 1900 à 9999.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Fin de la fenêtre, postérieure à `from`, 31 jours au plus après lui. Années 1900 à 9999.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "duration_minutes",
            "in": "query",
            "description": "Durée des créneaux cherchés, en minutes. Utilisée seulement avec `to`.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les créneaux libres, en une seule page (`nextCursor` toujours `null`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AvailabilitySlot"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "Ni `type_id` ni `subtype_id` ; `to` sans `from`, antérieur ou égal à `from`, ou à plus de 31 jours ; `duration_minutes` non entier ou hors de 5 à 480 ; motif qui n'appartient pas à `type_id` ; paramètre illisible ou date hors des années 1900 à 9999."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND · APPOINTMENT_TYPE_NOT_FOUND · APPOINTMENT_SUBTYPE_NOT_FOUND · VET_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              },
              {
                "code": "APPOINTMENT_TYPE_NOT_FOUND",
                "description": "Type de rendez-vous inconnu ou d'un autre site."
              },
              {
                "code": "APPOINTMENT_SUBTYPE_NOT_FOUND",
                "description": "Motif inconnu ou d'un autre site."
              },
              {
                "code": "VET_NOT_FOUND",
                "description": "Vétérinaire qui n'est pas dans l'équipe du site."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/availability/slots?site_id=84b9ad0c-5917-4d16-a75c-d3dc97abe595&type_id=d2982217-f30f-4d2e-bec2-4bda58da3ca0&vet_id=bf5c3a90-ea0b-4483-832b-d9fe907fa99e&from=2027-06-20T22:00:00Z&to=2027-06-21T22:00:00Z"
          },
          "response": {
            "body": {
              "items": [
                {
                  "durationMinutes": 30,
                  "endAt": "2027-06-21T06:30:00Z",
                  "startAt": "2027-06-21T06:00:00Z",
                  "vetId": "bf5c3a90-ea0b-4483-832b-d9fe907fa99e",
                  "vetName": "Claire Moreau"
                },
                {
                  "durationMinutes": 30,
                  "endAt": "2027-06-21T06:45:00Z",
                  "startAt": "2027-06-21T06:15:00Z",
                  "vetId": "bf5c3a90-ea0b-4483-832b-d9fe907fa99e",
                  "vetName": "Claire Moreau"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/call-notes": {
      "get": {
        "tags": [
          "Notes d'appel et tâches"
        ],
        "summary": "Lister les notes d'appel d'un client ou d'un rendez-vous",
        "description": "Rend les notes d'appel d'un client ou celles reliées à un rendez-vous, sur les sites ouverts à l'application ou le seul site demandé, toutes catégories et tous statuts confondus, triées par date de modification (`updatedAt`) puis par identifiant. L'un des filtres `client_id` ou `appointment_id` est obligatoire : la route ne liste pas toutes les notes d'un site. Pagination par `cursor`, synchronisation par `updated_since` ; une écriture n'apparaît dans cette liste qu'au bout de 60 secondes environ. Un client ou un rendez-vous que l'application ne peut pas lire répond `404`, jamais une liste vide.",
        "operationId": "GET_api_partner_v1_call_notes",
        "security": [
          {
            "partner": [
              "agenda:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:read"
          ]
        },
        "x-treats-controller": "PartnerCallNoteController",
        "parameters": [
          {
            "name": "client_id",
            "in": "query",
            "description": "Notes de ce client. `client_id` ou `appointment_id` obligatoire.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "appointment_id",
            "in": "query",
            "description": "Notes reliées à ce rendez-vous, d'un site ouvert à l'application.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Limite la lecture à ce site. Sans lui ni l'en-tête : tous les sites ouverts à l'application. Le paramètre l'emporte sur l'en-tête.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Notes modifiées à partir de cet instant (inclus). Années 1900 à 9999.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Curseur opaque rendu par la page précédente (`nextCursor`). Absent : première page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Nombre de notes par page. Une valeur au-delà de 200 est ramenée à 200.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page de notes d'appel, par `updatedAt` puis `id` croissants.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CallNote"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "FILTER_REQUIRED · INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "FILTER_REQUIRED",
                "description": "Ni `client_id` ni `appointment_id`."
              },
              {
                "code": "INVALID_FIELD",
                "description": "Paramètre illisible (UUID, instant, `limit` inférieur à 1, `cursor` invalide) ou date hors des années 1900 à 9999. Un détail par paramètre."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND · CLIENT_NOT_FOUND · APPOINTMENT_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              },
              {
                "code": "CLIENT_NOT_FOUND",
                "description": "`client_id` : client inconnu, d'une autre organisation, ou que l'application ne peut pas lire."
              },
              {
                "code": "APPOINTMENT_NOT_FOUND",
                "description": "`appointment_id` : rendez-vous inconnu ou d'un site non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/call-notes?client_id=31ba4810-584b-4ebb-9f4e-b99cc9007c2d&limit=200"
          },
          "response": {
            "body": {
              "items": [
                {
                  "appointmentId": "27dcb082-4847-4221-89a3-8ec172203eb1",
                  "category": "APPOINTMENT_REQUEST",
                  "clientId": "31ba4810-584b-4ebb-9f4e-b99cc9007c2d",
                  "content": "Rappeler pour un vaccin",
                  "createdAt": "2026-10-15T02:04:23.960835Z",
                  "createdBy": {
                    "id": "14deba83-08d1-4fd3-98c4-7908063dee3b",
                    "name": "Intégration Agenda"
                  },
                  "id": "fe84f972-860b-489d-8cd0-9fa0e3b8f50a",
                  "patientId": "e7ab5f80-eeeb-4445-8169-774346054354",
                  "reminderAt": null,
                  "resolvedAt": "2026-10-15T02:04:31.488355Z",
                  "resolvedBy": {
                    "id": "14deba83-08d1-4fd3-98c4-7908063dee3b",
                    "name": "Intégration Agenda"
                  },
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "status": "RESOLVED",
                  "updatedAt": "2026-10-15T02:04:31.488650Z"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      },
      "post": {
        "tags": [
          "Notes d'appel et tâches"
        ],
        "summary": "Consigner un appel",
        "description": "Crée une note d'appel sur le site donné par `site_id` ou `Treats-Site-Id` (implicite si un seul site est ouvert), à traiter par l'équipe : elle apparaît à l'accueil de la clinique en statut `PENDING`, avec l'utilisateur d'intégration pour auteur. Le client doit être lisible par l'application ; l'animal, s'il est donné, doit lui appartenir. La note ne crée pas de rendez-vous : reliez-la ensuite à celui que vous prenez (`POST /call-notes/{id}/link-appointment`).",
        "operationId": "POST_api_partner_v1_call_notes",
        "security": [
          {
            "partner": [
              "agenda:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:write"
          ]
        },
        "x-treats-controller": "PartnerCallNoteController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Site de la note. Obligatoire (ici ou dans l'en-tête) si l'organisation a ouvert plusieurs sites à l'application.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "clientId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Client qui a appelé."
                  },
                  "patientId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Animal concerné, qui doit appartenir au client."
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "APPOINTMENT_REQUEST",
                      "MEDICAL_QUESTION",
                      "COMPLAINT",
                      "RESULTS_INQUIRY",
                      "OTHER"
                    ],
                    "description": "Objet de l'appel."
                  },
                  "content": {
                    "type": "string",
                    "description": "Contenu de la note, non vide.",
                    "maxLength": 5000
                  },
                  "reminderAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Rappel demandé à l'équipe. Années 1900 à 9999."
                  }
                },
                "required": [
                  "clientId",
                  "category",
                  "content"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "La note d'appel créée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallNote"
                }
              }
            }
          },
          "400": {
            "description": "BODY_REQUIRED · INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "BODY_REQUIRED",
                "description": "Corps absent."
              },
              {
                "code": "INVALID_FIELD",
                "description": "Champ absent, illisible ou inconnu (un détail par champ) ; catégorie inconnue ; `content` vide ou de plus de 5 000 caractères ; `patientId` qui n'appartient pas à `clientId`."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND · CLIENT_NOT_FOUND · PATIENT_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              },
              {
                "code": "CLIENT_NOT_FOUND",
                "description": "Client inconnu, d'une autre organisation, ou que l'application ne peut pas lire."
              },
              {
                "code": "PATIENT_NOT_FOUND",
                "description": "Animal inconnu, d'une autre organisation, ou dont le client n'est pas lisible par l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "body": {
              "category": "APPOINTMENT_REQUEST",
              "clientId": "31ba4810-584b-4ebb-9f4e-b99cc9007c2d",
              "content": "Rappeler pour un vaccin",
              "patientId": "e7ab5f80-eeeb-4445-8169-774346054354"
            },
            "method": "POST",
            "url": "/api/partner/v1/call-notes?site_id=84b9ad0c-5917-4d16-a75c-d3dc97abe595"
          },
          "response": {
            "body": {
              "appointmentId": null,
              "category": "APPOINTMENT_REQUEST",
              "clientId": "31ba4810-584b-4ebb-9f4e-b99cc9007c2d",
              "content": "Rappeler pour un vaccin",
              "createdAt": "2026-10-15T02:04:23.960835Z",
              "createdBy": {
                "id": "14deba83-08d1-4fd3-98c4-7908063dee3b",
                "name": "Intégration Agenda"
              },
              "id": "fe84f972-860b-489d-8cd0-9fa0e3b8f50a",
              "patientId": "e7ab5f80-eeeb-4445-8169-774346054354",
              "reminderAt": null,
              "resolvedAt": null,
              "resolvedBy": null,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "status": "PENDING",
              "updatedAt": "2026-10-15T02:04:23.965262Z"
            },
            "status": 201
          }
        }
      }
    },
    "/api/partner/v1/call-notes/{id}/link-appointment": {
      "post": {
        "tags": [
          "Notes d'appel et tâches"
        ],
        "summary": "Relier une note d'appel à un rendez-vous",
        "description": "Relie une note d'appel non traitée à un rendez-vous d'un site ouvert à l'application, typiquement celui pris à la suite de l'appel. Un nouveau lien remplace le précédent. Le statut de la note ne change pas : traitez-la à part (`POST /call-notes/{id}/resolve`). Une note déjà traitée ne se relie plus. Le rendez-vous n'est pas contrôlé comme étant celui du même client.",
        "operationId": "POST_api_partner_v1_call_notes_id_link_appointment",
        "security": [
          {
            "partner": [
              "agenda:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:write"
          ]
        },
        "x-treats-controller": "PartnerCallNoteController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la note d'appel.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "appointmentId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Rendez-vous à relier, d'un site ouvert à l'application."
                  }
                },
                "required": [
                  "appointmentId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "La note d'appel reliée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallNote"
                }
              }
            }
          },
          "400": {
            "description": "BODY_REQUIRED · INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "BODY_REQUIRED",
                "description": "Corps absent."
              },
              {
                "code": "INVALID_FIELD",
                "description": "`appointmentId` absent ou illisible, ou champ inconnu."
              }
            ]
          },
          "404": {
            "description": "CALL_NOTE_NOT_FOUND · NOT_FOUND · APPOINTMENT_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CALL_NOTE_NOT_FOUND",
                "description": "Note inconnue, d'une autre organisation, ou d'un site non ouvert à l'application."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              },
              {
                "code": "APPOINTMENT_NOT_FOUND",
                "description": "Rendez-vous inconnu ou d'un site non ouvert à l'application."
              }
            ]
          },
          "409": {
            "description": "CALL_NOTE_RESOLVED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CALL_NOTE_RESOLVED",
                "description": "La note est déjà traitée : elle ne se modifie plus."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "body": {
              "appointmentId": "27dcb082-4847-4221-89a3-8ec172203eb1"
            },
            "method": "POST",
            "url": "/api/partner/v1/call-notes/fe84f972-860b-489d-8cd0-9fa0e3b8f50a/link-appointment"
          },
          "response": {
            "body": {
              "appointmentId": "27dcb082-4847-4221-89a3-8ec172203eb1",
              "category": "APPOINTMENT_REQUEST",
              "clientId": "31ba4810-584b-4ebb-9f4e-b99cc9007c2d",
              "content": "Rappeler pour un vaccin",
              "createdAt": "2026-10-15T02:04:23.960835Z",
              "createdBy": {
                "id": "14deba83-08d1-4fd3-98c4-7908063dee3b",
                "name": "Intégration Agenda"
              },
              "id": "fe84f972-860b-489d-8cd0-9fa0e3b8f50a",
              "patientId": "e7ab5f80-eeeb-4445-8169-774346054354",
              "reminderAt": null,
              "resolvedAt": null,
              "resolvedBy": null,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "status": "PENDING",
              "updatedAt": "2026-10-15T02:04:27.809580Z"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/call-notes/{id}/resolve": {
      "post": {
        "tags": [
          "Notes d'appel et tâches"
        ],
        "summary": "Traiter une note d'appel",
        "description": "Marque une note d'appel comme traitée (`status: RESOLVED`), au nom de l'utilisateur d'intégration, qui devient `resolvedBy`. Une note traitée ne se modifie plus et ne se rouvre pas par l'API. Traiter une note déjà traitée répond `409`. Aucun corps n'est lu.",
        "operationId": "POST_api_partner_v1_call_notes_id_resolve",
        "security": [
          {
            "partner": [
              "agenda:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:write"
          ]
        },
        "x-treats-controller": "PartnerCallNoteController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la note d'appel.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "responses": {
          "200": {
            "description": "La note d'appel traitée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallNote"
                }
              }
            }
          },
          "404": {
            "description": "CALL_NOTE_NOT_FOUND · NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CALL_NOTE_NOT_FOUND",
                "description": "Note inconnue, d'une autre organisation, ou d'un site non ouvert à l'application."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              }
            ]
          },
          "409": {
            "description": "CALL_NOTE_RESOLVED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CALL_NOTE_RESOLVED",
                "description": "La note est déjà traitée."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "POST",
            "url": "/api/partner/v1/call-notes/fe84f972-860b-489d-8cd0-9fa0e3b8f50a/resolve"
          },
          "response": {
            "body": {
              "appointmentId": "27dcb082-4847-4221-89a3-8ec172203eb1",
              "category": "APPOINTMENT_REQUEST",
              "clientId": "31ba4810-584b-4ebb-9f4e-b99cc9007c2d",
              "content": "Rappeler pour un vaccin",
              "createdAt": "2026-10-15T02:04:23.960835Z",
              "createdBy": {
                "id": "14deba83-08d1-4fd3-98c4-7908063dee3b",
                "name": "Intégration Agenda"
              },
              "id": "fe84f972-860b-489d-8cd0-9fa0e3b8f50a",
              "patientId": "e7ab5f80-eeeb-4445-8169-774346054354",
              "reminderAt": null,
              "resolvedAt": "2026-10-15T02:04:31.488355Z",
              "resolvedBy": {
                "id": "14deba83-08d1-4fd3-98c4-7908063dee3b",
                "name": "Intégration Agenda"
              },
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "status": "RESOLVED",
              "updatedAt": "2026-10-15T02:04:31.488650Z"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/cancellation-reasons": {
      "get": {
        "tags": [
          "Agenda"
        ],
        "summary": "Lister les motifs d'annulation",
        "description": "Rend les sept motifs d'annulation acceptés par `POST /appointments/{id}/cancel`, avec leur libellé en français et l'obligation éventuelle d'une précision. La liste est la même pour toutes les organisations et ne dépend d'aucun site. Une seule page : `nextCursor` vaut toujours `null`.",
        "operationId": "GET_api_partner_v1_cancellation_reasons",
        "security": [
          {
            "partner": [
              "agenda:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:read"
          ]
        },
        "x-treats-controller": "PartnerCancellationReasonController",
        "responses": {
          "200": {
            "description": "Les motifs d'annulation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CancellationReason"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/cancellation-reasons"
          },
          "response": {
            "body": {
              "items": [
                {
                  "code": "CLIENT_REQUEST",
                  "label": "Demande du client",
                  "requiresNotes": false
                },
                {
                  "code": "VET_UNAVAILABLE",
                  "label": "Vétérinaire indisponible",
                  "requiresNotes": false
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/catalog/categories": {
      "get": {
        "tags": [
          "Catalogue"
        ],
        "summary": "Lire l'arbre des catégories",
        "description": "Rend toutes les catégories du catalogue de l'organisation, actives et inactives, sous forme d'arbre : les racines dans `items`, les sous-catégories dans `children`. Tri à chaque niveau : produits puis actes, ordre d'affichage choisi par la clinique, puis nom. L'arbre tient en une seule réponse (`nextCursor` toujours `null`). Les catégories sont communes à l'organisation : aucun site n'est à préciser.",
        "operationId": "GET_api_partner_v1_catalog_categories",
        "security": [
          {
            "partner": [
              "catalog:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "catalog:read"
          ]
        },
        "x-treats-controller": "PartnerCatalogController",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "description": "Restreint à l'arbre des produits ou à celui des actes. Insensible à la casse. Absent : les deux.",
            "schema": {
              "type": "string",
              "enum": [
                "PRODUCT",
                "SERVICE"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les catégories racines et leurs descendantes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CatalogCategory"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`type` autre que `PRODUCT` ou `SERVICE`."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/catalog/categories"
          },
          "response": {
            "body": {
              "items": [
                {
                  "active": true,
                  "children": [],
                  "code": "tarifs-libres",
                  "id": "a2a11f9a-ccaa-49aa-8bb8-1eaccb0f09d1",
                  "itemType": "PRODUCT",
                  "name": "Tarifs libres",
                  "parentId": null
                },
                {
                  "active": true,
                  "children": [
                    {
                      "active": true,
                      "children": [],
                      "code": "LABO_ALLERGO",
                      "id": "c627d91e-3522-49d3-9ca1-e60abb7005b7",
                      "itemType": "PRODUCT",
                      "name": "Allergologie",
                      "parentId": "03c88aa3-e462-4fad-ae83-0c1069ada0f9"
                    },
                    {
                      "active": true,
                      "children": [],
                      "code": "LABO_GENE",
                      "id": "ab55f0f7-6ed5-4260-b449-b5d581fda744",
                      "itemType": "PRODUCT",
                      "name": "Analyses Génétiques",
                      "parentId": "03c88aa3-e462-4fad-ae83-0c1069ada0f9"
                    },
                    {
                      "active": true,
                      "children": [],
                      "code": "LABO_BACT",
                      "id": "97db92ad-952b-4dfc-a3da-39ce1bb96148",
                      "itemType": "PRODUCT",
                      "name": "Bactériologie / Parasitologie",
                      "parentId": "03c88aa3-e462-4fad-ae83-0c1069ada0f9"
                    }
                  ],
                  "code": "LABORATOIRE_PROD",
                  "id": "03c88aa3-e462-4fad-ae83-0c1069ada0f9",
                  "itemType": "PRODUCT",
                  "name": "Analyses & Laboratoire",
                  "parentId": null
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/catalog/items": {
      "get": {
        "tags": [
          "Catalogue"
        ],
        "summary": "Lister les articles du catalogue",
        "description": "Rend une page des articles du catalogue de l'organisation, actifs et inactifs, avec le prix effectif du site visé : le prix propre du site s'il en a un, sinon le prix de l'organisation. Le site se donne par `site_id` ou `Treats-Site-Id` ; il est implicite quand l'organisation n'a ouvert qu'un site à l'application.\n\nTri par date de synchronisation (`updatedAt`) puis identifiant. Un changement de prix au seul site, ou l'entrée en vigueur d'un prix programmé, fait revenir l'article dans `updated_since`. Les modifications des 60 dernières secondes n'apparaissent qu'aux appels suivants ; la lecture par identifiant est immédiate.",
        "operationId": "GET_api_partner_v1_catalog_items",
        "security": [
          {
            "partner": [
              "catalog:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "catalog:read"
          ]
        },
        "x-treats-controller": "PartnerCatalogController",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "description": "Restreint aux produits ou aux actes. Insensible à la casse.",
            "schema": {
              "type": "string",
              "enum": [
                "PRODUCT",
                "SERVICE"
              ]
            }
          },
          {
            "name": "category_id",
            "in": "query",
            "description": "Restreint à une catégorie, sous-catégories comprises.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Texte cherché dans le nom ou le code de l'article (contient, insensible à la casse). Les caractères `%` et `_` sont cherchés tels quels.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "active",
            "in": "query",
            "description": "`true` : actifs seulement ; `false` : inactifs seulement. Absent : les deux, pour qu'une synchronisation voie une désactivation.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Site dont le prix est rendu. Obligatoire si l'organisation a ouvert plusieurs sites à l'application (ou passer l'en-tête `Treats-Site-Id`).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Ne rend que les articles dont `updatedAt` est postérieur ou égal à cet instant (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Curseur `nextCursor` de la page précédente, repassé tel quel. Absent : première page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Nombre d'articles par page, de 1 à 200 (au-delà de 200, ramené à 200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Site visé, à la place du paramètre `site_id`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page d'articles.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CatalogItem"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`type` autre que `PRODUCT` ou `SERVICE`, `active` autre que `true` ou `false`, `category_id` ou `updated_since` mal formé, `limit` inférieur à 1 ou non entier, curseur illisible. Un détail par champ."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un identifiant valide."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé n'est pas ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/catalog/items?limit=37&cursor=MTc5MjAyOTMzNjU5NTQ1NHw5MTVmYjBjYy01NTNhLTRhMTUtODZjYS01ZmI0ZmY3ZDJmYWU"
          },
          "response": {
            "body": {
              "items": [
                {
                  "active": true,
                  "billable": true,
                  "categoryId": "d7cca6c9-a423-4a34-91ff-df57d654c007",
                  "categoryName": "Aliments Physiologiques Chiens",
                  "code": null,
                  "currency": "EUR",
                  "gtin": "00052742055831",
                  "id": "38518a68-078b-4194-947b-4df2150b0f6b",
                  "name": "HILL'S VET ESSENTIALS CANINE MULTI-BENEFIT + WEIGHT ADULT MEDIUM 2KG",
                  "prescriptionOnly": true,
                  "priceHtCents": 2442,
                  "priceScope": "ORGANIZATION",
                  "priceTtcCents": 2930,
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "type": "PRODUCT",
                  "updatedAt": "2026-10-15T01:55:36.595841Z",
                  "vatRatePercent": 20
                },
                {
                  "active": true,
                  "billable": true,
                  "categoryId": "d7cca6c9-a423-4a34-91ff-df57d654c007",
                  "categoryName": "Aliments Physiologiques Chiens",
                  "code": null,
                  "currency": "EUR",
                  "gtin": "00052742055770",
                  "id": "9b98845e-f56c-4471-9122-bb015490608a",
                  "name": "HILL'S VET ESSENTIALS CANINE MULTI-BENEFIT + WEIGHT ADULT / SMALL & MINI 2KG",
                  "prescriptionOnly": true,
                  "priceHtCents": 2442,
                  "priceScope": "ORGANIZATION",
                  "priceTtcCents": 2930,
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "type": "PRODUCT",
                  "updatedAt": "2026-10-15T01:55:36.596305Z",
                  "vatRatePercent": 20
                }
              ],
              "nextCursor": "MTc5MjAyOTMzNjYxNTg2MnxjMDNiNzkyNy1jMDY4LTRjZDctODFhOS1hY2QyNjAzMjEyMDc"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/catalog/items/{id}": {
      "get": {
        "tags": [
          "Catalogue"
        ],
        "summary": "Lire un article du catalogue",
        "description": "Rend un article du catalogue de l'organisation avec le prix effectif du site visé, le même que celui de la fiche dans l'application. Le site se donne par `site_id` ou `Treats-Site-Id` ; il est implicite quand l'organisation n'a ouvert qu'un site. Un article inactif reste lisible. Un article d'une autre organisation, ou du référentiel d'une centrale, répond 404.",
        "operationId": "GET_api_partner_v1_catalog_items_id",
        "security": [
          {
            "partner": [
              "catalog:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "catalog:read"
          ]
        },
        "x-treats-controller": "PartnerCatalogController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de l'article.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Site dont le prix est rendu. Obligatoire si l'organisation a ouvert plusieurs sites à l'application (ou passer l'en-tête `Treats-Site-Id`).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Site visé, à la place du paramètre `site_id`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "L'article.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogItem"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un identifiant valide."
              }
            ]
          },
          "404": {
            "description": "CATALOG_ITEM_NOT_FOUND · SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CATALOG_ITEM_NOT_FOUND",
                "description": "Aucun article de ce catalogue ne porte cet identifiant."
              },
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé n'est pas ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/catalog/items/daad43c2-16a9-4b55-8fb4-5d7f81c243c7"
          },
          "response": {
            "body": {
              "active": true,
              "billable": true,
              "categoryId": "a2a11f9a-ccaa-49aa-8bb8-1eaccb0f09d1",
              "categoryName": "Tarifs libres",
              "code": "e2e--produit-8f0f05",
              "currency": "EUR",
              "gtin": null,
              "id": "daad43c2-16a9-4b55-8fb4-5d7f81c243c7",
              "name": "Produit",
              "prescriptionOnly": false,
              "priceHtCents": 1000,
              "priceScope": "ORGANIZATION",
              "priceTtcCents": 1200,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "type": "PRODUCT",
              "updatedAt": "2026-10-15T02:06:34.758557Z",
              "vatRatePercent": 20
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/catalog/search": {
      "get": {
        "tags": [
          "Catalogue"
        ],
        "summary": "Chercher un article du catalogue",
        "description": "Recherche plein texte dans le catalogue de l'organisation, la même que celle de la saisie d'un acte dans l'application, classée par pertinence. Chaque résultat est rendu comme par `GET /catalog/items`, avec le prix du site visé ; seuls les articles du catalogue de l'organisation sont rendus, ce qui peut donner moins de résultats que `limit`. Pas de pagination : `nextCursor` est toujours `null`.\n\nSi le moteur de recherche est indisponible, la route répond `503 SEARCH_UNAVAILABLE` avec `Retry-After: 30`, jamais une liste vide. En attendant, `GET /catalog/items?q=` cherche dans le nom et le code.",
        "operationId": "GET_api_partner_v1_catalog_search",
        "security": [
          {
            "partner": [
              "catalog:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "catalog:read"
          ]
        },
        "x-treats-controller": "PartnerCatalogController",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Texte cherché.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "species",
            "in": "query",
            "description": "Code d'espèce de l'animal (`DOG`, `CAT`…, voir les espèces de l'organisation). Garde les articles destinés à cette espèce et ceux qui ne visent aucune espèce. Un code mal formé est ignoré.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "module",
            "in": "query",
            "description": "Module de consultation (`vaccination`…) : restreint aux catégories que la clinique a rattachées à ce module. Un module inconnu ou sans catégorie ne filtre rien.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Site dont le prix est rendu. Obligatoire si l'organisation a ouvert plusieurs sites à l'application (ou passer l'en-tête `Treats-Site-Id`).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Nombre maximal de résultats, de 1 à 100 (au-delà, ramené à 100).",
            "schema": {
              "type": "integer",
              "default": 20
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Site visé, à la place du paramètre `site_id`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les meilleurs résultats, du plus pertinent au moins pertinent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CatalogItem"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`q` absent ou vide, `limit` inférieur à 1 ou non entier."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un identifiant valide."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé n'est pas ouvert à l'application."
              }
            ]
          },
          "503": {
            "description": "SEARCH_UNAVAILABLE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SEARCH_UNAVAILABLE",
                "description": "Le moteur de recherche est momentanément indisponible. Réessayer après le délai de l'en-tête `Retry-After` (30 secondes), ou utiliser `GET /catalog/items?q=`."
              }
            ]
          }
        }
      }
    },
    "/api/partner/v1/clients": {
      "get": {
        "tags": [
          "Clients"
        ],
        "summary": "Lister les clients",
        "description": "Rend les clients de l'organisation que vous pouvez voir (tous si l'organisation vous a ouvert tous ses sites, sinon ceux nés sur les sites ouverts), clients désactivés compris (`active: false`). Tri par `updatedAt` puis `id`, croissant : c'est la liste de synchronisation, à parcourir par `cursor` et à reprendre par `updated_since`. Une fiche créée ou modifiée n'y apparaît qu'environ 60 secondes après l'écriture ; sa lecture par `GET /clients/{id}` est immédiate.\n\nLes filtres `q`, `phone` et `email` sont des recherches : ils se combinent entre eux et rendent aussi une fiche tout juste créée. N'exige que `clients:read`.",
        "operationId": "GET_api_partner_v1_clients",
        "security": [
          {
            "partner": [
              "clients:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "clients:read"
          ]
        },
        "x-treats-controller": "PartnerClientsController",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Texte cherché dans le prénom, le nom ou la raison sociale (contient, casse et accents ignorés), ou dans le téléphone principal tel qu'il a été saisi. 100 caractères au plus. Le téléphone mobile n'est pas cherché ici : utilisez `phone`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "phone",
            "in": "query",
            "description": "Numéro complet, comparé au téléphone principal OU au mobile, quel que soit le format (`06 12 34 56 78`, `0612345678`, `+33 6 12 34 56 78` sont équivalents). 6 chiffres au moins. Pas de recherche partielle.",
            "schema": {
              "type": "string",
              "example": "+33612345678"
            }
          },
          {
            "name": "email",
            "in": "query",
            "description": "Adresse email exacte, casse ignorée. 255 caractères au plus.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Ne rend que les fiches dont `updatedAt` est postérieur ou égal à cet instant (ISO 8601, années 1900 à 9999). Pour une synchronisation incrémentale, repartez du plus grand `updatedAt` vu moins 5 minutes et dédoublonnez par `id`.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Curseur opaque rendu par la page précédente (`nextCursor`), à repasser tel quel avec les mêmes filtres. Absent : première page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Nombre de fiches par page, de 1 à 200. Au-delà de 200, ramené à 200.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page de clients.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Client"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`q` de plus de 100 caractères, `phone` de moins de 6 chiffres, `email` de plus de 255 caractères, `updated_since` mal formé ou hors des années 1900 à 9999, `limit` non entier ou inférieur à 1, `cursor` illisible. `details` nomme chaque paramètre refusé."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/clients?limit=13&cursor=MTc5MjAyOTMzODU2NjY0MHw5YmZkMTgxNS1hNWNmLTQ2MTktYTgyMS0yYjAyMGM5ZWI5ODc"
          },
          "response": {
            "body": {
              "items": [
                {
                  "active": true,
                  "address": {
                    "city": "Marseille",
                    "country": "France",
                    "line": "145 Place du Général de Gaulle, 13008 Marseille",
                    "postalCode": "13008",
                    "street": "145 Place du Général de Gaulle"
                  },
                  "birthDate": null,
                  "civility": null,
                  "companyName": null,
                  "consents": {
                    "email": true,
                    "mail": false,
                    "sms": true
                  },
                  "createdAt": "2026-10-15T01:55:38.566645Z",
                  "email": "laura.fournier26@exemple.fr",
                  "firstName": "Laura",
                  "id": "41f0e71c-f9b2-45bf-89c1-071bb0d14cab",
                  "lastName": "Fournier",
                  "mobilePhone": null,
                  "organizationId": "9b9ff4bd-d49e-4b44-a41a-82136caaa3f8",
                  "phone": "0612345678",
                  "preferredChannel": null,
                  "publicId": null,
                  "siren": null,
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "type": "INDIVIDUAL",
                  "updatedAt": "2026-10-15T01:55:38.566646Z"
                },
                {
                  "active": true,
                  "address": {
                    "city": "Lyon",
                    "country": "France",
                    "line": "78 Allée Nationale, 69006 Lyon",
                    "postalCode": "69006",
                    "street": "78 Allée Nationale"
                  },
                  "birthDate": null,
                  "civility": null,
                  "companyName": null,
                  "consents": {
                    "email": true,
                    "mail": false,
                    "sms": true
                  },
                  "createdAt": "2026-10-15T01:55:38.566651Z",
                  "email": "julien.martin27@exemple.fr",
                  "firstName": "Julien",
                  "id": "a15c47c2-6620-446b-8212-b9de9a38da85",
                  "lastName": "Martin",
                  "mobilePhone": null,
                  "organizationId": "9b9ff4bd-d49e-4b44-a41a-82136caaa3f8",
                  "phone": "0612345678",
                  "preferredChannel": null,
                  "publicId": null,
                  "siren": null,
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "type": "INDIVIDUAL",
                  "updatedAt": "2026-10-15T01:55:38.566652Z"
                }
              ],
              "nextCursor": "MTc5MjAyOTMzODU2NjcyN3w5ODdhOTRjNy0yNzNhLTQ2ZmQtOWFmOS05YTBlNmFkMTFjY2Y"
            },
            "status": 200
          }
        }
      },
      "post": {
        "tags": [
          "Clients"
        ],
        "summary": "Créer un client",
        "description": "Crée un client dans l'organisation, avec pour site d'origine le site désigné par `site_id` (ou `Treats-Site-Id`), implicite si l'organisation ne vous a ouvert qu'un site. Le client naît sans aucun accord de contact : c'est la clinique ou le propriétaire qui les donne. Exige `clients:write`.\n\nLe téléphone principal est unique dans toute l'organisation, sites non ouverts compris : vérifiez d'abord par `POST /clients/duplicate-check` ou `GET /clients?phone=`. Tous les champs refusés sont rendus en une seule réponse `400 INVALID_FIELD`, et tout champ hors contrat est refusé, y compris dans `address` et `consents`. Les textes ne doivent contenir aucune balise HTML.",
        "operationId": "POST_api_partner_v1_clients",
        "security": [
          {
            "partner": [
              "clients:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "clients:write"
          ]
        },
        "x-treats-controller": "PartnerClientsController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Site d'origine du client. Obligatoire si l'organisation vous a ouvert plusieurs sites (sinon le seul site ouvert). Peut aussi passer en en-tête `Treats-Site-Id`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id` (le paramètre l'emporte s'il est donné).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "firstName": {
                    "type": "string",
                    "description": "Prénom.",
                    "maxLength": 100
                  },
                  "lastName": {
                    "type": "string",
                    "description": "Nom.",
                    "maxLength": 100
                  },
                  "phone": {
                    "type": "string",
                    "description": "Téléphone principal : chiffres, espaces, `+`, `(`, `)`, `.`, `-`, avec 6 chiffres au moins. `phone` ou `email` est obligatoire. Déjà porté par un client de l'organisation : `409 CLIENT_DUPLICATE`.",
                    "maxLength": 20
                  },
                  "email": {
                    "type": "string",
                    "description": "Adresse email (format vérifié). `phone` ou `email` est obligatoire.",
                    "maxLength": 255
                  },
                  "mobilePhone": {
                    "type": "string",
                    "description": "Téléphone mobile, même format que `phone`. Ne compte pas comme moyen de contact obligatoire et n'est pas soumis au contrôle de doublon.",
                    "maxLength": 20
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "INDIVIDUAL",
                      "PROFESSIONAL"
                    ],
                    "description": "Type de client (casse ignorée).",
                    "default": "INDIVIDUAL"
                  },
                  "civility": {
                    "type": "string",
                    "enum": [
                      "MR",
                      "MRS",
                      "MS",
                      "DR"
                    ],
                    "description": "Civilité (casse ignorée)."
                  },
                  "companyName": {
                    "type": "string",
                    "description": "Raison sociale.",
                    "maxLength": 200
                  },
                  "address": {
                    "type": "object",
                    "properties": {
                      "street": {
                        "type": "string",
                        "description": "Voie (numéro et rue).",
                        "maxLength": 200
                      },
                      "postalCode": {
                        "type": "string",
                        "description": "Code postal.",
                        "maxLength": 20
                      },
                      "city": {
                        "type": "string",
                        "description": "Ville.",
                        "maxLength": 100
                      },
                      "country": {
                        "type": "string",
                        "description": "Pays.",
                        "maxLength": 100
                      }
                    },
                    "description": "Adresse postale. Aucun champ n'est obligatoire ; la ligne composée (`address.line`) est calculée par Treats."
                  },
                  "birthDate": {
                    "type": "string",
                    "format": "date",
                    "description": "Date de naissance du client (`AAAA-MM-JJ`), passée ou du jour, pas avant 1900."
                  },
                  "consents": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "boolean",
                        "description": "Seul `false` est accepté."
                      },
                      "sms": {
                        "type": "boolean",
                        "description": "Seul `false` est accepté."
                      },
                      "mail": {
                        "type": "boolean",
                        "description": "Seul `false` est accepté."
                      }
                    },
                    "description": "Accords de contact. Seul `false` est accepté (`true` ou `null` sont refusés) ; à la création il ne change rien, le client naissant sans aucun accord."
                  },
                  "preferredChannel": {
                    "type": "string",
                    "enum": [
                      "EMAIL",
                      "SMS",
                      "PHONE"
                    ],
                    "description": "Canal préféré pour les envois manuels (casse ignorée)."
                  }
                },
                "required": [
                  "firstName",
                  "lastName"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Le client créé.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Client"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · SITE_ID_REQUIRED · INVALID_SITE_ID · INVALID_REQUEST",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "Champ obligatoire absent (`firstName`, `lastName`, ou ni `phone` ni `email`), texte trop long ou contenant une balise HTML, email ou téléphone mal formé, date future ou avant 1900, valeur d'enum inconnue, `consents.*` à `true` ou `null`, `address` ou `consents` qui n'est pas un objet, champ inconnu (`address.ville`). Un détail par champ, chemin compris."
              },
              {
                "code": "SITE_ID_REQUIRED",
                "description": "L'organisation vous a ouvert plusieurs sites et aucun `site_id` n'est donné."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              },
              {
                "code": "INVALID_REQUEST",
                "description": "Refus résiduel de Treats sur une donnée que les contrôles ci-dessus n'ont pas arrêtée (filet de sécurité)."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé n'est pas un site que l'organisation vous a ouvert."
              }
            ]
          },
          "409": {
            "description": "CLIENT_DUPLICATE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CLIENT_DUPLICATE",
                "description": "Un client de l'organisation porte déjà ce téléphone principal, y compris sur un site qui ne vous est pas ouvert. La réponse ne donne ni l'identifiant ni le site de cette fiche."
              }
            ]
          }
        },
        "x-treats-webhooks": [
          "client.created"
        ],
        "x-treats-example": {
          "request": {
            "body": {
              "address": {
                "city": "Paris",
                "postalCode": "75011",
                "street": "12 rue des Lilas"
              },
              "birthDate": "1985-04-12",
              "civility": "MRS",
              "consents": {
                "sms": false
              },
              "email": "alice@exemple.fr",
              "firstName": "Alice",
              "lastName": "Partenaire",
              "phone": "0612345678",
              "preferredChannel": "EMAIL",
              "type": "INDIVIDUAL"
            },
            "method": "POST",
            "url": "/api/partner/v1/clients"
          },
          "response": {
            "body": {
              "active": true,
              "address": {
                "city": "Paris",
                "country": "France",
                "line": "12 rue des Lilas, 75011 Paris",
                "postalCode": "75011",
                "street": "12 rue des Lilas"
              },
              "birthDate": "1985-04-12",
              "civility": "MRS",
              "companyName": null,
              "consents": {
                "email": false,
                "mail": false,
                "sms": false
              },
              "createdAt": "2026-10-15T02:02:57.438435Z",
              "email": "alice@exemple.fr",
              "firstName": "Alice",
              "id": "b8381184-1f53-47d5-9238-10cad7d5f7d9",
              "lastName": "Partenaire",
              "mobilePhone": null,
              "organizationId": "9b9ff4bd-d49e-4b44-a41a-82136caaa3f8",
              "phone": "0612345678",
              "preferredChannel": "EMAIL",
              "publicId": "cli_7rrkwykx6",
              "siren": null,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "type": "INDIVIDUAL",
              "updatedAt": "2026-10-15T02:02:57.438435Z"
            },
            "status": 201
          }
        }
      }
    },
    "/api/partner/v1/clients/duplicate-check": {
      "post": {
        "tags": [
          "Clients"
        ],
        "summary": "Vérifier un doublon avant création",
        "description": "Dit si `POST /clients` accepterait ce téléphone, et rend les fiches proches que vous pouvez voir (même téléphone principal, ou même nom et prénom). La recherche porte sur toute l'organisation ; une fiche d'un site qui ne vous est pas ouvert fait passer `phoneAvailable` à `false` mais n'apparaît jamais dans `candidates`. Ne crée et ne modifie rien. Exige `clients:read`. Route de lecture malgré la méthode POST : pas d'en-tête `Idempotency-Key`.",
        "operationId": "POST_api_partner_v1_clients_duplicate_check",
        "security": [
          {
            "partner": [
              "clients:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "clients:read"
          ]
        },
        "x-treats-controller": "PartnerClientsController",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "phone": {
                    "type": "string",
                    "description": "Téléphone à vérifier, même format que pour `POST /clients`. Obligatoire si `firstName` et `lastName` ne sont pas donnés tous les deux.",
                    "maxLength": 20
                  },
                  "firstName": {
                    "type": "string",
                    "description": "Prénom. Pris en compte seulement avec `lastName`.",
                    "maxLength": 100
                  },
                  "lastName": {
                    "type": "string",
                    "description": "Nom. Pris en compte seulement avec `firstName`.",
                    "maxLength": 100
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Disponibilité du téléphone et fiches proches visibles.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientDuplicateCheck"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "Ni `phone`, ni le couple `firstName` et `lastName` (le refus porte sur `phone`) ; téléphone mal formé, texte trop long ou contenant une balise HTML ; champ hors de `phone`, `firstName`, `lastName`."
              }
            ]
          },
          "404": {
            "description": "CLIENT_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CLIENT_NOT_FOUND",
                "description": "L'organisation ne vous ouvre plus aucun site."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "body": {
              "phone": "0612345678"
            },
            "method": "POST",
            "url": "/api/partner/v1/clients/duplicate-check"
          },
          "response": {
            "body": {
              "candidates": [
                {
                  "clientId": "b8381184-1f53-47d5-9238-10cad7d5f7d9",
                  "firstName": "Alice",
                  "lastName": "Partenaire",
                  "match": "PHONE",
                  "publicId": "cli_7rrkwykx6",
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595"
                }
              ],
              "phoneAvailable": false
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/clients/{id}": {
      "get": {
        "tags": [
          "Clients"
        ],
        "summary": "Lire un client",
        "description": "Rend la fiche d'un client visible (organisation entière si tous les sites vous sont ouverts, sinon client né sur un site ouvert). Lecture immédiate, sans le délai de la liste. Un client d'une autre organisation, d'un site non ouvert ou inconnu répond `404 CLIENT_NOT_FOUND`. Exige `clients:read`.",
        "operationId": "GET_api_partner_v1_clients_id",
        "security": [
          {
            "partner": [
              "clients:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "clients:read"
          ]
        },
        "x-treats-controller": "PartnerClientsController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant du client (`id`, pas `publicId`).",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La fiche du client.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Client"
                }
              }
            }
          },
          "404": {
            "description": "CLIENT_NOT_FOUND · NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CLIENT_NOT_FOUND",
                "description": "Client inconnu, d'une autre organisation, ou né sur un site qui ne vous est pas ouvert."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/clients/b8381184-1f53-47d5-9238-10cad7d5f7d9"
          },
          "response": {
            "body": {
              "active": true,
              "address": {
                "city": "Paris",
                "country": "France",
                "line": "12 rue des Lilas, 75011 Paris",
                "postalCode": "75011",
                "street": "12 rue des Lilas"
              },
              "birthDate": "1985-04-12",
              "civility": "MRS",
              "companyName": null,
              "consents": {
                "email": false,
                "mail": false,
                "sms": false
              },
              "createdAt": "2026-10-15T02:02:57.438435Z",
              "email": "alice@exemple.fr",
              "firstName": "Alice",
              "id": "b8381184-1f53-47d5-9238-10cad7d5f7d9",
              "lastName": "Partenaire",
              "mobilePhone": null,
              "organizationId": "9b9ff4bd-d49e-4b44-a41a-82136caaa3f8",
              "phone": "0612345678",
              "preferredChannel": "EMAIL",
              "publicId": "cli_7rrkwykx6",
              "siren": null,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "type": "INDIVIDUAL",
              "updatedAt": "2026-10-15T02:02:57.438435Z"
            },
            "status": 200
          }
        }
      },
      "patch": {
        "tags": [
          "Clients"
        ],
        "summary": "Modifier un client",
        "description": "Modifie les champs envoyés : champ absent = inchangé, champ à `null` = vidé. Se vident : `email`, `mobilePhone`, `companyName`, chaque champ d'`address`, et `address: null` (toute l'adresse). Ne se vident pas (`400 INVALID_FIELD`, « ne peut pas être vidé ») : `firstName`, `lastName`, `type`, `civility`, `phone` (le remplacer), `birthDate`, `preferredChannel`, `consents.*`. Un corps vide `{}` ne change rien et rend la fiche.\n\nLe client doit garder un téléphone principal ou un email : vider l'email d'un client sans téléphone est refusé. Un accord de contact peut être retiré (`false`, à la demande du client), jamais donné (`true` refusé). Un nouveau téléphone déjà porté par un autre client de l'organisation répond `409 CLIENT_DUPLICATE`. Exige `clients:write`.",
        "operationId": "PATCH_api_partner_v1_clients_id",
        "security": [
          {
            "partner": [
              "clients:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "clients:write"
          ]
        },
        "x-treats-controller": "PartnerClientsController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant du client.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Site au nom duquel l'écriture est faite (journal). Facultatif : à défaut, le site d'origine du client s'il vous est ouvert, sinon le seul site ouvert. La fiche reste celle de l'organisation.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "firstName": {
                    "type": "string",
                    "description": "Prénom. Non vidable, ni à `null` ni à vide.",
                    "maxLength": 100
                  },
                  "lastName": {
                    "type": "string",
                    "description": "Nom. Non vidable, ni à `null` ni à vide.",
                    "maxLength": 100
                  },
                  "phone": {
                    "type": "string",
                    "description": "Téléphone principal (même format qu'à la création). Non vidable : le remplacer. Déjà porté par un autre client : `409 CLIENT_DUPLICATE`.",
                    "maxLength": 20
                  },
                  "email": {
                    "type": "string",
                    "description": "Adresse email. `null` la vide, si le client garde un téléphone principal.",
                    "nullable": true,
                    "maxLength": 255
                  },
                  "mobilePhone": {
                    "type": "string",
                    "description": "Téléphone mobile. `null` le vide.",
                    "nullable": true,
                    "maxLength": 20
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "INDIVIDUAL",
                      "PROFESSIONAL"
                    ],
                    "description": "Type de client. Non vidable."
                  },
                  "civility": {
                    "type": "string",
                    "enum": [
                      "MR",
                      "MRS",
                      "MS",
                      "DR"
                    ],
                    "description": "Civilité. Non vidable."
                  },
                  "companyName": {
                    "type": "string",
                    "description": "Raison sociale. `null` la vide.",
                    "nullable": true,
                    "maxLength": 200
                  },
                  "address": {
                    "type": "object",
                    "properties": {
                      "street": {
                        "type": "string",
                        "description": "Voie. `null` la vide.",
                        "nullable": true,
                        "maxLength": 200
                      },
                      "postalCode": {
                        "type": "string",
                        "description": "Code postal. `null` le vide.",
                        "nullable": true,
                        "maxLength": 20
                      },
                      "city": {
                        "type": "string",
                        "description": "Ville. `null` la vide.",
                        "nullable": true,
                        "maxLength": 100
                      },
                      "country": {
                        "type": "string",
                        "description": "Pays. `null` le vide.",
                        "nullable": true,
                        "maxLength": 100
                      }
                    },
                    "description": "Adresse : seuls les champs envoyés changent, un champ à `null` est vidé ; `address: null` vide toute l'adresse. La ligne composée suit.",
                    "nullable": true
                  },
                  "birthDate": {
                    "type": "string",
                    "format": "date",
                    "description": "Date de naissance, passée ou du jour, pas avant 1900. Non vidable."
                  },
                  "consents": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "boolean",
                        "description": "`false` : retire l'accord email."
                      },
                      "sms": {
                        "type": "boolean",
                        "description": "`false` : retire l'accord SMS."
                      },
                      "mail": {
                        "type": "boolean",
                        "description": "`false` : retire l'accord courrier."
                      }
                    },
                    "description": "Retrait d'accords de contact : seul `false` est accepté, `true` et `null` sont refusés."
                  },
                  "preferredChannel": {
                    "type": "string",
                    "enum": [
                      "EMAIL",
                      "SMS",
                      "PHONE"
                    ],
                    "description": "Canal préféré. Non vidable."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "La fiche modifiée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Client"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · SITE_ID_REQUIRED · INVALID_SITE_ID · INVALID_REQUEST",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "Champ non vidable envoyé à `null` (ou prénom, nom, téléphone vide), email vidé sur un client sans téléphone principal, `consents.*` à `true`, format ou longueur refusés, valeur d'enum inconnue, champ hors contrat. Un détail par champ."
              },
              {
                "code": "SITE_ID_REQUIRED",
                "description": "Aucun `site_id`, le site d'origine du client ne vous est pas ouvert, et l'organisation vous a ouvert plusieurs sites."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              },
              {
                "code": "INVALID_REQUEST",
                "description": "Refus résiduel de Treats sur une donnée que les contrôles ci-dessus n'ont pas arrêtée (filet de sécurité)."
              }
            ]
          },
          "404": {
            "description": "CLIENT_NOT_FOUND · NOT_FOUND · SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CLIENT_NOT_FOUND",
                "description": "Client inconnu, d'une autre organisation, ou né sur un site qui ne vous est pas ouvert."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              },
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé n'est pas un site que l'organisation vous a ouvert."
              }
            ]
          },
          "409": {
            "description": "CLIENT_DUPLICATE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CLIENT_DUPLICATE",
                "description": "Le nouveau téléphone principal est déjà porté par un autre client de l'organisation, y compris sur un site qui ne vous est pas ouvert. Ni l'identifiant ni le site de cette fiche ne sont rendus."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "body": {
              "mobilePhone": "07 12 34 56 78"
            },
            "method": "PATCH",
            "url": "/api/partner/v1/clients/b8381184-1f53-47d5-9238-10cad7d5f7d9"
          },
          "response": {
            "body": {
              "active": true,
              "address": {
                "city": "Paris",
                "country": "France",
                "line": "12 rue des Lilas, 75011 Paris",
                "postalCode": "75011",
                "street": "12 rue des Lilas"
              },
              "birthDate": "1985-04-12",
              "civility": "MRS",
              "companyName": null,
              "consents": {
                "email": false,
                "mail": false,
                "sms": false
              },
              "createdAt": "2026-10-15T02:02:57.438435Z",
              "email": "alice@exemple.fr",
              "firstName": "Alice",
              "id": "b8381184-1f53-47d5-9238-10cad7d5f7d9",
              "lastName": "Partenaire",
              "mobilePhone": "07 12 34 56 78",
              "organizationId": "9b9ff4bd-d49e-4b44-a41a-82136caaa3f8",
              "phone": "0612345678",
              "preferredChannel": "EMAIL",
              "publicId": "cli_7rrkwykx6",
              "siren": null,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "type": "INDIVIDUAL",
              "updatedAt": "2026-10-15T02:03:07.477017Z"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/consultations": {
      "get": {
        "tags": [
          "Consultations"
        ],
        "summary": "Lister les consultations",
        "description": "Rend les consultations des sites ouverts à l'application (ou du seul site demandé), brouillons et validées, triées par `updatedAt` croissant puis `id` : c'est la liste de synchronisation. Une consultation remonte quand son en-tête change ou quand un de ses modules est écrit. Une écriture apparaît dans cette liste au plus 60 secondes après ; la lecture par identifiant, elle, est immédiate. Reprendre une synchronisation à `updated_since = dernier updatedAt vu − 5 minutes` et dédoublonner par `id`.\n\nLes éléments n'ont ni `category` ni `ownerReport` : lire `GET /consultations/{id}` pour les avoir. Un consentement limité à certains sites ne voit, avec `patient_id`, que les animaux dont le client est rattaché à l'un de ses sites.",
        "operationId": "GET_api_partner_v1_consultations",
        "security": [
          {
            "partner": [
              "consultations:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "consultations:read"
          ]
        },
        "x-treats-controller": "PartnerConsultationsController",
        "parameters": [
          {
            "name": "patient_id",
            "in": "query",
            "description": "Ne garde que les consultations de cet animal. Animal inconnu ou non visible de l'application : `404 PATIENT_NOT_FOUND`, jamais une liste vide.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Ne garde que ce site. Le paramètre l'emporte sur l'en-tête.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Ne garde que ce statut. Lu sans distinction de casse (`draft` vaut `DRAFT`).",
            "schema": {
              "type": "string",
              "enum": [
                "DRAFT",
                "VALIDATED"
              ]
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Ne rend que les consultations dont `updatedAt` est postérieur ou égal à cet instant (ISO 8601, années 1900 à 9999).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Taille de la page, de 1 à 200. Au-delà de 200, la page fait 200.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Curseur rendu par la page précédente (`nextCursor`), à repasser tel quel avec les mêmes filtres.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page de consultations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ConsultationListItem"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`status` hors de `DRAFT`/`VALIDATED`, `patient_id` qui n'est pas un UUID, `updated_since` illisible ou hors des années 1900 à 9999, `limit` inférieur à 1, `cursor` invalide."
              }
            ]
          },
          "404": {
            "description": "PATIENT_NOT_FOUND · SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PATIENT_NOT_FOUND",
                "description": "`patient_id` inconnu, d'une autre organisation, ou dont le client n'est pas rattaché à un site ouvert à l'application."
              },
              {
                "code": "SITE_NOT_FOUND",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un site ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/consultations?limit=97&cursor=MTc5MjAyOTMzNTE2Mjc1NHw1MTc3MWFjNy03OWQ2LTQ0NGYtYjk2Ni0zNjNiZGM2NzJmNDA"
          },
          "response": {
            "body": {
              "items": [
                {
                  "appointmentId": "087514e4-bc63-445a-bb5f-4930bbe0147d",
                  "createdAt": "2026-10-15T01:55:42.356237Z",
                  "id": "517ac7ce-c0e8-4a2d-aba1-27ea460fa55d",
                  "motifId": null,
                  "patientId": "f4cb6f3f-0437-4080-a81c-af4963c5e93d",
                  "publicId": null,
                  "reason": "Examen annuel de santé",
                  "siteId": "6b1a3785-35f9-4e82-afaf-032c8a456c36",
                  "startedAt": "2024-12-27T10:45:00Z",
                  "status": "VALIDATED",
                  "templateSlug": null,
                  "updatedAt": "2026-10-15T01:55:35.162754Z",
                  "validatedAt": "2024-12-27T11:27:00Z",
                  "vetId": "c6a79700-4494-428e-b027-6cf0f833a580"
                },
                {
                  "appointmentId": "3bace588-0cbc-4e3a-9d90-5feda93e308b",
                  "createdAt": "2026-10-15T01:55:44.154997Z",
                  "id": "51a01160-04ef-4113-9e44-0172e0ebfc85",
                  "motifId": null,
                  "patientId": "83d7c8f8-d0bd-476d-a865-dcc4dcff75b8",
                  "publicId": null,
                  "reason": "Détartrage",
                  "siteId": "bc1a0f81-6261-4e75-a61b-982ed33872a4",
                  "startedAt": "2025-10-10T16:15:00Z",
                  "status": "VALIDATED",
                  "templateSlug": null,
                  "updatedAt": "2026-10-15T01:55:35.162754Z",
                  "validatedAt": "2025-10-10T16:50:00Z",
                  "vetId": "22cc70d9-fddb-4e74-b4d0-91613ab6a036"
                }
              ],
              "nextCursor": "MTc5MjAyOTMzNTE2Mjc1NHw1NGI0YzQwOC02M2FkLTQ1OTEtYjlhNi1lMTJkOWI1NjJiMzc"
            },
            "status": 200
          }
        }
      },
      "post": {
        "tags": [
          "Consultations"
        ],
        "summary": "Ouvrir une consultation en brouillon",
        "description": "Ouvre une consultation en brouillon pour un animal, avec ou sans rendez-vous. Le site est celui du rendez-vous ; sans rendez-vous, c'est `site_id` (ou `Treats-Site-Id`), ou le seul site ouvert à l'application. Les modules du modèle choisi sont posés (sans `templateSlug`, le modèle par défaut du site). Le motif affiché prend le nom du motif choisi, à défaut celui du modèle. Si le rendez-vous a un acte associé, sa ligne est posée dans le module de facturation, comme quand la clinique ouvre la consultation. Le rendez-vous passe « en cours » au nom du praticien `vetId`, sauf s'il est déjà terminé, à facturer ou annulé.\n\nIdempotente par rendez-vous : si ce rendez-vous a déjà sa consultation pour cet animal, elle est rendue telle quelle avec le statut `200` (son praticien n'est pas changé). Une nouvelle consultation répond `201`. Une consultation ne s'ouvre que sur un rendez-vous du jour (heure de Paris), sauf si l'animal est déjà en salle d'attente ou en consultation. Ne valide jamais la consultation : la validation reste au vétérinaire, dans Treats.",
        "operationId": "POST_api_partner_v1_consultations",
        "security": [
          {
            "partner": [
              "consultations:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "consultations:write"
          ]
        },
        "x-treats-controller": "PartnerConsultationsController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Site de la consultation sans rendez-vous. Avec un rendez-vous, à omettre ou égal au site du rendez-vous. Le paramètre l'emporte sur l'en-tête.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "patientId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Animal examiné, visible de l'application."
                  },
                  "vetId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Membre actif de l'équipe de l'organisation qui porte la consultation (voir `GET /staff`). Le compte d'intégration de l'application n'est pas accepté."
                  },
                  "appointmentId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Rendez-vous d'origine, sur un site ouvert à l'application et pour ce même animal."
                  },
                  "templateSlug": {
                    "type": "string",
                    "description": "Modèle de consultation du site. Absent : modèle par défaut du site."
                  },
                  "motifId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Motif actif du référentiel de l'organisation."
                  }
                },
                "required": [
                  "patientId",
                  "vetId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Consultation"
                }
              }
            },
            "description": "La consultation déjà ouverte pour ce rendez-vous, rendue telle quelle."
          },
          "201": {
            "description": "La consultation créée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Consultation"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · SITE_ID_REQUIRED · SITE_MISMATCH",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "Champ hors contrat, `patientId` ou `vetId` absent, identifiant qui n'est pas un UUID, `vetId` qui n'est pas un membre actif de l'équipe de l'organisation, `motifId` absent du référentiel, `templateSlug` inconnu de ce site. Tous les champs refusés en une seule réponse."
              },
              {
                "code": "SITE_ID_REQUIRED",
                "description": "Sans rendez-vous, l'organisation a ouvert plusieurs sites et ni `site_id` ni `Treats-Site-Id` n'est donné."
              },
              {
                "code": "SITE_MISMATCH",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas le site du rendez-vous : l'omettre ou donner celui du rendez-vous."
              }
            ]
          },
          "404": {
            "description": "PATIENT_NOT_FOUND · APPOINTMENT_NOT_FOUND · SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PATIENT_NOT_FOUND",
                "description": "Animal inconnu, d'une autre organisation, ou dont le client n'est pas rattaché à un site ouvert à l'application."
              },
              {
                "code": "APPOINTMENT_NOT_FOUND",
                "description": "Rendez-vous inconnu ou sur un site non ouvert à l'application."
              },
              {
                "code": "SITE_NOT_FOUND",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un site ouvert à l'application."
              }
            ]
          },
          "409": {
            "description": "APPOINTMENT_OTHER_PATIENT · CONSULTATION_APPOINTMENT_NOT_TODAY",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "APPOINTMENT_OTHER_PATIENT",
                "description": "Le rendez-vous est celui d'un autre animal."
              },
              {
                "code": "CONSULTATION_APPOINTMENT_NOT_TODAY",
                "description": "Le rendez-vous n'est pas aujourd'hui (heure de Paris) et l'animal n'est ni en salle d'attente ni en consultation. Le message donne la date du rendez-vous."
              }
            ]
          }
        },
        "x-treats-webhooks": [
          "consultation.started",
          "appointment.updated"
        ],
        "x-treats-example": {
          "request": {
            "body": {
              "appointmentId": "58314feb-1dc6-45b3-bc4e-182debcb9fde",
              "patientId": "7dd29725-ea42-4557-bc80-4b9165d4ef76",
              "vetId": "4a1fcb38-1fe9-4f4d-8acd-10fa4f4c2540"
            },
            "method": "POST",
            "url": "/api/partner/v1/consultations"
          },
          "response": {
            "body": {
              "appointmentId": "58314feb-1dc6-45b3-bc4e-182debcb9fde",
              "category": "CONSULTATION",
              "createdAt": "2026-10-15T02:05:17.432586Z",
              "id": "c445bded-90de-4d04-a1c7-9dddc247d9d5",
              "motifId": null,
              "ownerReport": null,
              "patientId": "7dd29725-ea42-4557-bc80-4b9165d4ef76",
              "publicId": "cns_t6sh5dmsw",
              "reason": null,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "startedAt": "2026-10-15T02:05:17.432586Z",
              "status": "DRAFT",
              "templateSlug": null,
              "updatedAt": "2026-10-15T02:05:17.481711Z",
              "validatedAt": null,
              "vetId": "4a1fcb38-1fe9-4f4d-8acd-10fa4f4c2540"
            },
            "status": 201
          }
        }
      }
    },
    "/api/partner/v1/consultations/{id}": {
      "get": {
        "tags": [
          "Consultations"
        ],
        "summary": "Lire une consultation",
        "description": "Rend l'en-tête de la consultation, son type d'événement et, une fois validée, la synthèse remise au propriétaire. Le contenu clinique se lit par `GET /consultations/{id}/modules`. Lecture immédiate, sans le délai de la liste.",
        "operationId": "GET_api_partner_v1_consultations_id",
        "security": [
          {
            "partner": [
              "consultations:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "consultations:read"
          ]
        },
        "x-treats-controller": "PartnerConsultationsController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la consultation.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La consultation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Consultation"
                }
              }
            }
          },
          "404": {
            "description": "CONSULTATION_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CONSULTATION_NOT_FOUND",
                "description": "Consultation inconnue, d'une autre organisation ou d'un site non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/consultations/c14bd471-e8ea-4ab4-bd9d-4dbaffe18c3f"
          },
          "response": {
            "body": {
              "appointmentId": null,
              "category": "CONSULTATION",
              "createdAt": "2026-10-15T02:05:18.336757Z",
              "id": "c14bd471-e8ea-4ab4-bd9d-4dbaffe18c3f",
              "motifId": null,
              "ownerReport": {
                "diagnosis": null,
                "instructions": "Revenir dans un an",
                "summary": "Synthèse"
              },
              "patientId": "bc455a99-37de-4799-b8f3-74d8fb4d3c01",
              "publicId": "cns_bm6ppmxf7",
              "reason": null,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "startedAt": "2026-10-15T02:05:18.336757Z",
              "status": "VALIDATED",
              "templateSlug": null,
              "updatedAt": "2026-10-15T02:05:57.964349Z",
              "validatedAt": "2026-10-15T02:05:57.954069Z",
              "vetId": "4a1fcb38-1fe9-4f4d-8acd-10fa4f4c2540"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/consultations/{id}/documents": {
      "get": {
        "tags": [
          "Documents"
        ],
        "summary": "Lister les documents d'une consultation",
        "description": "Rend les documents rattachés à la consultation et situés sur un site ouvert à l'application, en une seule page (`nextCursor` vaut toujours `null`). Métadonnées seulement : le contenu se télécharge par `GET /documents/{id}/download`.",
        "operationId": "GET_api_partner_v1_consultations_id_documents",
        "security": [
          {
            "partner": [
              "documents:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "documents:read"
          ]
        },
        "x-treats-controller": "PartnerConsultationsController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la consultation.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les documents de la consultation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Document"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "CONSULTATION_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CONSULTATION_NOT_FOUND",
                "description": "Consultation inconnue, d'une autre organisation ou d'un site non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/consultations/c445bded-90de-4d04-a1c7-9dddc247d9d5/documents"
          },
          "response": {
            "body": {
              "items": [
                {
                  "category": "IMAGERIE",
                  "consultationId": "c445bded-90de-4d04-a1c7-9dddc247d9d5",
                  "createdAt": "2026-10-15T02:06:13.461399Z",
                  "id": "7aa6c214-49d6-48f0-9e20-b486349443f1",
                  "patientId": "7dd29725-ea42-4557-bc80-4b9165d4ef76",
                  "signedAt": null,
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "status": "GENERATED",
                  "title": "Radio",
                  "type": "UPLOAD",
                  "updatedAt": "2026-10-15T02:06:13.461399Z",
                  "validatedAt": null,
                  "visibility": "INTERNAL"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/consultations/{id}/modules": {
      "get": {
        "tags": [
          "Consultations"
        ],
        "summary": "Lire les modules d'une consultation",
        "description": "Rend tous les modules de la consultation, dans l'ordre de l'écran, en une seule page (`nextCursor` vaut toujours `null`). Pour un module écrivable (`writable: true`), `data` donne son contenu selon le contrat publié ; pour les autres (actes, vaccination, mesures, imagerie, biologie…), seul le texte lisible `text` est rendu, sans aucun prix. Les notes d'équipe passent par `GET /consultations/{id}/notes`, pas par ces modules.",
        "operationId": "GET_api_partner_v1_consultations_id_modules",
        "security": [
          {
            "partner": [
              "consultations:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "consultations:read"
          ]
        },
        "x-treats-controller": "PartnerConsultationsController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la consultation.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les modules de la consultation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ConsultationModule"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "CONSULTATION_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CONSULTATION_NOT_FOUND",
                "description": "Consultation inconnue, d'une autre organisation ou d'un site non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/consultations/c445bded-90de-4d04-a1c7-9dddc247d9d5/modules"
          },
          "response": {
            "body": {
              "items": [
                {
                  "category": "history",
                  "data": {
                    "evolution": "STABLE",
                    "reason": "Boiterie postérieure gauche depuis 2 jours, « ne pose plus la patte »",
                    "symptomOnset": "ONE_THREE_DAYS"
                  },
                  "id": "716a6b38-b5d9-449a-83e8-8426d2724538",
                  "name": "Anamnèse",
                  "position": 1,
                  "slug": "anamnese",
                  "text": "Motif de consultation : Boiterie postérieure gauche depuis 2 jours, « ne pose plus la patte »\nDébut des symptômes : 1-3 jours\nÉvolution : Stable",
                  "updatedAt": "2026-10-15T02:05:23.643009Z",
                  "writable": true
                },
                {
                  "category": "history",
                  "data": {
                    "allergies": null,
                    "currentTreatment": null,
                    "medicalHistory": null,
                    "notes": null,
                    "previousSurgeries": null
                  },
                  "id": "eed1fdbf-06a7-4b99-9755-83716696c98c",
                  "name": "Commémoratifs",
                  "position": 2,
                  "slug": "commemoratifs",
                  "text": null,
                  "updatedAt": "2026-10-15T02:05:17.453584Z",
                  "writable": true
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/consultations/{id}/modules/{slug}": {
      "put": {
        "tags": [
          "Consultations"
        ],
        "summary": "Écrire un module de consultation",
        "description": "Écrit un module d'une consultation en brouillon. Cinq modules s'écrivent : `anamnese` (`reason`, `symptomOnset`, `evolution`), `commemoratifs` (`medicalHistory`, `previousSurgeries`, `currentTreatment`, `allergies`, `notes`), `examen-general` (`normal`, `generalState`, `mucousMembranes`, `capillaryRefillSeconds`, `dehydration`, `notes`), `diagnostic` (`primary`, `differential`, `notes`) et `comportement` (`attitude`, `interaction`, `notes`). Le corps ne contient que les champs du module visé : leur type, leurs bornes et leurs valeurs permises sont dans les schémas `ModuleData…` et sur `GET /module-definitions/{slug}`. Exemple : `PUT /consultations/{id}/modules/anamnese` avec `{\"reason\": \"Boiterie depuis 2 jours\", \"symptomOnset\": \"ONE_THREE_DAYS\", \"evolution\": \"STABLE\"}`.\n\nChamp absent = inchangé, champ à `null` = effacé ; un objet (`allergies`) ou une liste (`primary`, `differential`) se remplace en entier. Tout le reste du module est gardé tel quel, dont les actes que la clinique y a posés : la facture brouillon n'est jamais touchée. L'écriture se fait sur le module relu au moment d'écrire : une saisie du vétérinaire faite entre-temps est gardée, jamais écrasée. Un corps invalide est refusé en entier avant toute écriture, avec un détail par champ (chemin compris, `primary[0].diagnosisId`).\n\nModule absent de la consultation : il est ajouté et la route répond `201`. Module déjà présent : il est mis à jour, réponse `200`.",
        "operationId": "PUT_api_partner_v1_consultations_id_modules_slug",
        "security": [
          {
            "partner": [
              "consultations:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "consultations:write"
          ]
        },
        "x-treats-controller": "PartnerConsultationsController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la consultation.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "slug",
            "in": "path",
            "description": "Module à écrire. Un autre module du site répond `409 MODULE_READ_ONLY`, un module inconnu `404 MODULE_NOT_FOUND`.",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "anamnese",
                "commemoratifs",
                "examen-general",
                "diagnostic",
                "comportement"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "description": "`anamnese` : motif de consultation, dans les mots du propriétaire.",
                    "nullable": true,
                    "maxLength": 20000
                  },
                  "symptomOnset": {
                    "type": "string",
                    "enum": [
                      "LESS_24H",
                      "ONE_THREE_DAYS",
                      "MORE_THREE_DAYS",
                      "CHRONIC"
                    ],
                    "description": "`anamnese` : début des symptômes.",
                    "nullable": true
                  },
                  "evolution": {
                    "type": "string",
                    "enum": [
                      "IMPROVEMENT",
                      "STABLE",
                      "WORSENING"
                    ],
                    "description": "`anamnese` : évolution.",
                    "nullable": true
                  },
                  "medicalHistory": {
                    "type": "string",
                    "description": "`commemoratifs` : antécédents médicaux.",
                    "nullable": true,
                    "maxLength": 20000
                  },
                  "previousSurgeries": {
                    "type": "string",
                    "description": "`commemoratifs` : chirurgies antérieures.",
                    "nullable": true,
                    "maxLength": 20000
                  },
                  "currentTreatment": {
                    "type": "string",
                    "description": "`commemoratifs` : traitement en cours.",
                    "nullable": true,
                    "maxLength": 20000
                  },
                  "allergies": {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "enum": [
                          "FOOD",
                          "MEDICATION",
                          "OTHER"
                        ],
                        "description": "Type d'allergie.",
                        "nullable": true
                      },
                      "allergen": {
                        "type": "string",
                        "description": "Allergène.",
                        "nullable": true,
                        "maxLength": 500
                      }
                    },
                    "description": "`commemoratifs` : allergie, objet remplacé en entier. Les deux champs absents ou nuls effacent l'allergie. Un autre champ que `type` et `allergen` est refusé.",
                    "nullable": true
                  },
                  "normal": {
                    "type": "boolean",
                    "description": "`examen-general` : `true` pour un examen sans particularité (RAS).",
                    "nullable": true
                  },
                  "generalState": {
                    "type": "string",
                    "enum": [
                      "GOOD",
                      "CORRECT",
                      "ALTERED",
                      "BAD"
                    ],
                    "description": "`examen-general` : état général.",
                    "nullable": true
                  },
                  "mucousMembranes": {
                    "type": "string",
                    "enum": [
                      "PINK",
                      "PALE",
                      "CONGESTED",
                      "ICTERIC",
                      "CYANOTIC"
                    ],
                    "description": "`examen-general` : muqueuses.",
                    "nullable": true
                  },
                  "capillaryRefillSeconds": {
                    "type": "number",
                    "description": "`examen-general` : temps de recoloration capillaire, en secondes (0 à 20).",
                    "nullable": true,
                    "minimum": 0,
                    "maximum": 20
                  },
                  "dehydration": {
                    "type": "string",
                    "enum": [
                      "NONE",
                      "FIVE_PERCENT",
                      "SIX_EIGHT_PERCENT",
                      "TEN_TWELVE_PERCENT",
                      "TWELVE_FIFTEEN_PERCENT"
                    ],
                    "description": "`examen-general` : déshydratation.",
                    "nullable": true
                  },
                  "primary": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "label": {
                          "type": "string",
                          "description": "Libellé. Obligatoire sauf si `diagnosisId` est donné (le nom du diagnostic est alors repris).",
                          "maxLength": 500
                        },
                        "diagnosisId": {
                          "type": "string",
                          "format": "uuid",
                          "description": "Diagnostic actif du référentiel de l'organisation, sinon refus.",
                          "nullable": true
                        }
                      }
                    },
                    "description": "`diagnostic` : diagnostic principal, 30 éléments au plus, liste remplacée en entier ; `null` ou `[]` l'efface. Un autre champ que `label` et `diagnosisId` est refusé.",
                    "nullable": true
                  },
                  "differential": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "label": {
                          "type": "string",
                          "description": "Libellé. Obligatoire sauf si `diagnosisId` est donné.",
                          "maxLength": 500
                        },
                        "diagnosisId": {
                          "type": "string",
                          "format": "uuid",
                          "description": "Diagnostic actif du référentiel de l'organisation, sinon refus.",
                          "nullable": true
                        }
                      }
                    },
                    "description": "`diagnostic` : diagnostic différentiel, mêmes règles que `primary`.",
                    "nullable": true
                  },
                  "attitude": {
                    "type": "string",
                    "enum": [
                      "NORMAL",
                      "DEPRESSED",
                      "AGGRESSIVE",
                      "ANXIOUS"
                    ],
                    "description": "`comportement` : attitude.",
                    "nullable": true
                  },
                  "interaction": {
                    "type": "string",
                    "enum": [
                      "NORMAL",
                      "DIMINISHED",
                      "ABSENT"
                    ],
                    "description": "`comportement` : interaction.",
                    "nullable": true
                  },
                  "notes": {
                    "type": "string",
                    "description": "`commemoratifs`, `examen-general`, `diagnostic`, `comportement` : notes du module. Celles des commémoratifs et de l'examen général sont réservées à l'équipe (jamais imprimées sur le compte rendu).",
                    "nullable": true,
                    "maxLength": 20000
                  }
                }
              }
            }
          },
          "description": "Les champs du module visé seulement, tous facultatifs. Un champ d'un autre module est refusé (`400 INVALID_FIELD`)."
        },
        "responses": {
          "200": {
            "description": "Le module mis à jour, relu après écriture.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConsultationModule"
                }
              }
            }
          },
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConsultationModule"
                }
              }
            },
            "description": "Le module ajouté à la consultation, relu après écriture."
          },
          "400": {
            "description": "BODY_REQUIRED · INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "BODY_REQUIRED",
                "description": "Corps absent ou vide."
              },
              {
                "code": "INVALID_FIELD",
                "description": "Champ inconnu du contrat de ce module, texte trop long, valeur hors de la liste permise (sensible à la casse), nombre hors bornes, mauvais type, `label` manquant, `diagnosisId` qui n'est pas un UUID ou absent du référentiel. Un détail par champ, avec son chemin ; rien n'est écrit."
              }
            ]
          },
          "404": {
            "description": "CONSULTATION_NOT_FOUND · MODULE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CONSULTATION_NOT_FOUND",
                "description": "Consultation inconnue, d'une autre organisation ou d'un site non ouvert à l'application."
              },
              {
                "code": "MODULE_NOT_FOUND",
                "description": "Aucun module de ce nom sur le site de la consultation (liste : `GET /module-definitions`)."
              }
            ]
          },
          "409": {
            "description": "MODULE_READ_ONLY · CONSULTATION_VALIDATED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "MODULE_READ_ONLY",
                "description": "Module en lecture seule par l'API (actes, vaccination, mesures, imagerie…). Le message liste les modules écrivables."
              },
              {
                "code": "CONSULTATION_VALIDATED",
                "description": "La consultation est validée : elle ne se modifie plus par l'API. Seul un vétérinaire peut la déverrouiller, dans Treats."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "body": {
              "notes": "Réexaminé par le partenaire"
            },
            "method": "PUT",
            "url": "/api/partner/v1/consultations/c445bded-90de-4d04-a1c7-9dddc247d9d5/modules/examen-general"
          },
          "response": {
            "body": {
              "category": "examination",
              "data": {
                "capillaryRefillSeconds": 1.5,
                "dehydration": "NONE",
                "generalState": "CORRECT",
                "mucousMembranes": "PINK",
                "normal": false,
                "notes": "Réexaminé par le partenaire"
              },
              "id": "bfc18cc8-e770-4894-b0b9-537b612971ea",
              "name": "Examen général",
              "position": 3,
              "slug": "examen-general",
              "text": "État général : Bon\nMuqueuses : Roses\nTRC : Normal (≤ 2s)\nDéshydratation : Aucune\nNotes : Réexaminé par le partenaire",
              "updatedAt": "2026-10-15T02:05:54.248451Z",
              "writable": true
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/consultations/{id}/notes": {
      "get": {
        "tags": [
          "Consultations"
        ],
        "summary": "Lire les notes d'équipe d'une consultation",
        "description": "Rend les notes d'équipe de la consultation, en une seule page (`nextCursor` vaut toujours `null`). Les notes confidentielles ne sont jamais rendues. Exige le droit `consultations:notes`, distinct de `consultations:write` qui permet d'écrire une note : une application peut signaler « résultats reçus » sans lire les notes de l'équipe.",
        "operationId": "GET_api_partner_v1_consultations_id_notes",
        "security": [
          {
            "partner": [
              "consultations:notes"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "consultations:notes"
          ]
        },
        "x-treats-controller": "PartnerConsultationsController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la consultation.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les notes d'équipe.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ConsultationNote"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "CONSULTATION_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CONSULTATION_NOT_FOUND",
                "description": "Consultation inconnue, d'une autre organisation ou d'un site non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/consultations/c445bded-90de-4d04-a1c7-9dddc247d9d5/notes"
          },
          "response": {
            "body": {
              "items": [
                {
                  "authorUserId": "4a1fcb38-1fe9-4f4d-8acd-10fa4f4c2540",
                  "consultationId": "c445bded-90de-4d04-a1c7-9dddc247d9d5",
                  "content": "Note équipe",
                  "createdAt": "2026-10-15T02:06:01.687854Z",
                  "createdByThisApplication": false,
                  "id": "dd9e6982-f00a-4357-a7d2-b1dcc200f5f8"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      },
      "post": {
        "tags": [
          "Consultations"
        ],
        "summary": "Ajouter une note d'équipe",
        "description": "Ajoute une note d'équipe à la consultation, signée par le compte d'intégration de l'application. La note n'est jamais confidentielle. Permise aussi sur une consultation validée : une note s'ajoute, elle ne modifie pas la consultation. Une note ne se modifie ni ne se supprime par l'API.",
        "operationId": "POST_api_partner_v1_consultations_id_notes",
        "security": [
          {
            "partner": [
              "consultations:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "consultations:write"
          ]
        },
        "x-treats-controller": "PartnerConsultationsController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la consultation.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "content": {
                    "type": "string",
                    "description": "Texte de la note, non vide.",
                    "maxLength": 10000
                  }
                },
                "required": [
                  "content"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "La note créée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConsultationNote"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`content` absent, vide ou de plus de 10 000 caractères ; champ hors contrat. `confidential`, `isConfidential`, `private` ou `isPrivate` sont refusés : une note confidentielle ne s'écrit pas par l'API."
              }
            ]
          },
          "404": {
            "description": "CONSULTATION_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CONSULTATION_NOT_FOUND",
                "description": "Consultation inconnue, d'une autre organisation ou d'un site non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "body": {
              "content": "Résultats reçus (partenaire)"
            },
            "method": "POST",
            "url": "/api/partner/v1/consultations/c445bded-90de-4d04-a1c7-9dddc247d9d5/notes"
          },
          "response": {
            "body": {
              "authorUserId": "27f21172-8f2e-432d-8767-26380372c389",
              "consultationId": "c445bded-90de-4d04-a1c7-9dddc247d9d5",
              "content": "Résultats reçus (partenaire)",
              "createdAt": "2026-10-15T02:06:05.289060Z",
              "createdByThisApplication": true,
              "id": "322bc567-416c-4954-bbe4-8eaf2286c459"
            },
            "status": 201
          }
        }
      }
    },
    "/api/partner/v1/credit-notes": {
      "get": {
        "tags": [
          "Facturation"
        ],
        "summary": "Lister les avoirs",
        "description": "Rend une page des avoirs émis par les sites ouverts à l'application, sans leurs lignes ni leur restitution (à lire au détail). Un avoir ne se modifie pas après émission : sa date de synchronisation est sa date d'émission.\n\nTri par date d'émission puis identifiant. Les avoirs des 60 dernières secondes n'apparaissent qu'aux appels suivants ; la lecture par identifiant est immédiate. Un `client_id` ou un `invoice_id` inconnu rend une liste vide, pas une erreur.",
        "operationId": "GET_api_partner_v1_credit_notes",
        "security": [
          {
            "partner": [
              "billing:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "billing:read"
          ]
        },
        "x-treats-controller": "PartnerCreditNoteController",
        "parameters": [
          {
            "name": "client_id",
            "in": "query",
            "description": "Restreint aux avoirs de ce client.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "invoice_id",
            "in": "query",
            "description": "Restreint aux avoirs de cette facture.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "issued_from",
            "in": "query",
            "description": "Jour d'émission minimal, inclus (`AAAA-MM-JJ`, heure de Paris).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "issued_to",
            "in": "query",
            "description": "Jour d'émission maximal, inclus (`AAAA-MM-JJ`, heure de Paris).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Restreint à un site ouvert à l'application (ou passer l'en-tête `Treats-Site-Id`). Absent : tous les sites ouverts.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Ne rend que les avoirs émis à cet instant ou après (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Curseur `nextCursor` de la page précédente, repassé tel quel. Absent : première page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Nombre d'avoirs par page, de 1 à 200 (au-delà de 200, ramené à 200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Site visé, à la place du paramètre `site_id`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page d'avoirs (`lines`, `settlement` et `amountDueToClientCents` à `null`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CreditNote"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "Date `issued_from` / `issued_to` mal formée ou hors des années 1900 à 9999, `client_id`, `invoice_id` ou `updated_since` mal formé, `limit` inférieur à 1 ou non entier, curseur illisible. Un détail par champ."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un identifiant valide."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé n'est pas ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/credit-notes?invoice_id=e6303709-375d-4067-af6a-360bb484be7e"
          },
          "response": {
            "body": {
              "items": [
                {
                  "amountDueToClientCents": null,
                  "clientId": "e58157c4-b62f-48b0-b40e-a5c2a6bbdc4e",
                  "currency": "EUR",
                  "id": "0ee3aa8f-2fab-435b-b50c-5d1e81799ac1",
                  "invoiceId": "e6303709-375d-4067-af6a-360bb484be7e",
                  "invoiceNumber": "REN-2026-00001",
                  "issuedAt": "2026-10-15T02:06:35.150059Z",
                  "lines": null,
                  "number": "AV-REN-2026-00001",
                  "reason": "PRICING_ERROR",
                  "reasonDetails": "",
                  "settlement": null,
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "totalHtCents": 1000,
                  "totalTtcCents": 1200,
                  "totalVatCents": 200,
                  "type": "PARTIAL",
                  "updatedAt": "2026-10-15T02:06:35.150059Z"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/credit-notes/{id}": {
      "get": {
        "tags": [
          "Facturation"
        ],
        "summary": "Lire un avoir",
        "description": "Rend le détail d'un avoir émis par un site ouvert à l'application : ses lignes, la ligne de facture que chacune corrige, ce qui reste à rendre au client et la restitution faite (remboursement ou acompte). Un avoir d'un site non ouvert répond 404.",
        "operationId": "GET_api_partner_v1_credit_notes_id",
        "security": [
          {
            "partner": [
              "billing:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "billing:read"
          ]
        },
        "x-treats-controller": "PartnerCreditNoteController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de l'avoir.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "L'avoir, avec ses lignes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreditNote"
                }
              }
            }
          },
          "404": {
            "description": "CREDIT_NOTE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CREDIT_NOTE_NOT_FOUND",
                "description": "Aucun avoir de ce périmètre ne porte cet identifiant."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/credit-notes/0ee3aa8f-2fab-435b-b50c-5d1e81799ac1"
          },
          "response": {
            "body": {
              "amountDueToClientCents": 0,
              "clientId": "e58157c4-b62f-48b0-b40e-a5c2a6bbdc4e",
              "currency": "EUR",
              "id": "0ee3aa8f-2fab-435b-b50c-5d1e81799ac1",
              "invoiceId": "e6303709-375d-4067-af6a-360bb484be7e",
              "invoiceNumber": "REN-2026-00001",
              "issuedAt": "2026-10-15T02:06:35.150059Z",
              "lines": [
                {
                  "id": "e9c5f5fe-ab93-4225-83a5-89e2a95ae374",
                  "invoiceLineId": "49e35b75-e940-4c09-ae26-dc083ef0ee96",
                  "label": "Consultation",
                  "quantity": 1,
                  "totalHtCents": 1000,
                  "unitPriceHtCents": 1000,
                  "vatRatePercent": 20
                }
              ],
              "number": "AV-REN-2026-00001",
              "reason": "PRICING_ERROR",
              "reasonDetails": "",
              "settlement": null,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "totalHtCents": 1000,
              "totalTtcCents": 1200,
              "totalVatCents": 200,
              "type": "PARTIAL",
              "updatedAt": "2026-10-15T02:06:35.150059Z"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/credit-notes/{id}/pdf": {
      "get": {
        "tags": [
          "Facturation"
        ],
        "summary": "Télécharger le PDF d'un avoir",
        "description": "Rend le PDF officiel de l'avoir, avec l'en-tête du site émetteur. Même périmètre que `GET /credit-notes/{id}`. Le fichier est servi avec `Content-Disposition: inline; filename=\"avoir-<id>.pdf\"`.",
        "operationId": "GET_api_partner_v1_credit_notes_id_pdf",
        "security": [
          {
            "partner": [
              "billing:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "billing:read"
          ]
        },
        "x-treats-controller": "PartnerCreditNoteController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de l'avoir.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Le PDF de l'avoir.",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "CREDIT_NOTE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CREDIT_NOTE_NOT_FOUND",
                "description": "Aucun avoir de ce périmètre ne porte cet identifiant."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/credit-notes/0ee3aa8f-2fab-435b-b50c-5d1e81799ac1/pdf"
          },
          "response": {
            "contentType": "application/pdf",
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/documents/{id}": {
      "get": {
        "tags": [
          "Documents"
        ],
        "summary": "Lire les métadonnées d'un document",
        "description": "Rend les métadonnées d'un document d'un site ouvert à l'application : nature, rubrique, titre, statut, visibilité, dates. Jamais son contenu ni ses notes internes : le contenu se télécharge par `GET /documents/{id}/download`.",
        "operationId": "GET_api_partner_v1_documents_id",
        "security": [
          {
            "partner": [
              "documents:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "documents:read"
          ]
        },
        "x-treats-controller": "PartnerDocumentsController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant du document.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Le document.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Document"
                }
              }
            }
          },
          "404": {
            "description": "DOCUMENT_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "DOCUMENT_NOT_FOUND",
                "description": "Document inconnu, d'une autre organisation ou d'un site non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/documents/dfa44859-81d6-4a8f-9145-c6e02a4ff4a1"
          },
          "response": {
            "body": {
              "category": "RESULTATS_LABO",
              "consultationId": null,
              "createdAt": "2026-10-15T02:06:09.218367Z",
              "id": "dfa44859-81d6-4a8f-9145-c6e02a4ff4a1",
              "patientId": "7dd29725-ea42-4557-bc80-4b9165d4ef76",
              "signedAt": null,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "status": "GENERATED",
              "title": "Bilan",
              "type": "UPLOAD",
              "updatedAt": "2026-10-15T02:06:09.218367Z",
              "validatedAt": null,
              "visibility": "INTERNAL"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/documents/{id}/content": {
      "get": {
        "tags": [
          "Documents"
        ],
        "summary": "Télécharger le contenu d'un document",
        "description": "Rend les octets du document, après le même contrôle du site à chaque appel. Un fichier déposé est rendu avec son propre type (`application/pdf` pour tout dépôt par l'API ; une image ou un autre fichier déposé par la clinique garde le sien). Un document composé dans Treats est rendu en PDF à la demande. En-têtes `Content-Disposition: attachment` (nom d'origine d'un fichier déposé ; titre suivi de `.pdf` pour un document composé) et `Cache-Control: private, no-store` : ne pas mettre ce contenu en cache partagé. C'est l'adresse que rend `GET /documents/{id}/download` quand `requiresAuthorization` vaut `true`.",
        "operationId": "GET_api_partner_v1_documents_id_content",
        "security": [
          {
            "partner": [
              "documents:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "documents:read"
          ]
        },
        "x-treats-controller": "PartnerDocumentsController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant du document.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Le contenu du document (PDF le plus souvent ; le type d'origine pour un fichier déposé par la clinique).",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "DOCUMENT_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "DOCUMENT_NOT_FOUND",
                "description": "Document inconnu, d'une autre organisation ou d'un site non ouvert à l'application."
              }
            ]
          }
        }
      }
    },
    "/api/partner/v1/documents/{id}/download": {
      "get": {
        "tags": [
          "Documents"
        ],
        "summary": "Obtenir l'adresse de téléchargement d'un document",
        "description": "Rend l'adresse où télécharger le contenu du document, produite seulement après le contrôle du site. Pour un fichier stocké, en production : une adresse pré-signée du stockage, valable 15 minutes, à suivre SANS en-tête d'autorisation (`requiresAuthorization: false`). Pour un document composé dans Treats (compte rendu, ordonnance… rendus en PDF à la demande), ou si le stockage ne pré-signe pas : la route `GET /documents/{id}/content` de l'API, à appeler avec le jeton (`requiresAuthorization: true`). Lire `requiresAuthorization` plutôt que de deviner à l'adresse. Une adresse pré-signée expirée se redemande ici.",
        "operationId": "GET_api_partner_v1_documents_id_download",
        "security": [
          {
            "partner": [
              "documents:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "documents:read"
          ]
        },
        "x-treats-controller": "PartnerDocumentsController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant du document.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "L'adresse de téléchargement.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentDownload"
                }
              }
            }
          },
          "404": {
            "description": "DOCUMENT_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "DOCUMENT_NOT_FOUND",
                "description": "Document inconnu, d'une autre organisation ou d'un site non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/documents/dfa44859-81d6-4a8f-9145-c6e02a4ff4a1/download"
          },
          "response": {
            "body": {
              "expiresAt": null,
              "requiresAuthorization": true,
              "url": "http://localhost:8141/api/partner/v1/documents/dfa44859-81d6-4a8f-9145-c6e02a4ff4a1/content"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/imaging/exams": {
      "get": {
        "tags": [
          "Imagerie"
        ],
        "summary": "Lister les demandes d'examen d'imagerie",
        "description": "Rend les demandes d'examen adressées aux fournisseurs que représente l'application, sur les sites ouverts par l'organisation (ou sur le seul site demandé). Tri par date de modification croissante puis identifiant ; une demande modifiée apparaît dans la liste au plus 60 secondes après, alors que `GET /imaging/exams/{id}` la rend tout de suite. Pour suivre les examens à réaliser, lire `status=REQUESTED` avec `updated_since`. La liste ne rend pas les fichiers reçus, seulement leur décompte.",
        "operationId": "GET_api_partner_v1_imaging_exams",
        "security": [
          {
            "partner": [
              "imaging:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "imaging:write"
          ]
        },
        "x-treats-controller": "PartnerImagingController",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Filtre de statut, insensible à la casse. Absent : tous les statuts.",
            "schema": {
              "type": "string",
              "enum": [
                "REQUESTED",
                "RECEIVED",
                "CANCELLED"
              ]
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Limite la liste à un site ouvert à l'application (sinon `404 SITE_NOT_FOUND`). Absent : tous les sites ouverts.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Ne rend que les demandes modifiées à cet instant ou après (inclusif).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Curseur opaque rendu par la page précédente (`nextCursor`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Taille de page, de 1 à 200 (au-delà, 200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page de demandes d'examen.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ImagingExam"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`status` hors de `REQUESTED`, `RECEIVED`, `CANCELLED`, `updated_since` illisible, `limit` non entier ou nul, `cursor` illisible."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "403": {
            "description": "PARTNER_PROVIDER_NOT_LINKED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PARTNER_PROVIDER_NOT_LINKED",
                "description": "L'application n'est rattachée à aucun laboratoire ni fournisseur d'imagerie : le rattachement se demande à Treats."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé n'est pas ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/imaging/exams?status=RECEIVED&limit=200"
          },
          "response": {
            "body": {
              "items": [
                {
                  "accessionNumber": "800000000",
                  "examAt": "2026-10-15T02:17:16.556734Z",
                  "id": "db5cd85a-27d0-4b4c-ad11-949e9b375b60",
                  "modality": "RADIO",
                  "patient": {
                    "birthDate": "2019-04-12",
                    "breed": null,
                    "id": "e47568bf-3601-4acc-9808-8ff6a07f449b",
                    "microchipNumber": "250269203755048",
                    "name": "Ginger",
                    "owner": {
                      "firstName": "Marie",
                      "lastName": "Labimg"
                    },
                    "publicId": "pat_yntvjdr7t",
                    "sex": "FEMALE_SPAYED",
                    "species": "CAT"
                  },
                  "provider": "PICOXIA",
                  "receivedAt": "2026-10-15T02:17:30.381211Z",
                  "requestedAt": "2026-10-15T02:17:16.560334Z",
                  "results": {
                    "imageCount": 2,
                    "reportReceived": true,
                    "studyLink": "https://pacs.example/study/abc"
                  },
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "status": "RECEIVED",
                  "updatedAt": "2026-10-15T02:17:30.381212Z",
                  "veterinarian": "Admin Rennes"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/imaging/exams/{id}": {
      "get": {
        "tags": [
          "Imagerie"
        ],
        "summary": "Lire une demande d'examen d'imagerie",
        "description": "Rend une demande d'examen avec l'animal et l'état des résultats reçus. La demande doit être d'un site ouvert à l'application et adressée à l'un de ses fournisseurs ; sinon la route répond 404, sans dire si elle existe ailleurs. Lecture immédiate, sans le délai des listes.",
        "operationId": "GET_api_partner_v1_imaging_exams_id",
        "security": [
          {
            "partner": [
              "imaging:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "imaging:write"
          ]
        },
        "x-treats-controller": "PartnerImagingController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la demande.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La demande.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImagingExam"
                }
              }
            }
          },
          "403": {
            "description": "PARTNER_PROVIDER_NOT_LINKED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PARTNER_PROVIDER_NOT_LINKED",
                "description": "L'application n'est rattachée à aucun laboratoire ni fournisseur d'imagerie."
              }
            ]
          },
          "404": {
            "description": "NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "NOT_FOUND",
                "description": "Demande inconnue, d'un site non ouvert, adressée à un autre fournisseur, ou identifiant qui n'est pas un UUID."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/imaging/exams/db5cd85a-27d0-4b4c-ad11-949e9b375b60"
          },
          "response": {
            "body": {
              "accessionNumber": "800000000",
              "examAt": "2026-10-15T02:17:16.556734Z",
              "id": "db5cd85a-27d0-4b4c-ad11-949e9b375b60",
              "modality": "RADIO",
              "patient": {
                "birthDate": "2019-04-12",
                "breed": null,
                "id": "e47568bf-3601-4acc-9808-8ff6a07f449b",
                "microchipNumber": "250269203755048",
                "name": "Ginger",
                "owner": {
                  "firstName": "Marie",
                  "lastName": "Labimg"
                },
                "publicId": "pat_yntvjdr7t",
                "sex": "FEMALE_SPAYED",
                "species": "CAT"
              },
              "provider": "PICOXIA",
              "receivedAt": "2026-10-15T02:17:30.381211Z",
              "requestedAt": "2026-10-15T02:17:16.560334Z",
              "results": {
                "imageCount": 2,
                "reportReceived": true,
                "studyLink": "https://pacs.example/study/abc"
              },
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "status": "RECEIVED",
              "updatedAt": "2026-10-15T02:17:30.381212Z",
              "veterinarian": "Admin Rennes"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/imaging/exams/{id}/results": {
      "post": {
        "tags": [
          "Imagerie"
        ],
        "summary": "Déposer les résultats d'un examen d'imagerie",
        "description": "Dépose, en `multipart/form-data` uniquement (un corps JSON est refusé), les images, le compte rendu PDF et le lien vers l'étude d'une demande. Les fichiers entrent parmi les documents d'imagerie de l'animal, la demande passe `RECEIVED` et la consultation d'origine est prévenue. Tout est contrôlé avant la première écriture : un envoi refusé n'écrit rien. Réponse `200` dans tous les cas, premier dépôt compris.\n\nTreats dédoublonne fichier par fichier, selon la position : la n-ième image d'un envoi (`images[n]`) et le compte rendu ne sont enregistrés qu'une fois par examen. Un second envoi identique, avec une autre `Idempotency-Key`, n'ajoute rien et répond `alreadyReceived: true` ; un envoi qui porte plus d'images ajoute seulement les positions nouvelles. Une image différente envoyée à une position déjà reçue est donc ignorée : envoyer toutes les images de l'examen en un seul dépôt. Le lien `studyLink` n'est enregistré que si le dépôt ajoute au moins un fichier. La même `Idempotency-Key` rejoue la première réponse.",
        "operationId": "POST_api_partner_v1_imaging_exams_id_results",
        "security": [
          {
            "partner": [
              "imaging:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "imaging:write"
          ]
        },
        "x-treats-controller": "PartnerImagingController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la demande d'examen.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "images": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "Images de l'examen, en JPEG (`image/jpeg`) ou PNG (`image/png`) : champ répétable (`images` ou `images[]`), 30 au plus, 25 Mo au plus chacune. Le type déclaré ET la signature du contenu sont vérifiés."
                  },
                  "report": {
                    "type": "string",
                    "format": "binary",
                    "description": "Compte rendu, déclaré `application/pdf`, 25 Mo au plus, contenu commençant par `%PDF`."
                  },
                  "studyLink": {
                    "type": "string",
                    "description": "Lien vers l'étude chez le fournisseur (visionneuse) : adresse `http://` ou `https://`, 2 000 caractères au plus.",
                    "maxLength": 2000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accusé de réception du dépôt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImagingResultsReceipt"
                }
              }
            }
          },
          "400": {
            "description": "MULTIPART_REQUIRED · MULTIPART_INVALID · NO_FILE · FILE_TOO_LARGE · UNSUPPORTED_FILE_TYPE · INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "MULTIPART_REQUIRED",
                "description": "Corps qui n'est pas en `multipart/form-data` (un corps JSON, par exemple)."
              },
              {
                "code": "MULTIPART_INVALID",
                "description": "Envoi multipart illisible."
              },
              {
                "code": "NO_FILE",
                "description": "Ni image (`images`) ni compte rendu (`report`)."
              },
              {
                "code": "FILE_TOO_LARGE",
                "description": "Un fichier dépasse 25 Mo (le champ fautif est désigné dans `details` : `images[0]`, `report`), ou l'envoi dépasse 200 Mo."
              },
              {
                "code": "UNSUPPORTED_FILE_TYPE",
                "description": "Image qui n'est pas un JPEG ou un PNG, ou compte rendu qui n'est pas un PDF, par le type déclaré ou par le contenu (un PDF déclaré `image/jpeg` est refusé). Champ fautif dans `details`."
              },
              {
                "code": "INVALID_FIELD",
                "description": "Plus de 30 images, ou `studyLink` qui n'est pas un lien http(s) de 2 000 caractères au plus."
              }
            ]
          },
          "403": {
            "description": "PARTNER_PROVIDER_NOT_LINKED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PARTNER_PROVIDER_NOT_LINKED",
                "description": "L'application n'est rattachée à aucun laboratoire ni fournisseur d'imagerie."
              }
            ]
          },
          "404": {
            "description": "NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "NOT_FOUND",
                "description": "Demande inconnue, d'un site non ouvert, adressée à un autre fournisseur, ou identifiant qui n'est pas un UUID."
              }
            ]
          },
          "409": {
            "description": "IMAGING_EXAM_CANCELLED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "IMAGING_EXAM_CANCELLED",
                "description": "La demande a été annulée par la clinique : aucun résultat n'est accepté."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "form": {
              "images": "<fichier>",
              "report": "<fichier>",
              "studyLink": "https://pacs.example/study/abc"
            },
            "method": "POST",
            "url": "/api/partner/v1/imaging/exams/db5cd85a-27d0-4b4c-ad11-949e9b375b60/results"
          },
          "response": {
            "body": {
              "alreadyReceived": false,
              "examId": "db5cd85a-27d0-4b4c-ad11-949e9b375b60",
              "imageCount": 2,
              "reportReceived": true,
              "status": "RECEIVED"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/invoices": {
      "get": {
        "tags": [
          "Facturation"
        ],
        "summary": "Lister les factures émises",
        "description": "Rend une page des factures émises par les sites ouverts à l'application, tous clients confondus. Les brouillons n'existent pas pour l'API : ni numéro légal, ni montant définitif. Les pièces reprises d'un ancien logiciel qui ne sont pas des factures (devis, bons de livraison, provisoires) sont exclues, même payées.\n\nTri par date de synchronisation (`updatedAt`) puis identifiant : une facture réglée, créditée ou annulée revient dans `updated_since`. Les modifications des 60 dernières secondes n'apparaissent qu'aux appels suivants ; la lecture par identifiant est immédiate. Un `client_id` inconnu rend une liste vide, pas une erreur.",
        "operationId": "GET_api_partner_v1_invoices",
        "security": [
          {
            "partner": [
              "billing:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "billing:read"
          ]
        },
        "x-treats-controller": "PartnerInvoiceController",
        "parameters": [
          {
            "name": "client_id",
            "in": "query",
            "description": "Restreint aux factures de ce client.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Restreint à ce statut. Insensible à la casse. `DRAFT` est refusé.",
            "schema": {
              "type": "string",
              "enum": [
                "PENDING",
                "PARTIAL",
                "PAID",
                "CANCELLED"
              ]
            }
          },
          {
            "name": "issued_from",
            "in": "query",
            "description": "Jour d'émission minimal, inclus (`AAAA-MM-JJ`, heure de Paris).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "issued_to",
            "in": "query",
            "description": "Jour d'émission maximal, inclus (`AAAA-MM-JJ`, heure de Paris).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Restreint à un site ouvert à l'application (ou passer l'en-tête `Treats-Site-Id`). Absent : tous les sites ouverts.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Ne rend que les factures dont `updatedAt` est postérieur ou égal à cet instant (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Curseur `nextCursor` de la page précédente, repassé tel quel. Absent : première page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Nombre de factures par page, de 1 à 200 (au-delà de 200, ramené à 200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Site visé, à la place du paramètre `site_id`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page de factures émises.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InvoiceSummary"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`status` hors liste (dont `DRAFT` : les brouillons ne sont pas exposés), date `issued_from` / `issued_to` mal formée ou hors des années 1900 à 9999, `client_id` ou `updated_since` mal formé, `limit` inférieur à 1 ou non entier, curseur illisible. Un détail par champ."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un identifiant valide."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé n'est pas ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/invoices?status=PARTIAL&client_id=e58157c4-b62f-48b0-b40e-a5c2a6bbdc4e"
          },
          "response": {
            "body": {
              "items": [
                {
                  "balanceDueCents": 4500,
                  "clientId": "e58157c4-b62f-48b0-b40e-a5c2a6bbdc4e",
                  "creditedCents": 1200,
                  "currency": "EUR",
                  "dueDate": "2026-11-14",
                  "id": "e6303709-375d-4067-af6a-360bb484be7e",
                  "issuedAt": "2026-10-15T02:06:34.924071Z",
                  "number": "REN-2026-00001",
                  "paidCents": 2000,
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "status": "PARTIAL",
                  "totalHtCents": 6726,
                  "totalTtcCents": 7700,
                  "totalVatCents": 974,
                  "updatedAt": "2026-10-15T02:06:35.221673Z"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/invoices/{id}": {
      "get": {
        "tags": [
          "Facturation"
        ],
        "summary": "Lire une facture émise",
        "description": "Rend le détail d'une facture émise par un site ouvert à l'application : lignes imprimées, ventilation de TVA, totaux, encaissé, avoirs et reste à payer. Les totaux sont ceux du document émis ; la ventilation de TVA leur est égale au centime. Un brouillon, une pièce reprise qui n'est pas une facture, ou une facture d'un site non ouvert répond 404.",
        "operationId": "GET_api_partner_v1_invoices_id",
        "security": [
          {
            "partner": [
              "billing:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "billing:read"
          ]
        },
        "x-treats-controller": "PartnerInvoiceController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la facture.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La facture.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "404": {
            "description": "INVOICE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVOICE_NOT_FOUND",
                "description": "Aucune facture émise de ce périmètre ne porte cet identifiant (brouillon, autre organisation, site non ouvert, pièce qui n'est pas une facture)."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/invoices/c777d792-7399-4f78-94f3-bdd3d07c5c12"
          },
          "response": {
            "body": {
              "balanceDueCents": 0,
              "cancelledAt": null,
              "clientId": "885c98b4-4b3a-48f5-8033-18d0915ac0ae",
              "consultationIds": [
                "d165b307-8138-4a61-a40e-3e03503dc6ca"
              ],
              "creditedCents": 0,
              "currency": "EUR",
              "discountHtCents": 0,
              "dueDate": null,
              "globalDiscountPercent": null,
              "id": "c777d792-7399-4f78-94f3-bdd3d07c5c12",
              "issuedAt": "2026-03-07T14:37:00Z",
              "lines": [
                {
                  "catalogItemId": "2bfe854e-998d-445f-8244-88492eda84b6",
                  "consultationId": null,
                  "discountPercent": null,
                  "id": "e70adf09-2a65-4645-91e4-478bc9fa1614",
                  "label": "Consultation comportementale avec compte rendu, exercices demandant préparation",
                  "patientId": null,
                  "quantity": 1,
                  "totalHtCents": 9167,
                  "totalTtcCents": 11000,
                  "totalVatCents": 1833,
                  "type": "ACT",
                  "unit": null,
                  "unitPriceHtCents": 9167,
                  "unitPriceTtcCents": 11000,
                  "vatRatePercent": 20
                },
                {
                  "catalogItemId": "b812ffed-4bdf-44cb-85de-4bf8ab448559",
                  "consultationId": null,
                  "discountPercent": null,
                  "id": "579c5e2e-a5a1-4024-ba52-0d2ae573366a",
                  "label": "Consultation NAC",
                  "patientId": null,
                  "quantity": 1,
                  "totalHtCents": 3850,
                  "totalTtcCents": 4620,
                  "totalVatCents": 770,
                  "type": "ACT",
                  "unit": null,
                  "unitPriceHtCents": 3850,
                  "unitPriceTtcCents": 4620,
                  "vatRatePercent": 20
                }
              ],
              "number": "FAC-000116",
              "paidCents": 34258,
              "patientIds": [
                "fafa4efe-a7b8-4ac5-b868-31199cc88f5d"
              ],
              "replacedByInvoiceId": null,
              "replacesInvoiceId": null,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "status": "PAID",
              "subtotalHtCents": 28549,
              "totalHtCents": 28549,
              "totalTtcCents": 34258,
              "totalVatCents": 5709,
              "updatedAt": "2026-10-15T01:55:35.162754Z",
              "vatBreakdown": [
                {
                  "baseHtCents": 28549,
                  "ratePercent": 20,
                  "vatCents": 5709
                }
              ]
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/invoices/{id}/pdf": {
      "get": {
        "tags": [
          "Facturation"
        ],
        "summary": "Télécharger le PDF d'une facture émise",
        "description": "Rend le PDF officiel de la facture, le même que celui remis au client, avec l'en-tête du site émetteur. Même périmètre que `GET /invoices/{id}`. Le fichier est servi avec `Content-Disposition: inline; filename=\"facture-<id>.pdf\"`.",
        "operationId": "GET_api_partner_v1_invoices_id_pdf",
        "security": [
          {
            "partner": [
              "billing:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "billing:read"
          ]
        },
        "x-treats-controller": "PartnerInvoiceController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la facture.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Le PDF de la facture.",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "INVOICE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVOICE_NOT_FOUND",
                "description": "Aucune facture émise de ce périmètre ne porte cet identifiant."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/invoices/e6303709-375d-4067-af6a-360bb484be7e/pdf"
          },
          "response": {
            "contentType": "application/pdf",
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/lab/orders": {
      "get": {
        "tags": [
          "Laboratoire"
        ],
        "summary": "Lister les commandes d'analyses",
        "description": "Rend les commandes d'analyses adressées aux laboratoires que représente l'application, sur les sites ouverts par l'organisation (ou sur le seul site demandé). Tri par date de modification croissante puis identifiant ; une commande modifiée apparaît dans la liste au plus 60 secondes après, alors que `GET /lab/orders/{id}` la rend tout de suite. Pour suivre les nouvelles commandes, lire `status=PENDING` avec `updated_since`. La liste ne rend ni les résultats ni les commandes adressées à un autre laboratoire.",
        "operationId": "GET_api_partner_v1_lab_orders",
        "security": [
          {
            "partner": [
              "lab:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "lab:write"
          ]
        },
        "x-treats-controller": "PartnerLabController",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Filtre de statut, insensible à la casse. Absent : tous les statuts.",
            "schema": {
              "type": "string",
              "enum": [
                "PENDING",
                "COMPLETED",
                "CANCELLED"
              ]
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Limite la liste à un site ouvert à l'application (sinon `404 SITE_NOT_FOUND`). Absent : tous les sites ouverts.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Ne rend que les commandes modifiées à cet instant ou après (inclusif).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Curseur opaque rendu par la page précédente (`nextCursor`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Taille de page, de 1 à 200 (au-delà, 200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page de commandes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LabOrder"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`status` hors de `PENDING`, `COMPLETED`, `CANCELLED`, `updated_since` illisible, `limit` non entier ou nul, `cursor` illisible."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "403": {
            "description": "PARTNER_PROVIDER_NOT_LINKED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PARTNER_PROVIDER_NOT_LINKED",
                "description": "L'application n'est rattachée à aucun laboratoire ni fournisseur d'imagerie : le rattachement se demande à Treats."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé n'est pas ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/lab/orders?status=PENDING&limit=200"
          },
          "response": {
            "body": {
              "items": [
                {
                  "createdAt": "2026-10-15T02:17:17.036757Z",
                  "id": "a56e73a0-5a76-4694-ae85-8ee1894da929",
                  "modality": "REFERENCE_LAB",
                  "notes": "À jeun",
                  "patient": {
                    "birthDate": "2019-04-12",
                    "breed": null,
                    "id": "e47568bf-3601-4acc-9808-8ff6a07f449b",
                    "microchipNumber": "250269203755048",
                    "name": "Ginger",
                    "owner": {
                      "firstName": "Marie",
                      "lastName": "Labimg"
                    },
                    "publicId": "pat_yntvjdr7t",
                    "sex": "FEMALE_SPAYED",
                    "species": "CAT"
                  },
                  "provider": "IDEXX",
                  "providerOrderId": "IDX-1001",
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "status": "PENDING",
                  "testCodes": [
                    "CBC",
                    "CHEM10"
                  ],
                  "updatedAt": "2026-10-15T02:17:17.040205Z",
                  "veterinarian": null
                },
                {
                  "createdAt": "2026-10-15T02:17:17.101579Z",
                  "id": "213aacb2-f653-4ff6-a42f-ad0c28c43f0d",
                  "modality": "REFERENCE_LAB",
                  "notes": null,
                  "patient": {
                    "birthDate": "2020-02-01",
                    "breed": null,
                    "id": "3fb86f61-3748-4396-9b37-50726f30a997",
                    "microchipNumber": "250269409088582",
                    "name": "Pistache",
                    "owner": {
                      "firstName": "Marie",
                      "lastName": "Labimg"
                    },
                    "publicId": "pat_4p36k9cyt",
                    "sex": "MALE",
                    "species": "DOG"
                  },
                  "provider": "IDEXX",
                  "providerOrderId": "IDX-1002",
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "status": "PENDING",
                  "testCodes": [
                    "T4"
                  ],
                  "updatedAt": "2026-10-15T02:17:17.102541Z",
                  "veterinarian": null
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/lab/orders/{id}": {
      "get": {
        "tags": [
          "Laboratoire"
        ],
        "summary": "Lire une commande d'analyses",
        "description": "Rend une commande d'analyses, avec l'animal concerné. La commande doit être de l'organisation, d'un site ouvert à l'application et adressée à l'un de ses laboratoires ; sinon la route répond 404, sans dire si elle existe ailleurs. Lecture immédiate, sans le délai des listes.",
        "operationId": "GET_api_partner_v1_lab_orders_id",
        "security": [
          {
            "partner": [
              "lab:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "lab:write"
          ]
        },
        "x-treats-controller": "PartnerLabController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la commande.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La commande.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LabOrder"
                }
              }
            }
          },
          "403": {
            "description": "PARTNER_PROVIDER_NOT_LINKED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PARTNER_PROVIDER_NOT_LINKED",
                "description": "L'application n'est rattachée à aucun laboratoire ni fournisseur d'imagerie."
              }
            ]
          },
          "404": {
            "description": "NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "NOT_FOUND",
                "description": "Commande inconnue, d'une autre organisation, d'un site non ouvert, adressée à un autre laboratoire, ou identifiant qui n'est pas un UUID."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/lab/orders/a56e73a0-5a76-4694-ae85-8ee1894da929"
          },
          "response": {
            "body": {
              "createdAt": "2026-10-15T02:17:17.036757Z",
              "id": "a56e73a0-5a76-4694-ae85-8ee1894da929",
              "modality": "REFERENCE_LAB",
              "notes": "À jeun",
              "patient": {
                "birthDate": "2019-04-12",
                "breed": null,
                "id": "e47568bf-3601-4acc-9808-8ff6a07f449b",
                "microchipNumber": "250269203755048",
                "name": "Ginger",
                "owner": {
                  "firstName": "Marie",
                  "lastName": "Labimg"
                },
                "publicId": "pat_yntvjdr7t",
                "sex": "FEMALE_SPAYED",
                "species": "CAT"
              },
              "provider": "IDEXX",
              "providerOrderId": "IDX-1001",
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "status": "COMPLETED",
              "testCodes": [
                "CBC",
                "CHEM10"
              ],
              "updatedAt": "2026-10-15T02:17:42.384695Z",
              "veterinarian": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/lab/results": {
      "post": {
        "tags": [
          "Laboratoire"
        ],
        "summary": "Déposer un résultat d'analyses",
        "description": "Enregistre un résultat structuré (lignes d'analyses) avec, au besoin, son compte rendu PDF en base64. Rattachement, du plus sûr au moins sûr : la commande (`orderId`), l'identifiant de l'animal (`patient.id` ou `patient.publicId`, de l'organisation et visible des sites ouverts), puis la puce (un seul animal de l'organisation). Tous les identifiants fournis doivent désigner le même animal ; jamais le nom. Sans rattachement sûr, le résultat est quand même enregistré et attend dans « Résultats à rattacher » du site (`matched: false`, `unmatchedReason`). Un résultat rattaché entre au dossier de l'animal (et le PDF parmi ses documents) ; rattaché par sa commande, celle-ci passe `COMPLETED` (`final: false` : reste `PENDING`).\n\nSite de réception : celui de la commande, sinon `site_id` ou `Treats-Site-Id`, sinon le seul site ouvert (`SITE_ID_REQUIRED` s'il y en a plusieurs). Réponse `201` pour un nouveau résultat. Renvoyer le même `externalResultId` du même laboratoire dans la même organisation, avec une autre `Idempotency-Key`, est une republication (`200`, même `id`) : les lignes sont toutes remplacées par celles envoyées, le PDF est remplacé s'il est fourni et gardé sinon, `collectedAt` et `veterinarian` sont mis à jour s'ils sont fournis. Le rattachement du premier envoi, `resultType` et `modality` sont conservés : `orderId` et `patient` sont contrôlés mais ne changent plus rien. La même `Idempotency-Key` rejoue la première réponse sans rien réécrire. Le même identifiant venu d'un autre laboratoire ou dans une autre organisation est un résultat distinct (`201`).",
        "operationId": "POST_api_partner_v1_lab_results",
        "security": [
          {
            "partner": [
              "lab:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "lab:write"
          ]
        },
        "x-treats-controller": "PartnerLabController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Site de réception d'un résultat sans `orderId`, s'il doit être précisé (ignoré avec une commande).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LabResultCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LabResult"
                }
              }
            },
            "description": "Résultat republié (même `externalResultId`, même laboratoire, même organisation) : même `id`, lignes remplacées."
          },
          "201": {
            "description": "Résultat créé, relu avec ses lignes et son rattachement.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LabResult"
                }
              }
            }
          },
          "400": {
            "description": "BODY_REQUIRED · INVALID_FIELD · PROVIDER_REQUIRED · INVALID_SITE_ID · SITE_ID_REQUIRED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "BODY_REQUIRED",
                "description": "Corps absent."
              },
              {
                "code": "INVALID_FIELD",
                "description": "Champ inconnu (y compris dans `patient`), `externalResultId` absent ou trop long, ni ligne ni PDF, plus de 500 lignes, ligne sans `analyte` ou sans valeur, nombre envoyé en texte ou à plus de 6 décimales, `low` supérieure à `high`, `flag` ou `resultType` inconnu, puce ou UUID mal formé, PDF illisible, trop gros ou qui n'est pas un PDF, `provider` inconnu, non représenté par l'application ou différent de celui de la commande. Un détail par champ."
              },
              {
                "code": "PROVIDER_REQUIRED",
                "description": "L'application représente plusieurs fournisseurs et ni `provider` ni `orderId` ne disent lequel."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              },
              {
                "code": "SITE_ID_REQUIRED",
                "description": "Sans `orderId`, plusieurs sites ouverts et aucun précisé. Exigé aussi pour une republication sans `orderId`, même si le résultat garde son site d'origine."
              }
            ]
          },
          "403": {
            "description": "PARTNER_PROVIDER_NOT_LINKED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PARTNER_PROVIDER_NOT_LINKED",
                "description": "L'application n'est rattachée à aucun laboratoire ni fournisseur d'imagerie."
              }
            ]
          },
          "404": {
            "description": "LAB_ORDER_NOT_FOUND · SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "LAB_ORDER_NOT_FOUND",
                "description": "`orderId` inconnu, d'une autre organisation, d'un site non ouvert ou adressé à un autre laboratoire."
              },
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé n'est pas ouvert à l'application."
              }
            ]
          },
          "409": {
            "description": "EXTERNAL_RESULT_ID_CONFLICT",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "EXTERNAL_RESULT_ID_CONFLICT",
                "description": "Cet `externalResultId` est déjà porté par un résultat du même laboratoire reçu sur un site que la connexion ne couvre pas (rien n'est écrit : employer un autre identifiant), ou il est en cours d'enregistrement par un autre appel simultané (renvoyer dans quelques secondes : ce sera une republication)."
              }
            ]
          }
        },
        "x-treats-webhooks": [
          "lab.result.received"
        ],
        "x-treats-example": {
          "request": {
            "body": {
              "collectedAt": "2026-10-14T07:30:00Z",
              "externalResultId": "DX-1",
              "lines": [
                {
                  "analyte": "HCT",
                  "category": "Hématologie",
                  "flag": "NORMAL",
                  "high": 45,
                  "low": 30,
                  "numericValue": 38.5,
                  "referenceRange": "30 - 45",
                  "testCode": "CBC",
                  "testName": "Numération",
                  "unit": "%",
                  "value": "38.5"
                },
                {
                  "analyte": "GLU",
                  "category": "Biochimie",
                  "flag": "HIGH",
                  "high": 7.8,
                  "low": 3.9,
                  "notes": "Contrôler à jeun",
                  "numericValue": 12.31,
                  "testCode": "CHEM10",
                  "unit": "mmol/L",
                  "value": "12.31"
                }
              ],
              "orderId": "a56e73a0-5a76-4694-ae85-8ee1894da929",
              "resultType": "BLOOD_PANEL",
              "veterinarian": "Dr Biologiste"
            },
            "method": "POST",
            "url": "/api/partner/v1/lab/results"
          },
          "response": {
            "body": {
              "collectedAt": "2026-10-14T07:30:00Z",
              "createdAt": "2026-10-15T02:17:42.381267Z",
              "externalResultId": "DX-1",
              "id": "03c303b7-4d68-420e-aa53-4519875d1ace",
              "lines": [
                {
                  "analyte": "HCT",
                  "category": "Hématologie",
                  "flag": "NORMAL",
                  "high": 45,
                  "low": 30,
                  "notes": null,
                  "numericValue": 38.5,
                  "referenceRange": "30 - 45",
                  "testCode": "CBC",
                  "testName": "Numération",
                  "unit": "%",
                  "value": "38.5"
                },
                {
                  "analyte": "GLU",
                  "category": "Biochimie",
                  "flag": "HIGH",
                  "high": 7.8,
                  "low": 3.9,
                  "notes": "Contrôler à jeun",
                  "numericValue": 12.31,
                  "referenceRange": null,
                  "testCode": "CHEM10",
                  "testName": null,
                  "unit": "mmol/L",
                  "value": "12.31"
                }
              ],
              "matched": true,
              "matchedBy": "ORDER",
              "modality": "REFERENCE_LAB",
              "orderId": "a56e73a0-5a76-4694-ae85-8ee1894da929",
              "patientId": "e47568bf-3601-4acc-9808-8ff6a07f449b",
              "patientPublicId": "pat_yntvjdr7t",
              "pdfReceived": false,
              "provider": "IDEXX",
              "resultType": "BLOOD_PANEL",
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "unmatchedReason": null
            },
            "status": 201
          }
        }
      }
    },
    "/api/partner/v1/lab/results/{id}": {
      "get": {
        "tags": [
          "Laboratoire"
        ],
        "summary": "Lire un résultat d'analyses",
        "description": "Rend un résultat avec toutes ses lignes et son état de rattachement, par exemple pour savoir si l'équipe de la clinique a rattaché un résultat resté en attente (`matched` passe à `true`, `patientId` est rempli). Le résultat doit être de l'organisation, reçu sur un site ouvert à l'application et venu de l'un de ses laboratoires, quel que soit le canal par lequel Treats l'a reçu ; sinon 404.",
        "operationId": "GET_api_partner_v1_lab_results_id",
        "security": [
          {
            "partner": [
              "lab:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "lab:write"
          ]
        },
        "x-treats-controller": "PartnerLabController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant Treats du résultat (`id` rendu par `POST /lab/results`).",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Le résultat.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LabResult"
                }
              }
            }
          },
          "403": {
            "description": "PARTNER_PROVIDER_NOT_LINKED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PARTNER_PROVIDER_NOT_LINKED",
                "description": "L'application n'est rattachée à aucun laboratoire ni fournisseur d'imagerie."
              }
            ]
          },
          "404": {
            "description": "NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "NOT_FOUND",
                "description": "Résultat inconnu, d'une autre organisation, reçu sur un site non ouvert, venu d'un autre laboratoire, ou identifiant qui n'est pas un UUID."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/lab/results/03c303b7-4d68-420e-aa53-4519875d1ace"
          },
          "response": {
            "body": {
              "collectedAt": "2026-10-14T07:30:00Z",
              "createdAt": "2026-10-15T02:17:42.381267Z",
              "externalResultId": "DX-1",
              "id": "03c303b7-4d68-420e-aa53-4519875d1ace",
              "lines": [
                {
                  "analyte": "HCT",
                  "category": "Hématologie",
                  "flag": "NORMAL",
                  "high": 45,
                  "low": 30,
                  "notes": null,
                  "numericValue": 38.5,
                  "referenceRange": "30 - 45",
                  "testCode": "CBC",
                  "testName": "Numération",
                  "unit": "%",
                  "value": "38.5"
                },
                {
                  "analyte": "GLU",
                  "category": "Biochimie",
                  "flag": "HIGH",
                  "high": 7.8,
                  "low": 3.9,
                  "notes": "Contrôler à jeun",
                  "numericValue": 12.31,
                  "referenceRange": null,
                  "testCode": "CHEM10",
                  "testName": null,
                  "unit": "mmol/L",
                  "value": "12.31"
                }
              ],
              "matched": true,
              "matchedBy": "ORDER",
              "modality": "REFERENCE_LAB",
              "orderId": "a56e73a0-5a76-4694-ae85-8ee1894da929",
              "patientId": "e47568bf-3601-4acc-9808-8ff6a07f449b",
              "patientPublicId": "pat_yntvjdr7t",
              "pdfReceived": false,
              "provider": "IDEXX",
              "resultType": "BLOOD_PANEL",
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "unmatchedReason": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/lab/results/{id}/pdf": {
      "post": {
        "tags": [
          "Laboratoire"
        ],
        "summary": "Déposer le compte rendu PDF d'un résultat",
        "description": "Joint un compte rendu PDF à un résultat déjà reçu, en `multipart/form-data` (champ `file`), quand il n'a pas été envoyé en base64 avec le résultat. Il remplace le précédent. Résultat rattaché : le PDF entre parmi les documents de l'animal. Résultat en attente : le fichier est gardé et entre au dossier quand l'équipe rattache le résultat. Mêmes conditions d'accès que `GET /lab/results/{id}`. Ni les lignes ni le rattachement ne changent.",
        "operationId": "POST_api_partner_v1_lab_results_id_pdf",
        "security": [
          {
            "partner": [
              "lab:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "lab:write"
          ]
        },
        "x-treats-controller": "PartnerLabController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant Treats du résultat.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Le compte rendu, déclaré `application/pdf`, 20 Mo au plus ; son contenu doit commencer par la signature `%PDF`."
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Le résultat relu (`pdfReceived: true`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LabResult"
                }
              }
            }
          },
          "400": {
            "description": "MULTIPART_REQUIRED · MULTIPART_INVALID · NO_FILE · FILE_TOO_LARGE · UNSUPPORTED_FILE_TYPE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "MULTIPART_REQUIRED",
                "description": "Corps qui n'est pas en `multipart/form-data`."
              },
              {
                "code": "MULTIPART_INVALID",
                "description": "Envoi multipart illisible."
              },
              {
                "code": "NO_FILE",
                "description": "Aucun champ `file`, ou fichier illisible."
              },
              {
                "code": "FILE_TOO_LARGE",
                "description": "PDF de plus de 20 Mo (ou envoi trop volumineux)."
              },
              {
                "code": "UNSUPPORTED_FILE_TYPE",
                "description": "Fichier non déclaré `application/pdf`, ou dont le contenu n'est pas un PDF."
              }
            ]
          },
          "403": {
            "description": "PARTNER_PROVIDER_NOT_LINKED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PARTNER_PROVIDER_NOT_LINKED",
                "description": "L'application n'est rattachée à aucun laboratoire ni fournisseur d'imagerie."
              }
            ]
          },
          "404": {
            "description": "NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "NOT_FOUND",
                "description": "Résultat inconnu, d'une autre organisation, reçu sur un site non ouvert, venu d'un autre laboratoire, ou identifiant qui n'est pas un UUID."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "form": {
              "file": "<fichier>"
            },
            "method": "POST",
            "url": "/api/partner/v1/lab/results/03c303b7-4d68-420e-aa53-4519875d1ace/pdf"
          },
          "response": {
            "body": {
              "collectedAt": "2026-10-14T07:30:00Z",
              "createdAt": "2026-10-15T02:17:42.381267Z",
              "externalResultId": "DX-1",
              "id": "03c303b7-4d68-420e-aa53-4519875d1ace",
              "lines": [
                {
                  "analyte": "GLU",
                  "category": "Biochimie",
                  "flag": "NORMAL",
                  "high": 7.8,
                  "low": 3.9,
                  "notes": null,
                  "numericValue": 6.1,
                  "referenceRange": null,
                  "testCode": "CHEM10",
                  "testName": null,
                  "unit": "mmol/L",
                  "value": "6.1"
                }
              ],
              "matched": true,
              "matchedBy": "ORDER",
              "modality": "REFERENCE_LAB",
              "orderId": "a56e73a0-5a76-4694-ae85-8ee1894da929",
              "patientId": "e47568bf-3601-4acc-9808-8ff6a07f449b",
              "patientPublicId": "pat_yntvjdr7t",
              "pdfReceived": true,
              "provider": "IDEXX",
              "resultType": "BLOOD_PANEL",
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "unmatchedReason": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/me": {
      "get": {
        "tags": [
          "Accès"
        ],
        "summary": "Lire ce que le jeton ouvre",
        "description": "Rend l'application, l'organisation, les sites ouverts et les droits effectifs du jeton. Premier appel conseillé après l'obtention d'un jeton : il donne les identifiants de sites à passer aux écritures quand l'organisation en a ouvert plusieurs. Aucun droit particulier n'est exigé.",
        "operationId": "GET_api_partner_v1_me",
        "security": [
          {
            "partner": []
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": []
        },
        "x-treats-controller": "PartnerMeController",
        "responses": {
          "200": {
            "description": "L'application, l'organisation, les sites et les droits.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "description": "INVALID_TOKEN",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_TOKEN",
                "description": "Jeton inconnu, expiré, ou dont l'autorisation a été retirée (`UNAUTHORIZED` : jeton absent)."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/me"
          },
          "response": {
            "body": {
              "app": {
                "company": "Éditeur Exemple",
                "environment": "LIVE",
                "id": "61997f5c-e021-4d76-91ec-23e393498b9a",
                "name": "Correctifs"
              },
              "grant": {
                "allSites": true,
                "grantedAt": "2026-10-15T02:13:51.728686Z",
                "id": "a06e9f1a-65ce-496a-8bd1-48e80f3f0fdd"
              },
              "organization": {
                "id": "9f825730-94a5-46bd-b1aa-e0a407441d31",
                "name": "Groupe Vétérinaire Exemple"
              },
              "scopes": [
                "org:read",
                "agenda:read"
              ],
              "sites": [
                {
                  "id": "61b796e2-921f-428b-b9aa-30c684b4ed91",
                  "name": "Clinique des Tilleuls Centre"
                },
                {
                  "id": "6b4e48ea-0112-4957-a834-121c1d272492",
                  "name": "Clinique des Tilleuls Nord"
                }
              ]
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/module-definitions": {
      "get": {
        "tags": [
          "Consultations"
        ],
        "summary": "Lister les modules de consultation",
        "description": "Rend les modules de consultation proposés par un site, un par `slug`, en une seule page (`nextCursor` vaut toujours `null`). Pour les cinq modules écrivables (`writable: true`), `fields` publie le contrat de leur `data` : champs, types, bornes et valeurs permises, les mêmes que le formulaire de la clinique. Les autres sont listés en lecture seule, `fields` vide. Sans `site_id`, le premier site ouvert à l'application.",
        "operationId": "GET_api_partner_v1_module_definitions",
        "security": [
          {
            "partner": [
              "consultations:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "consultations:read"
          ]
        },
        "x-treats-controller": "PartnerModuleDefinitionsController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Site dont lire les modules. Le paramètre l'emporte sur l'en-tête.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les modules du site.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ModuleDefinition"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un site ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/module-definitions"
          },
          "response": {
            "body": {
              "items": [
                {
                  "category": "history",
                  "fields": [
                    {
                      "description": "Ce qui amène l'animal, dans les mots du propriétaire.",
                      "items": null,
                      "label": "Motif de consultation",
                      "max": null,
                      "maxLength": 20000,
                      "min": null,
                      "name": "reason",
                      "properties": null,
                      "teamOnly": false,
                      "type": "TEXT",
                      "unit": null,
                      "values": null
                    },
                    {
                      "description": null,
                      "items": null,
                      "label": "Début des symptômes",
                      "max": null,
                      "maxLength": null,
                      "min": null,
                      "name": "symptomOnset",
                      "properties": null,
                      "teamOnly": false,
                      "type": "ENUM",
                      "unit": null,
                      "values": [
                        {
                          "label": "< 24h",
                          "value": "LESS_24H"
                        },
                        {
                          "label": "1-3 jours",
                          "value": "ONE_THREE_DAYS"
                        },
                        {
                          "label": "> 3 jours",
                          "value": "MORE_THREE_DAYS"
                        }
                      ]
                    },
                    {
                      "description": null,
                      "items": null,
                      "label": "Évolution",
                      "max": null,
                      "maxLength": null,
                      "min": null,
                      "name": "evolution",
                      "properties": null,
                      "teamOnly": false,
                      "type": "ENUM",
                      "unit": null,
                      "values": [
                        {
                          "label": "Amélioration",
                          "value": "IMPROVEMENT"
                        },
                        {
                          "label": "Stable",
                          "value": "STABLE"
                        },
                        {
                          "label": "Aggravation",
                          "value": "WORSENING"
                        }
                      ]
                    }
                  ],
                  "name": "Anamnèse",
                  "slug": "anamnese",
                  "writable": true
                },
                {
                  "category": "history",
                  "fields": [
                    {
                      "description": null,
                      "items": null,
                      "label": "Antécédents médicaux",
                      "max": null,
                      "maxLength": 20000,
                      "min": null,
                      "name": "medicalHistory",
                      "properties": null,
                      "teamOnly": false,
                      "type": "TEXT",
                      "unit": null,
                      "values": null
                    },
                    {
                      "description": null,
                      "items": null,
                      "label": "Chirurgies antérieures",
                      "max": null,
                      "maxLength": 20000,
                      "min": null,
                      "name": "previousSurgeries",
                      "properties": null,
                      "teamOnly": false,
                      "type": "TEXT",
                      "unit": null,
                      "values": null
                    },
                    {
                      "description": null,
                      "items": null,
                      "label": "Traitement en cours",
                      "max": null,
                      "maxLength": 20000,
                      "min": null,
                      "name": "currentTreatment",
                      "properties": null,
                      "teamOnly": false,
                      "type": "TEXT",
                      "unit": null,
                      "values": null
                    }
                  ],
                  "name": "Commémoratifs",
                  "slug": "commemoratifs",
                  "writable": true
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/module-definitions/{slug}": {
      "get": {
        "tags": [
          "Consultations"
        ],
        "summary": "Lire le contrat d'un module",
        "description": "Rend un module de consultation du site et, s'il est écrivable, le contrat de son `data` : c'est le corps attendu par `PUT /consultations/{id}/modules/{slug}`. Sans `site_id`, le premier site ouvert à l'application.",
        "operationId": "GET_api_partner_v1_module_definitions_slug",
        "security": [
          {
            "partner": [
              "consultations:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "consultations:read"
          ]
        },
        "x-treats-controller": "PartnerModuleDefinitionsController",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "Identifiant du module (`anamnese`, `examen-general`, `soins`…).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "anamnese"
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Site dont lire le module. Le paramètre l'emporte sur l'en-tête.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Le module et son contrat.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModuleDefinition"
                }
              }
            }
          },
          "404": {
            "description": "MODULE_NOT_FOUND · SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "MODULE_NOT_FOUND",
                "description": "Aucun module de ce nom sur le site."
              },
              {
                "code": "SITE_NOT_FOUND",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un site ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/module-definitions/examen-general"
          },
          "response": {
            "body": {
              "category": "examination",
              "fields": [
                {
                  "description": null,
                  "items": null,
                  "label": "Examen sans particularité (RAS)",
                  "max": null,
                  "maxLength": null,
                  "min": null,
                  "name": "normal",
                  "properties": null,
                  "teamOnly": false,
                  "type": "BOOLEAN",
                  "unit": null,
                  "values": null
                },
                {
                  "description": null,
                  "items": null,
                  "label": "État général",
                  "max": null,
                  "maxLength": null,
                  "min": null,
                  "name": "generalState",
                  "properties": null,
                  "teamOnly": false,
                  "type": "ENUM",
                  "unit": null,
                  "values": [
                    {
                      "label": "Bon",
                      "value": "GOOD"
                    },
                    {
                      "label": "Correct",
                      "value": "CORRECT"
                    },
                    {
                      "label": "Altéré",
                      "value": "ALTERED"
                    }
                  ]
                }
              ],
              "name": "Examen général",
              "slug": "examen-general",
              "writable": true
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/oauth/token": {
      "post": {
        "tags": [
          "Accès"
        ],
        "summary": "Obtenir un jeton d'accès",
        "description": "Échange les identifiants de l'application contre un jeton pour UNE organisation (OAuth 2, `client_credentials`). Les identifiants passent dans le corps (`client_id`, `client_secret`) ou en HTTP Basic ; le corps est un formulaire (`application/x-www-form-urlencoded`, la norme OAuth) ou du JSON. L'organisation doit avoir connecté l'application, sinon `unauthorized_client`.\n\nLe jeton est opaque, vaut 60 minutes et porte les droits de l'autorisation au moment de l'appel. Il est relu à chaque appel : une autorisation retirée, un droit retiré ou une application suspendue mordent tout de suite. Demandes de jeton plafonnées à 60 par minute et par adresse IP, et 30 par minute et par application (`429`, `Retry-After`). Seule route de l'API dont les erreurs suivent le format OAuth (`TokenError`) et non l'enveloppe commune.",
        "operationId": "POST_api_partner_v1_oauth_token",
        "security": [],
        "x-treats-access": {
          "kind": "public",
          "scope": "system",
          "reason": "partner-oauth-token"
        },
        "x-treats-controller": "PartnerTokenController",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "enum": [
                      "client_credentials"
                    ],
                    "description": "Toujours `client_credentials`."
                  },
                  "client_id": {
                    "type": "string",
                    "description": "Identifiant de l'application (ou en HTTP Basic)."
                  },
                  "client_secret": {
                    "type": "string",
                    "description": "Secret de l'application (ou en HTTP Basic). Rendu une seule fois par Treats ; à garder côté serveur."
                  },
                  "organization_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Organisation pour laquelle le jeton est demandé."
                  }
                },
                "required": [
                  "grant_type",
                  "organization_id"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Jeton émis. En-têtes `Cache-Control: no-store`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string",
                      "description": "Jeton à passer en `Authorization: Bearer …` sur chaque appel."
                    },
                    "token_type": {
                      "type": "string",
                      "enum": [
                        "Bearer"
                      ],
                      "description": "Toujours `Bearer`."
                    },
                    "expires_in": {
                      "type": "integer",
                      "description": "Durée de validité en secondes (3600)."
                    },
                    "scope": {
                      "type": "string",
                      "description": "Droits effectifs, séparés par des espaces."
                    },
                    "organization_id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "Organisation du jeton."
                    }
                  },
                  "required": [
                    "access_token",
                    "token_type",
                    "expires_in",
                    "scope",
                    "organization_id"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "invalid_request · unsupported_grant_type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "invalid_request",
                "description": "`grant_type`, `client_id`, `client_secret` ou `organization_id` absent ou illisible (`treats_code` : `INVALID_REQUEST`, `INVALID_ORGANIZATION_ID`, `INVALID_BODY`)."
              },
              {
                "code": "unsupported_grant_type",
                "description": "Autre `grant_type` que `client_credentials`."
              }
            ]
          },
          "401": {
            "description": "invalid_client",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "invalid_client",
                "description": "Identifiant ou secret faux, secret retiré, ou application suspendue (`treats_code` : `INVALID_CLIENT`, `PARTNER_APP_SUSPENDED`)."
              }
            ]
          },
          "403": {
            "description": "unauthorized_client",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "unauthorized_client",
                "description": "L'organisation n'a pas connecté l'application, ou l'a déconnectée (`treats_code` : `NO_GRANT`)."
              }
            ]
          },
          "429": {
            "description": "slow_down",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "slow_down",
                "description": "Trop de demandes de jeton (`treats_code` : `RATE_LIMITED`). Réutiliser le jeton : il vaut 60 minutes."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "form": {
              "client_id": "papp_WrrBzNxkv7usmt2z1v3HTNSWXphm5uCzfl3d1z4c6Sw",
              "client_secret": "…",
              "grant_type": "client_credentials",
              "organization_id": "9b9ff4bd-d49e-4b44-a41a-82136caaa3f8"
            },
            "method": "POST",
            "url": "/api/partner/v1/oauth/token"
          },
          "response": {
            "body": {
              "access_token": "…",
              "expires_in": 3600,
              "organization_id": "9b9ff4bd-d49e-4b44-a41a-82136caaa3f8",
              "scope": "org:read agenda:read agenda:write clients:read clients:write patients:read patients:write consultations:read consultations:write billing:read documents:read documents:write lab:write imaging:write",
              "token_type": "Bearer"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/patients": {
      "get": {
        "tags": [
          "Animaux"
        ],
        "summary": "Lister les animaux",
        "description": "Rend les animaux des clients que vous pouvez voir, animaux décédés compris (`deceased: true`). Tri par `updatedAt` puis `id`, croissant : c'est la liste de synchronisation, à parcourir par `cursor` et à reprendre par `updated_since`. Une fiche créée ou modifiée n'y apparaît qu'environ 60 secondes après l'écriture ; sa lecture par `GET /patients/{id}` est immédiate, de même qu'une recherche par `microchip`.\n\nUn `client_id` hors de votre périmètre (autre organisation, site non ouvert, inconnu) répond `404 CLIENT_NOT_FOUND`, jamais une liste vide. Exige `patients:read`.",
        "operationId": "GET_api_partner_v1_patients",
        "security": [
          {
            "partner": [
              "patients:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "patients:read"
          ]
        },
        "x-treats-controller": "PartnerPatientsController",
        "parameters": [
          {
            "name": "client_id",
            "in": "query",
            "description": "Seulement les animaux de ce client.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "microchip",
            "in": "query",
            "description": "Numéro de puce exact, 15 chiffres. Rend l'animal tout juste créé, sans attendre.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Ne rend que les animaux dont `updatedAt` est postérieur ou égal à cet instant (ISO 8601, années 1900 à 9999). Pour une synchronisation incrémentale, repartez du plus grand `updatedAt` vu moins 5 minutes et dédoublonnez par `id`.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Curseur opaque rendu par la page précédente (`nextCursor`), à repasser tel quel avec les mêmes filtres. Absent : première page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Nombre d'animaux par page, de 1 à 200. Au-delà de 200, ramené à 200.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page d'animaux.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Patient"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`client_id` qui n'est pas un UUID, `microchip` qui n'a pas 15 chiffres, `updated_since` mal formé ou hors des années 1900 à 9999, `limit` non entier ou inférieur à 1, `cursor` illisible."
              }
            ]
          },
          "404": {
            "description": "CLIENT_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CLIENT_NOT_FOUND",
                "description": "`client_id` inconnu, d'une autre organisation, ou d'un client né sur un site qui ne vous est pas ouvert."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/patients?client_id=b8381184-1f53-47d5-9238-10cad7d5f7d9"
          },
          "response": {
            "body": {
              "items": [
                {
                  "birthDate": "2020-06-15",
                  "birthDatePrecision": "DAY",
                  "breed": "Labrador Retriever",
                  "breedId": "b6004c5f-5da0-4e8f-a1dd-053751b4813e",
                  "clientId": "b8381184-1f53-47d5-9238-10cad7d5f7d9",
                  "coatColor": "Fauve",
                  "createdAt": "2026-10-15T02:03:13.995251Z",
                  "deceased": false,
                  "deceasedDate": null,
                  "id": "8166f358-8478-4c44-bac2-43914d4cbc60",
                  "microchipNumber": "250028378012832",
                  "name": "Rex",
                  "passportNumber": null,
                  "publicId": "pat_vhdrb7ppn",
                  "sex": "MALE",
                  "species": "DOG",
                  "sterilized": false,
                  "tattooNumber": null,
                  "updatedAt": "2026-10-15T02:03:13.996015Z"
                },
                {
                  "birthDate": "2023-01-01",
                  "birthDatePrecision": "YEAR",
                  "breed": null,
                  "breedId": null,
                  "clientId": "b8381184-1f53-47d5-9238-10cad7d5f7d9",
                  "coatColor": null,
                  "createdAt": "2026-10-15T02:03:14.138769Z",
                  "deceased": false,
                  "deceasedDate": null,
                  "id": "8ae5e2a8-57ef-4f40-a774-97d31bad26c1",
                  "microchipNumber": null,
                  "name": "Mimi",
                  "passportNumber": null,
                  "publicId": "pat_msxsz85v4",
                  "sex": null,
                  "species": "CAT",
                  "sterilized": null,
                  "tattooNumber": null,
                  "updatedAt": "2026-10-15T02:03:14.139469Z"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      },
      "post": {
        "tags": [
          "Animaux"
        ],
        "summary": "Créer un animal",
        "description": "Crée un animal pour un client que vous pouvez voir. Les règles de l'organisation s'appliquent : l'espèce doit être connue d'elle (`GET /species`), une race du référentiel doit appartenir à cette espèce, et une puce ne peut être portée que par un seul animal de l'organisation (une puce connue d'une autre organisation est acceptée). Exige `patients:write`.\n\nTous les champs refusés sont rendus en une seule réponse `400 INVALID_FIELD` ; tout champ hors contrat est refusé. Le site de l'écriture est `site_id` s'il est donné, sinon le site d'origine du propriétaire s'il vous est ouvert, sinon le seul site ouvert ; l'animal reste celui de l'organisation.",
        "operationId": "POST_api_partner_v1_patients",
        "security": [
          {
            "partner": [
              "patients:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "patients:write"
          ]
        },
        "x-treats-controller": "PartnerPatientsController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Site au nom duquel l'écriture est faite. Facultatif (voir la description).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "clientId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Propriétaire. Doit être un client que vous pouvez voir, sinon `404 CLIENT_NOT_FOUND`."
                  },
                  "name": {
                    "type": "string",
                    "description": "Nom de l'animal.",
                    "maxLength": 100
                  },
                  "species": {
                    "type": "string",
                    "description": "Code d'espèce de `GET /species`. Espèce inconnue de l'organisation : `400 INVALID_FIELD` sur `species`.",
                    "maxLength": 30
                  },
                  "breed": {
                    "type": "string",
                    "description": "Race en texte libre. Absente avec un `breedId` : le libellé de la race du référentiel est recopié.",
                    "maxLength": 100
                  },
                  "breedId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Race du référentiel. Inconnue de l'organisation ou d'une autre espèce : `400 INVALID_FIELD` sur `breedId`."
                  },
                  "sex": {
                    "type": "string",
                    "enum": [
                      "MALE",
                      "FEMALE",
                      "MALE_NEUTERED",
                      "FEMALE_SPAYED",
                      "UNKNOWN"
                    ],
                    "description": "Sexe (casse ignorée)."
                  },
                  "sterilized": {
                    "type": "boolean",
                    "description": "Animal stérilisé."
                  },
                  "birthDate": {
                    "type": "string",
                    "format": "date",
                    "description": "Date de naissance (`AAAA-MM-JJ`), passée ou du jour, pas avant 1900."
                  },
                  "birthDatePrecision": {
                    "type": "string",
                    "enum": [
                      "DAY",
                      "MONTH",
                      "YEAR"
                    ],
                    "description": "Précision de `birthDate`, seulement avec elle. `MONTH` : date ancrée au 1er du mois ; `YEAR` : au 1er janvier. Absente : date exacte (`DAY`)."
                  },
                  "microchipNumber": {
                    "type": "string",
                    "description": "Numéro de puce : exactement 15 chiffres (ISO 11784/11785). Déjà porté par un animal de l'organisation : `409 MICROCHIP_ALREADY_REGISTERED`."
                  },
                  "tattooNumber": {
                    "type": "string",
                    "description": "Numéro de tatouage.",
                    "maxLength": 50
                  },
                  "passportNumber": {
                    "type": "string",
                    "description": "Numéro de passeport.",
                    "maxLength": 50
                  },
                  "coatColor": {
                    "type": "string",
                    "description": "Robe (couleur du pelage).",
                    "maxLength": 100
                  }
                },
                "required": [
                  "clientId",
                  "name",
                  "species"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "L'animal créé.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Patient"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · SITE_ID_REQUIRED · INVALID_SITE_ID · INVALID_REQUEST",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`clientId`, `name` ou `species` absent ; `clientId` ou `breedId` qui n'est pas un UUID ; espèce inconnue de l'organisation (`species`) ; race inconnue ou d'une autre espèce (`breedId`) ; `birthDatePrecision` sans `birthDate` ; puce qui n'a pas 15 chiffres ; date future ou avant 1900 ; texte trop long ou contenant une balise HTML ; valeur d'enum inconnue ; champ hors contrat."
              },
              {
                "code": "SITE_ID_REQUIRED",
                "description": "Aucun `site_id`, le site d'origine du propriétaire ne vous est pas ouvert, et l'organisation vous a ouvert plusieurs sites."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              },
              {
                "code": "INVALID_REQUEST",
                "description": "Refus résiduel de Treats sur une donnée que les contrôles ci-dessus n'ont pas arrêtée (filet de sécurité)."
              }
            ]
          },
          "404": {
            "description": "CLIENT_NOT_FOUND · SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "CLIENT_NOT_FOUND",
                "description": "Propriétaire inconnu, d'une autre organisation, ou né sur un site qui ne vous est pas ouvert."
              },
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé n'est pas un site que l'organisation vous a ouvert."
              }
            ]
          },
          "409": {
            "description": "MICROCHIP_ALREADY_REGISTERED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "MICROCHIP_ALREADY_REGISTERED",
                "description": "Cette puce est déjà enregistrée pour un animal de l'organisation, y compris d'un client que vous ne voyez pas. La réponse ne dit ni quel animal, ni quel client, ni quel site."
              }
            ]
          }
        },
        "x-treats-webhooks": [
          "patient.created"
        ],
        "x-treats-example": {
          "request": {
            "body": {
              "birthDate": "2020-06-15",
              "breedId": "b6004c5f-5da0-4e8f-a1dd-053751b4813e",
              "clientId": "b8381184-1f53-47d5-9238-10cad7d5f7d9",
              "coatColor": "Fauve",
              "microchipNumber": "250028378012832",
              "name": "Rex",
              "sex": "MALE",
              "species": "DOG",
              "sterilized": false
            },
            "method": "POST",
            "url": "/api/partner/v1/patients"
          },
          "response": {
            "body": {
              "birthDate": "2020-06-15",
              "birthDatePrecision": "DAY",
              "breed": "Labrador Retriever",
              "breedId": "b6004c5f-5da0-4e8f-a1dd-053751b4813e",
              "clientId": "b8381184-1f53-47d5-9238-10cad7d5f7d9",
              "coatColor": "Fauve",
              "createdAt": "2026-10-15T02:03:13.995251Z",
              "deceased": false,
              "deceasedDate": null,
              "id": "8166f358-8478-4c44-bac2-43914d4cbc60",
              "microchipNumber": "250028378012832",
              "name": "Rex",
              "passportNumber": null,
              "publicId": "pat_vhdrb7ppn",
              "sex": "MALE",
              "species": "DOG",
              "sterilized": false,
              "tattooNumber": null,
              "updatedAt": "2026-10-15T02:03:13.995251Z"
            },
            "status": 201
          }
        }
      }
    },
    "/api/partner/v1/patients/{id}": {
      "get": {
        "tags": [
          "Animaux"
        ],
        "summary": "Lire un animal",
        "description": "Rend l'identité d'un animal dont vous voyez le propriétaire. Lecture immédiate, sans le délai de la liste. Un animal d'une autre organisation, d'un client né sur un site non ouvert, ou inconnu répond `404 PATIENT_NOT_FOUND`. Exige `patients:read`.",
        "operationId": "GET_api_partner_v1_patients_id",
        "security": [
          {
            "partner": [
              "patients:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "patients:read"
          ]
        },
        "x-treats-controller": "PartnerPatientsController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de l'animal (`id`, pas `publicId`).",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La fiche de l'animal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Patient"
                }
              }
            }
          },
          "404": {
            "description": "PATIENT_NOT_FOUND · NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PATIENT_NOT_FOUND",
                "description": "Animal inconnu, d'une autre organisation, ou dont le propriétaire est né sur un site qui ne vous est pas ouvert."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/patients/8166f358-8478-4c44-bac2-43914d4cbc60"
          },
          "response": {
            "body": {
              "birthDate": "2020-05-01",
              "birthDatePrecision": "MONTH",
              "breed": "Labrador Retriever",
              "breedId": "b6004c5f-5da0-4e8f-a1dd-053751b4813e",
              "clientId": "b8381184-1f53-47d5-9238-10cad7d5f7d9",
              "coatColor": null,
              "createdAt": "2026-10-15T02:03:13.995251Z",
              "deceased": false,
              "deceasedDate": null,
              "id": "8166f358-8478-4c44-bac2-43914d4cbc60",
              "microchipNumber": "250028378012832",
              "name": "Rex",
              "passportNumber": null,
              "publicId": "pat_vhdrb7ppn",
              "sex": "MALE_NEUTERED",
              "species": "DOG",
              "sterilized": true,
              "tattooNumber": null,
              "updatedAt": "2026-10-15T02:03:24.362676Z"
            },
            "status": 200
          }
        }
      },
      "patch": {
        "tags": [
          "Animaux"
        ],
        "summary": "Modifier l'identité d'un animal",
        "description": "Modifie les champs envoyés : champ absent = inchangé, champ à `null` = vidé. Se vident : `sex` (non renseigné), `birthDate` (date et précision effacées), `breed` (race effacée, texte et lien ; de même `breedId: null` envoyé sans `breed`), `microchipNumber`, `tattooNumber`, `passportNumber`, `coatColor`. Ne se vident pas (`400 INVALID_FIELD`) : `name`, `species`, `sterilized`, `birthDatePrecision`.\n\nRefusés, chacun sous son propre nom : `clientId` (transfert de propriétaire), `deceased` et `deceasedDate` (décès), qui restent à la clinique. Sans `breedId` dans le corps, changer `species`, ou envoyer un `breed` différent du libellé de la race liée, retire le lien à la race du référentiel. Une `birthDate` envoyée sans `birthDatePrecision` est une date exacte (`DAY`). Une puce déjà portée par un autre animal de l'organisation répond `409 MICROCHIP_ALREADY_REGISTERED`. Exige `patients:write`.",
        "operationId": "PATCH_api_partner_v1_patients_id",
        "security": [
          {
            "partner": [
              "patients:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "patients:write"
          ]
        },
        "x-treats-controller": "PartnerPatientsController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de l'animal.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Site au nom duquel l'écriture est faite. Facultatif : à défaut, le site d'origine du propriétaire s'il vous est ouvert, sinon le seul site ouvert.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Nom. Non vidable, ni à `null` ni à vide.",
                    "maxLength": 100
                  },
                  "species": {
                    "type": "string",
                    "description": "Code d'espèce de `GET /species`. Non vidable. Espèce inconnue de l'organisation : `400 INVALID_FIELD`.",
                    "maxLength": 30
                  },
                  "breed": {
                    "type": "string",
                    "description": "Race en texte. `null` efface la race (texte et lien).",
                    "nullable": true,
                    "maxLength": 100
                  },
                  "breedId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Race du référentiel, de l'espèce de l'animal. `null` sans `breed` : race effacée.",
                    "nullable": true
                  },
                  "sex": {
                    "type": "string",
                    "enum": [
                      "MALE",
                      "FEMALE",
                      "MALE_NEUTERED",
                      "FEMALE_SPAYED",
                      "UNKNOWN"
                    ],
                    "description": "Sexe. `null` : non renseigné.",
                    "nullable": true
                  },
                  "sterilized": {
                    "type": "boolean",
                    "description": "Animal stérilisé. Non vidable."
                  },
                  "birthDate": {
                    "type": "string",
                    "format": "date",
                    "description": "Date de naissance, passée ou du jour, pas avant 1900. `null` efface la date et sa précision.",
                    "nullable": true
                  },
                  "birthDatePrecision": {
                    "type": "string",
                    "enum": [
                      "DAY",
                      "MONTH",
                      "YEAR"
                    ],
                    "description": "Précision, seulement envoyée avec `birthDate`. Non vidable."
                  },
                  "microchipNumber": {
                    "type": "string",
                    "description": "Numéro de puce, 15 chiffres. `null` le vide.",
                    "nullable": true
                  },
                  "tattooNumber": {
                    "type": "string",
                    "description": "Numéro de tatouage. `null` le vide.",
                    "nullable": true,
                    "maxLength": 50
                  },
                  "passportNumber": {
                    "type": "string",
                    "description": "Numéro de passeport. `null` le vide.",
                    "nullable": true,
                    "maxLength": 50
                  },
                  "coatColor": {
                    "type": "string",
                    "description": "Robe. `null` la vide.",
                    "nullable": true,
                    "maxLength": 100
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "La fiche modifiée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Patient"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · SITE_ID_REQUIRED · INVALID_SITE_ID · INVALID_REQUEST",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`clientId`, `deceased` ou `deceasedDate` envoyé ; champ non vidable à `null` (ou `name`, `species` vide) ; `birthDatePrecision` sans `birthDate` ; espèce inconnue de l'organisation (`species`) ; race inconnue ou d'une autre espèce (`breedId`) ; puce qui n'a pas 15 chiffres ; format, longueur ou enum refusés ; champ hors contrat."
              },
              {
                "code": "SITE_ID_REQUIRED",
                "description": "Aucun `site_id`, le site d'origine du propriétaire ne vous est pas ouvert, et l'organisation vous a ouvert plusieurs sites."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              },
              {
                "code": "INVALID_REQUEST",
                "description": "Refus résiduel de Treats sur une donnée que les contrôles ci-dessus n'ont pas arrêtée (filet de sécurité)."
              }
            ]
          },
          "404": {
            "description": "PATIENT_NOT_FOUND · NOT_FOUND · SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PATIENT_NOT_FOUND",
                "description": "Animal inconnu, d'une autre organisation, ou dont le propriétaire est né sur un site qui ne vous est pas ouvert."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              },
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé n'est pas un site que l'organisation vous a ouvert."
              }
            ]
          },
          "409": {
            "description": "MICROCHIP_ALREADY_REGISTERED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "MICROCHIP_ALREADY_REGISTERED",
                "description": "Cette puce est déjà enregistrée pour un autre animal de l'organisation. La réponse ne dit ni lequel, ni où."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "body": {
              "birthDate": "2020-05-01",
              "birthDatePrecision": "MONTH",
              "sex": "MALE_NEUTERED",
              "sterilized": true
            },
            "method": "PATCH",
            "url": "/api/partner/v1/patients/8166f358-8478-4c44-bac2-43914d4cbc60"
          },
          "response": {
            "body": {
              "birthDate": "2020-05-01",
              "birthDatePrecision": "MONTH",
              "breed": "Labrador Retriever",
              "breedId": "b6004c5f-5da0-4e8f-a1dd-053751b4813e",
              "clientId": "b8381184-1f53-47d5-9238-10cad7d5f7d9",
              "coatColor": null,
              "createdAt": "2026-10-15T02:03:13.995251Z",
              "deceased": false,
              "deceasedDate": null,
              "id": "8166f358-8478-4c44-bac2-43914d4cbc60",
              "microchipNumber": "250028378012832",
              "name": "Rex",
              "passportNumber": null,
              "publicId": "pat_vhdrb7ppn",
              "sex": "MALE_NEUTERED",
              "species": "DOG",
              "sterilized": true,
              "tattooNumber": null,
              "updatedAt": "2026-10-15T02:03:24.221059Z"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/patients/{id}/documents": {
      "get": {
        "tags": [
          "Documents"
        ],
        "summary": "Lister les documents d'un animal",
        "description": "Rend les documents du dossier de l'animal situés sur un site ouvert à l'application, en une seule page (`nextCursor` vaut toujours `null`). Le dossier d'un animal est commun aux sites de l'organisation : un document d'un site non ouvert n'est pas rendu. Métadonnées seulement : le contenu se télécharge par `GET /documents/{id}/download`.",
        "operationId": "GET_api_partner_v1_patients_id_documents",
        "security": [
          {
            "partner": [
              "documents:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "documents:read"
          ]
        },
        "x-treats-controller": "PartnerPatientRecordController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de l'animal.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les documents de l'animal.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Document"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "PATIENT_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PATIENT_NOT_FOUND",
                "description": "Animal inconnu, d'une autre organisation, ou dont le client n'est pas rattaché à un site ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/patients/7dd29725-ea42-4557-bc80-4b9165d4ef76/documents"
          },
          "response": {
            "body": {
              "items": [
                {
                  "category": "RESULTATS_LABO",
                  "consultationId": null,
                  "createdAt": "2026-10-15T02:06:09.218367Z",
                  "id": "dfa44859-81d6-4a8f-9145-c6e02a4ff4a1",
                  "patientId": "7dd29725-ea42-4557-bc80-4b9165d4ef76",
                  "signedAt": null,
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "status": "GENERATED",
                  "title": "Bilan",
                  "type": "UPLOAD",
                  "updatedAt": "2026-10-15T02:06:09.218367Z",
                  "validatedAt": null,
                  "visibility": "INTERNAL"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      },
      "post": {
        "tags": [
          "Documents"
        ],
        "summary": "Déposer un PDF dans le dossier d'un animal",
        "description": "Dépose un PDF (compte rendu de laboratoire, d'imagerie…) dans le dossier de l'animal, en `multipart/form-data`. Seul le PDF est accepté : type déclaré `application/pdf` ET contenu qui commence par la signature `%PDF-`. 20 Mo au plus par fichier ; un envoi plus gros est arrêté pendant la réception, même sans `Content-Length`. Le document naît de type `UPLOAD`, statut `GENERATED`, visibilité `INTERNAL` : la clinique décide de le montrer au propriétaire. Il est signé par le compte d'intégration de l'application.\n\nAvec `consultationId`, le document est rattaché à cette consultation et naît sur son site. Sans, il naît sur `site_id` (ou `Treats-Site-Id`), ou sur le seul site ouvert à l'application. `Idempotency-Key` est obligatoire ; un même envoi est reconnu à son type et à sa taille : utiliser une clé par dépôt, sans quoi deux fichiers différents de même taille sous la même clé rendraient le premier.",
        "operationId": "POST_api_partner_v1_patients_id_documents",
        "security": [
          {
            "partner": [
              "documents:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "documents:write"
          ]
        },
        "x-treats-controller": "PartnerPatientRecordController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de l'animal.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Site du document, sans `consultationId`. Ignoré avec `consultationId`. Le paramètre l'emporte sur l'en-tête.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Le PDF, partie déclarée `application/pdf`, 20 Mo au plus. Le nom du fichier envoyé (sans chemin, 150 caractères au plus) est gardé ; à défaut, `document.pdf`."
                  },
                  "title": {
                    "type": "string",
                    "description": "Titre du document, de 1 à 200 caractères. Absent : le nom du fichier.",
                    "maxLength": 200
                  },
                  "category": {
                    "type": "string",
                    "enum": [
                      "RESULTATS_LABO",
                      "IMAGERIE",
                      "AUTRE"
                    ],
                    "description": "Rubrique du dossier.",
                    "default": "AUTRE"
                  },
                  "consultationId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Consultation de ce même animal à laquelle rattacher le document, sur un site ouvert à l'application."
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Le document créé.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Document"
                }
              }
            }
          },
          "400": {
            "description": "MULTIPART_REQUIRED · MULTIPART_INVALID · FILE_REQUIRED · FILE_EMPTY · FILE_TOO_LARGE · FILE_UNREADABLE · FILE_TYPE_NOT_ALLOWED · INVALID_FIELD · CONSULTATION_OTHER_PATIENT · SITE_ID_REQUIRED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "MULTIPART_REQUIRED",
                "description": "Corps qui n'est pas en `multipart/form-data`."
              },
              {
                "code": "MULTIPART_INVALID",
                "description": "Envoi multipart illisible."
              },
              {
                "code": "FILE_REQUIRED",
                "description": "Partie `file` absente."
              },
              {
                "code": "FILE_EMPTY",
                "description": "Fichier vide."
              },
              {
                "code": "FILE_TOO_LARGE",
                "description": "Fichier de plus de 20 Mo (ou envoi total trop gros), détecté sur la taille annoncée ou pendant la réception."
              },
              {
                "code": "FILE_UNREADABLE",
                "description": "Fichier illisible à la réception."
              },
              {
                "code": "FILE_TYPE_NOT_ALLOWED",
                "description": "Partie non déclarée `application/pdf`, ou contenu qui n'est pas un PDF."
              },
              {
                "code": "INVALID_FIELD",
                "description": "`title` vide ou de plus de 200 caractères, `category` hors de `RESULTATS_LABO`/`IMAGERIE`/`AUTRE`, `consultationId` qui n'est pas un UUID."
              },
              {
                "code": "CONSULTATION_OTHER_PATIENT",
                "description": "`consultationId` est la consultation d'un autre animal."
              },
              {
                "code": "SITE_ID_REQUIRED",
                "description": "Sans `consultationId`, l'organisation a ouvert plusieurs sites et ni `site_id` ni `Treats-Site-Id` n'est donné."
              }
            ]
          },
          "404": {
            "description": "PATIENT_NOT_FOUND · CONSULTATION_NOT_FOUND · SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PATIENT_NOT_FOUND",
                "description": "Animal inconnu, d'une autre organisation, ou dont le client n'est pas rattaché à un site ouvert à l'application."
              },
              {
                "code": "CONSULTATION_NOT_FOUND",
                "description": "`consultationId` inconnu, d'une autre organisation ou d'un site non ouvert à l'application."
              },
              {
                "code": "SITE_NOT_FOUND",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un site ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "form": {
              "category": "IMAGERIE",
              "consultationId": "c445bded-90de-4d04-a1c7-9dddc247d9d5",
              "file": "<fichier>",
              "title": "Radio"
            },
            "method": "POST",
            "url": "/api/partner/v1/patients/7dd29725-ea42-4557-bc80-4b9165d4ef76/documents"
          },
          "response": {
            "body": {
              "category": "IMAGERIE",
              "consultationId": "c445bded-90de-4d04-a1c7-9dddc247d9d5",
              "createdAt": "2026-10-15T02:06:13.461399Z",
              "id": "7aa6c214-49d6-48f0-9e20-b486349443f1",
              "patientId": "7dd29725-ea42-4557-bc80-4b9165d4ef76",
              "signedAt": null,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "status": "GENERATED",
              "title": "Radio",
              "type": "UPLOAD",
              "updatedAt": "2026-10-15T02:06:13.461399Z",
              "validatedAt": null,
              "visibility": "INTERNAL"
            },
            "status": 201
          }
        }
      }
    },
    "/api/partner/v1/patients/{id}/history": {
      "get": {
        "tags": [
          "Consultations"
        ],
        "summary": "Lire l'historique clinique d'un animal",
        "description": "Rend les consultations récentes de l'animal, de la plus récente à la plus ancienne, résumées, sur les sites ouverts à l'application seulement (hospitalisations comprises, résultats de laboratoire exclus). C'est un résumé, sans curseur (`nextCursor` vaut toujours `null`) : pour tout synchroniser, `GET /consultations?patient_id=…`. `limit` compte les lignes rendues, après le filtre des sites.",
        "operationId": "GET_api_partner_v1_patients_id_history",
        "security": [
          {
            "partner": [
              "consultations:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "consultations:read"
          ]
        },
        "x-treats-controller": "PartnerPatientRecordController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de l'animal.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Nombre de consultations au plus, de 1 à 200. Au-delà de 200, 200.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Ne garde que les consultations de ce jour ou après (`AAAA-MM-JJ`, Europe/Paris, inclusif, années 1900 à 9999).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "L'historique résumé.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/HistoryEntry"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`limit` inférieur à 1 ou non entier, `since` illisible ou hors des années 1900 à 9999."
              }
            ]
          },
          "404": {
            "description": "PATIENT_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PATIENT_NOT_FOUND",
                "description": "Animal inconnu, d'une autre organisation, ou dont le client n'est pas rattaché à un site ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/patients/eee033ad-dfdd-4079-8a3a-d82ad2360a3b/history?limit=2"
          },
          "response": {
            "body": {
              "items": [
                {
                  "category": "CONSULTATION",
                  "consultationId": "982e1e4b-e5ff-411b-9504-8d9f85809f11",
                  "date": "2026-10-15T02:14:19.218069Z",
                  "hasDocuments": false,
                  "hasPrescription": false,
                  "reason": "Sur X 2",
                  "siteId": "61b796e2-921f-428b-b9aa-30c684b4ed91",
                  "title": "Consultation",
                  "validated": false,
                  "vetName": "Admin Organisation"
                },
                {
                  "category": "CONSULTATION",
                  "consultationId": "a5bfe7b3-deaf-4111-a18f-47eb82ad8442",
                  "date": "2026-10-15T02:14:19.147393Z",
                  "hasDocuments": false,
                  "hasPrescription": false,
                  "reason": "Sur X 1",
                  "siteId": "61b796e2-921f-428b-b9aa-30c684b4ed91",
                  "title": "Consultation",
                  "validated": false,
                  "vetName": "Admin Organisation"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/patients/{id}/weights": {
      "get": {
        "tags": [
          "Animaux"
        ],
        "summary": "Lister les pesées d'un animal",
        "description": "Rend toutes les pesées d'un animal dont vous voyez le propriétaire, de la plus récente à la plus ancienne. Liste complète en une seule page : `nextCursor` vaut toujours `null`, et ni `limit` ni `cursor` ne sont lus. Exige `patients:read`.",
        "operationId": "GET_api_partner_v1_patients_id_weights",
        "security": [
          {
            "partner": [
              "patients:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "patients:read"
          ]
        },
        "x-treats-controller": "PartnerPatientsController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de l'animal.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les pesées de l'animal.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PatientWeight"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "PATIENT_NOT_FOUND · NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PATIENT_NOT_FOUND",
                "description": "Animal inconnu, d'une autre organisation, ou dont le propriétaire est né sur un site qui ne vous est pas ouvert."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/patients/8166f358-8478-4c44-bac2-43914d4cbc60/weights"
          },
          "response": {
            "body": {
              "items": [
                {
                  "id": "2d435e5a-2dfe-4d38-999e-de59d912eaf2",
                  "measuredAt": "2026-10-01T09:00:00Z",
                  "weightKg": 12.9
                },
                {
                  "id": "02133572-181f-4e8a-ade7-541f20984aff",
                  "measuredAt": "2026-09-01T09:00:00Z",
                  "weightKg": 12.4
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/payments": {
      "get": {
        "tags": [
          "Facturation"
        ],
        "summary": "Lister les encaissements",
        "description": "Rend une page des mouvements d'encaissement des sites ouverts à l'application : règlements de factures, acomptes et remboursements (montants négatifs). Un paiement enregistré sans site est rattaché au site de la facture qu'il règle. Les règlements de pièces reprises qui ne sont pas des factures sont exclus : un paiement ne pointe jamais vers une facture introuvable par l'API.\n\nTri par date de synchronisation (`updatedAt`) puis identifiant. Les modifications des 60 dernières secondes n'apparaissent qu'aux appels suivants. Aucune route ne lit un paiement par son identifiant.",
        "operationId": "GET_api_partner_v1_payments",
        "security": [
          {
            "partner": [
              "billing:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "billing:read"
          ]
        },
        "x-treats-controller": "PartnerPaymentController",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "description": "Jour d'encaissement minimal, inclus (`AAAA-MM-JJ`, heure de Paris).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Jour d'encaissement maximal, inclus (`AAAA-MM-JJ`, heure de Paris).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Restreint à un site ouvert à l'application (ou passer l'en-tête `Treats-Site-Id`). Absent : tous les sites ouverts.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Ne rend que les mouvements dont `updatedAt` est postérieur ou égal à cet instant (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Curseur `nextCursor` de la page précédente, repassé tel quel. Absent : première page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Nombre de mouvements par page, de 1 à 200 (au-delà de 200, ramené à 200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Site visé, à la place du paramètre `site_id`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page de mouvements d'encaissement.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Payment"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "Date `from` / `to` mal formée ou hors des années 1900 à 9999, `updated_since` mal formé, `limit` inférieur à 1 ou non entier, curseur illisible. Un détail par champ."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un identifiant valide."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé n'est pas ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/payments?from=2026-10-15&to=2026-10-15&limit=200"
          },
          "response": {
            "body": {
              "items": [
                {
                  "amountCents": 2000,
                  "clientId": "e58157c4-b62f-48b0-b40e-a5c2a6bbdc4e",
                  "creditNoteId": null,
                  "currency": "EUR",
                  "groupId": null,
                  "id": "ab57b84c-70f5-454f-9a23-41bfd241698d",
                  "invoiceId": "e6303709-375d-4067-af6a-360bb484be7e",
                  "kind": "INVOICE",
                  "method": "CASH",
                  "receivedAt": "2026-10-15T02:06:35.199141Z",
                  "reference": null,
                  "refundedCents": 0,
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "updatedAt": "2026-10-15T02:06:35.221673Z"
                },
                {
                  "amountCents": 6000,
                  "clientId": "e58157c4-b62f-48b0-b40e-a5c2a6bbdc4e",
                  "creditNoteId": null,
                  "currency": "EUR",
                  "groupId": null,
                  "id": "f2013c0f-3c92-4d1e-a49c-e8cbc6442a7d",
                  "invoiceId": "4b1a19b3-0c7b-42e1-b1a7-eebdee15f6d9",
                  "kind": "INVOICE",
                  "method": "CARD",
                  "receivedAt": "2026-10-15T02:07:07.650110Z",
                  "reference": null,
                  "refundedCents": 0,
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "updatedAt": "2026-10-15T02:07:07.672971Z"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/prescriptions": {
      "get": {
        "tags": [
          "Ordonnances"
        ],
        "summary": "Lister les ordonnances signées",
        "description": "Rend une page des ordonnances signées (ou annulées après signature) établies sur les sites ouverts à l'application, sans leurs lignes (`lineCount` donne leur nombre). Les brouillons ne sont jamais rendus. Tri par date de synchronisation (`updatedAt`) puis identifiant : une ordonnance annulée revient dans `updated_since`.\n\nLes modifications des 60 dernières secondes n'apparaissent qu'aux appels suivants ; la lecture par identifiant est immédiate. Un `patient_id` inconnu rend une liste vide, pas une erreur.",
        "operationId": "GET_api_partner_v1_prescriptions",
        "security": [
          {
            "partner": [
              "prescriptions:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "prescriptions:read"
          ]
        },
        "x-treats-controller": "PartnerPrescriptionController",
        "parameters": [
          {
            "name": "patient_id",
            "in": "query",
            "description": "Restreint aux ordonnances de cet animal.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Restreint à ce statut. Insensible à la casse. `DRAFT` est refusé.",
            "schema": {
              "type": "string",
              "enum": [
                "SIGNED",
                "CANCELLED"
              ]
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Restreint à un site ouvert à l'application (ou passer l'en-tête `Treats-Site-Id`). Absent : tous les sites ouverts.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Ne rend que les ordonnances dont `updatedAt` est postérieur ou égal à cet instant (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Curseur `nextCursor` de la page précédente, repassé tel quel. Absent : première page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Nombre d'ordonnances par page, de 1 à 200 (au-delà de 200, ramené à 200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Site visé, à la place du paramètre `site_id`.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page d'ordonnances, sans lignes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Prescription"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`status` hors liste (dont `DRAFT` : les brouillons ne sont pas exposés), `patient_id` ou `updated_since` mal formé, `limit` inférieur à 1 ou non entier, curseur illisible. Un détail par champ."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un identifiant valide."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé n'est pas ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/prescriptions?patient_id=3750f656-f9de-4d7e-a311-29c32a48c510"
          },
          "response": {
            "body": {
              "items": [
                {
                  "cancellationReason": null,
                  "cancelledAt": null,
                  "consultationId": "517d37b1-69e9-4d77-84a1-bcc98bf18fb9",
                  "expiresAt": "2027-10-15T02:06:35.800844Z",
                  "id": "33d6a02d-9243-412c-84e0-040687ceb2a0",
                  "issuedAt": "2026-10-15T02:06:35.800844Z",
                  "lineCount": 1,
                  "lines": null,
                  "number": "ORD-2026-00001",
                  "patientId": "3750f656-f9de-4d7e-a311-29c32a48c510",
                  "prescriberName": null,
                  "recommendations": null,
                  "renewalPolicy": null,
                  "renewedFromId": null,
                  "signedAt": "2026-10-15T02:06:35.800844Z",
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "status": "SIGNED",
                  "type": "SIMPLE",
                  "updatedAt": "2026-10-15T02:06:35.829440Z",
                  "valid": null
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/prescriptions/{id}": {
      "get": {
        "tags": [
          "Ordonnances"
        ],
        "summary": "Lire une ordonnance signée",
        "description": "Rend le détail d'une ordonnance signée (ou annulée après signature) d'un site ouvert à l'application : prescripteur, recommandations, renouvellement, annulation et lignes avec leur posologie. Un brouillon, ou une ordonnance d'un site non ouvert, répond 404.",
        "operationId": "GET_api_partner_v1_prescriptions_id",
        "security": [
          {
            "partner": [
              "prescriptions:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "prescriptions:read"
          ]
        },
        "x-treats-controller": "PartnerPrescriptionController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de l'ordonnance.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "L'ordonnance, avec ses lignes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Prescription"
                }
              }
            }
          },
          "404": {
            "description": "PRESCRIPTION_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PRESCRIPTION_NOT_FOUND",
                "description": "Aucune ordonnance signée de ce périmètre ne porte cet identifiant (brouillon, autre organisation, site non ouvert)."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/prescriptions/33d6a02d-9243-412c-84e0-040687ceb2a0"
          },
          "response": {
            "body": {
              "cancellationReason": null,
              "cancelledAt": null,
              "consultationId": "517d37b1-69e9-4d77-84a1-bcc98bf18fb9",
              "expiresAt": "2027-10-15T02:06:35.800844Z",
              "id": "33d6a02d-9243-412c-84e0-040687ceb2a0",
              "issuedAt": "2026-10-15T02:06:35.800844Z",
              "lineCount": 1,
              "lines": [
                {
                  "catalogItemId": "daad43c2-16a9-4b55-8fb4-5d7f81c243c7",
                  "computedDoseUnit": null,
                  "computedDoseValue": null,
                  "conditioning": null,
                  "dosage": "1 comprimé matin et soir",
                  "duration": "7 jours",
                  "freeText": false,
                  "id": "200c88f1-3a94-46a3-b31d-19b2a0a0934a",
                  "instructions": "Avec le repas",
                  "name": "Produit",
                  "offLabelUse": false,
                  "quantity": 1,
                  "route": null,
                  "substance": null
                }
              ],
              "number": "ORD-2026-00001",
              "patientId": "3750f656-f9de-4d7e-a311-29c32a48c510",
              "prescriberName": "Dr Claire Moreau",
              "recommendations": null,
              "renewalPolicy": "NOT_ALLOWED",
              "renewedFromId": null,
              "signedAt": "2026-10-15T02:06:35.800844Z",
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "status": "SIGNED",
              "type": "SIMPLE",
              "updatedAt": "2026-10-15T02:06:35.829440Z",
              "valid": true
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/prescriptions/{id}/pdf": {
      "get": {
        "tags": [
          "Ordonnances"
        ],
        "summary": "Télécharger le PDF d'une ordonnance signée",
        "description": "Rend le PDF officiel de l'ordonnance, celui remis au client, avec l'en-tête du site où elle a été établie. Même périmètre que `GET /prescriptions/{id}` : un brouillon répond 404. Le fichier est servi avec `Content-Disposition: inline; filename=\"ordonnance-<id>.pdf\"`.",
        "operationId": "GET_api_partner_v1_prescriptions_id_pdf",
        "security": [
          {
            "partner": [
              "prescriptions:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "prescriptions:read"
          ]
        },
        "x-treats-controller": "PartnerPrescriptionController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de l'ordonnance.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Le PDF de l'ordonnance.",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "PRESCRIPTION_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "PRESCRIPTION_NOT_FOUND",
                "description": "Aucune ordonnance signée de ce périmètre ne porte cet identifiant."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/prescriptions/33d6a02d-9243-412c-84e0-040687ceb2a0/pdf"
          },
          "response": {
            "contentType": "application/pdf",
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/rooms": {
      "get": {
        "tags": [
          "Organisation"
        ],
        "summary": "Lister les salles",
        "description": "Rend les salles et ressources actives (salles, bloc, imagerie, équipements…) des sites ouverts à l'application, ou du seul site demandé par `site_id` ou l'en-tête `Treats-Site-Id`. Les ressources désactivées par la clinique ne sont pas rendues. Résultat groupé par site, puis dans l'ordre d'affichage choisi par la clinique et par nom. Une seule page : `nextCursor` vaut toujours `null`.",
        "operationId": "GET_api_partner_v1_rooms",
        "security": [
          {
            "partner": [
              "org:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "org:read"
          ]
        },
        "x-treats-controller": "PartnerRoomsController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Limite la lecture à ce site. Sans lui ni l'en-tête : tous les sites ouverts à l'application. Le paramètre l'emporte sur l'en-tête.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les salles actives.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Room"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/rooms"
          },
          "response": {
            "body": {
              "items": [],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/sites": {
      "get": {
        "tags": [
          "Organisation"
        ],
        "summary": "Lister les sites",
        "description": "Rend les sites de l'organisation que l'application peut lire : tous ses sites si l'organisation a ouvert l'accès à tous, sinon ceux qu'elle a choisis. Les sites en sommeil sont inclus (`dormant: true`). Tri par nom, sans distinction de casse. Liste rendue en une seule page : `nextCursor` vaut toujours `null`, et la route ne lit ni `limit`, ni `cursor`, ni `updated_since`, ni filtre de site.",
        "operationId": "GET_api_partner_v1_sites",
        "security": [
          {
            "partner": [
              "org:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "org:read"
          ]
        },
        "x-treats-controller": "PartnerSitesController",
        "responses": {
          "200": {
            "description": "Les sites ouverts à l'application.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Site"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/sites"
          },
          "response": {
            "body": {
              "items": [
                {
                  "address": {
                    "city": null,
                    "country": null,
                    "line": "15 Rue du Château, 92100 Boulogne-Billancourt",
                    "postalCode": null,
                    "street": null
                  },
                  "code": "BOU",
                  "dormant": false,
                  "email": "boulogne@exemple.fr",
                  "id": "6b4e48ea-0112-4957-a834-121c1d272492",
                  "name": "Clinique des Tilleuls Nord",
                  "organizationId": "9f825730-94a5-46bd-b1aa-e0a407441d31",
                  "phone": "0112345678",
                  "siret": "92100000000002",
                  "timezone": "Europe/Paris"
                },
                {
                  "address": {
                    "city": null,
                    "country": null,
                    "line": "3 Boulevard de l'Oise, 95000 Cergy",
                    "postalCode": null,
                    "street": null
                  },
                  "code": "CER",
                  "dormant": false,
                  "email": "cergy@exemple.fr",
                  "id": "bc1a0f81-6261-4e75-a61b-982ed33872a4",
                  "name": "Clinique des Tilleuls Sud",
                  "organizationId": "9f825730-94a5-46bd-b1aa-e0a407441d31",
                  "phone": "0112345678",
                  "siret": "95000000000009",
                  "timezone": "Europe/Paris"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/sites/{id}": {
      "get": {
        "tags": [
          "Organisation"
        ],
        "summary": "Lire un site",
        "description": "Rend un site de l'organisation. Un site d'une autre organisation, un site de l'organisation que l'application ne peut pas lire et un identifiant inconnu répondent tous `404 SITE_NOT_FOUND` : la réponse ne dit pas si le site existe.",
        "operationId": "GET_api_partner_v1_sites_id",
        "security": [
          {
            "partner": [
              "org:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "org:read"
          ]
        },
        "x-treats-controller": "PartnerSitesController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant du site.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Le site.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "404": {
            "description": "SITE_NOT_FOUND · NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Site inconnu, d'une autre organisation, ou non ouvert à l'application."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/sites/84b9ad0c-5917-4d16-a75c-d3dc97abe595"
          },
          "response": {
            "body": {
              "address": {
                "city": null,
                "country": null,
                "line": "12 Rue de la Visitation, 35000 Rennes",
                "postalCode": null,
                "street": null
              },
              "code": "MAM",
              "dormant": false,
              "email": "contact@exemple.fr",
              "id": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "name": "Clinique des Tilleuls",
              "organizationId": "9b9ff4bd-d49e-4b44-a41a-82136caaa3f8",
              "phone": "0212345678",
              "siret": "35000000000001",
              "timezone": "Europe/Paris"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/sites/{id}/holidays": {
      "get": {
        "tags": [
          "Organisation"
        ],
        "summary": "Lister les jours fériés et fermetures d'un site",
        "description": "Rend les jours fériés communs à tous les sites (`scope: NATIONAL`) et les jours de fermeture propres au site (`scope: SITE`), triés par date croissante. Tous les jours enregistrés sont rendus, passés compris, sans filtre de période. Un jour marqué `recurringYearly` n'apparaît qu'une fois, à sa date d'origine : reportez-le sur chaque année. Les fermetures exceptionnelles sur une période (congés, formation) et les absences de l'équipe ne sont pas rendues. Une seule page : `nextCursor` vaut toujours `null`.",
        "operationId": "GET_api_partner_v1_sites_id_holidays",
        "security": [
          {
            "partner": [
              "org:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "org:read"
          ]
        },
        "x-treats-controller": "PartnerSitesController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant du site.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les jours fériés et de fermeture du site, par date croissante.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SiteHoliday"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "SITE_NOT_FOUND · NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Site inconnu, d'une autre organisation, ou non ouvert à l'application."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/sites/84b9ad0c-5917-4d16-a75c-d3dc97abe595/holidays"
          },
          "response": {
            "body": {
              "items": [
                {
                  "date": "2026-01-01",
                  "id": "497d66c8-e21d-4154-8e03-1a0f3824373b",
                  "label": "Jour de l'An",
                  "recurringYearly": true,
                  "scope": "NATIONAL"
                },
                {
                  "date": "2026-04-06",
                  "id": "b9a14e74-9ff0-4e1c-a5cb-2e93e8ad23c1",
                  "label": "Lundi de Pâques",
                  "recurringYearly": false,
                  "scope": "NATIONAL"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/sites/{id}/opening-hours": {
      "get": {
        "tags": [
          "Organisation"
        ],
        "summary": "Lire les horaires d'ouverture d'un site",
        "description": "Rend les horaires hebdomadaires du site, un élément par jour paramétré, du lundi au dimanche. Seuls les jours que la clinique a renseignés sont rendus : un site sans horaires rend une liste vide, et un jour absent n'est ni ouvert ni fermé, il est inconnu. Les heures sont locales au site (`HH:mm`, fuseau `timezone` du site). Cette route ne tient compte ni des jours fériés ni des fermetures (voir `GET /sites/{id}/holidays`), ni des absences de l'équipe. Une seule page : `nextCursor` vaut toujours `null`.",
        "operationId": "GET_api_partner_v1_sites_id_opening_hours",
        "security": [
          {
            "partner": [
              "org:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "org:read"
          ]
        },
        "x-treats-controller": "PartnerSitesController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant du site.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les horaires du site, du lundi au dimanche.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SiteOpeningHours"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "SITE_NOT_FOUND · NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Site inconnu, d'une autre organisation, ou non ouvert à l'application."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/sites/84b9ad0c-5917-4d16-a75c-d3dc97abe595/opening-hours"
          },
          "response": {
            "body": {
              "items": [
                {
                  "breakEnd": "14:00",
                  "breakStart": "12:30",
                  "close": "19:00",
                  "closed": false,
                  "dayOfWeek": "MONDAY",
                  "open": "08:30",
                  "slotMinutes": 30
                },
                {
                  "breakEnd": "14:00",
                  "breakStart": "12:30",
                  "close": "19:00",
                  "closed": false,
                  "dayOfWeek": "TUESDAY",
                  "open": "08:30",
                  "slotMinutes": 30
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/species": {
      "get": {
        "tags": [
          "Organisation"
        ],
        "summary": "Lister les espèces",
        "description": "Rend les espèces que l'accueil de la clinique peut choisir : les espèces Treats et celles créées par l'organisation, moins celles qu'elle a masquées ou désactivées pour le site. Le site pris en compte est celui de `site_id` (ou de l'en-tête `Treats-Site-Id`) ; à défaut, le site unique ouvert à l'application ; à défaut, s'il y en a plusieurs, aucun réglage de site n'est appliqué et la liste suit le réglage de l'organisation. Tri dans l'ordre d'affichage choisi par la clinique, puis par libellé. Une seule page : `nextCursor` vaut toujours `null`.",
        "operationId": "GET_api_partner_v1_species",
        "security": [
          {
            "partner": [
              "org:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "org:read"
          ]
        },
        "x-treats-controller": "PartnerSpeciesController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Site dont on applique le choix d'espèces. Le paramètre l'emporte sur l'en-tête.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les espèces proposées.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Species"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/species"
          },
          "response": {
            "body": {
              "items": [
                {
                  "code": "CAT",
                  "group": "CAT",
                  "label": "Chat",
                  "parentCode": null
                },
                {
                  "code": "DOG",
                  "group": "DOG",
                  "label": "Chien",
                  "parentCode": null
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/species/{code}/breeds": {
      "get": {
        "tags": [
          "Organisation"
        ],
        "summary": "Lister les races d'une espèce",
        "description": "Rend les races actives de l'espèce, moins celles que l'organisation a masquées, races créées par l'organisation comprises. L'espèce doit figurer dans `GET /species` pour le même site (mêmes règles de choix du site) ; sinon `404 SPECIES_NOT_FOUND`. Le code d'espèce est lu sans distinction de casse. Tri dans l'ordre d'affichage choisi par la clinique, puis par libellé. Une seule page : `nextCursor` vaut toujours `null`.",
        "operationId": "GET_api_partner_v1_species_code_breeds",
        "security": [
          {
            "partner": [
              "org:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "org:read"
          ]
        },
        "x-treats-controller": "PartnerSpeciesController",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "description": "Code de l'espèce (`code` d'une espèce de `GET /species`), sans distinction de casse.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "DOG"
            }
          },
          {
            "name": "site_id",
            "in": "query",
            "description": "Site dont on applique le choix d'espèces. Le paramètre l'emporte sur l'en-tête.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les races de l'espèce.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Breed"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND · SPECIES_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              },
              {
                "code": "SPECIES_NOT_FOUND",
                "description": "Espèce inconnue, ou non proposée par l'organisation pour ce site (masquée, désactivée)."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/species/DOG/breeds"
          },
          "response": {
            "body": {
              "items": [
                {
                  "code": "LABRADOR_RETRIEVER",
                  "icadCode": "122",
                  "id": "b6004c5f-5da0-4e8f-a1dd-053751b4813e",
                  "label": "Labrador Retriever",
                  "speciesCode": "DOG"
                },
                {
                  "code": "BERGER_ALLEMAND",
                  "icadCode": "166",
                  "id": "2d5bcaf2-0ad1-4582-b939-1bb6f35ff8f3",
                  "label": "Berger Allemand",
                  "speciesCode": "DOG"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/staff": {
      "get": {
        "tags": [
          "Organisation"
        ],
        "summary": "Lister l'équipe",
        "description": "Rend les membres actifs de l'équipe qui ont une affectation active et en cours de validité sur un site ouvert à l'application, ou sur le seul site demandé par `site_id` ou l'en-tête `Treats-Site-Id`. Ni email, ni téléphone, ni droits ; les comptes techniques (intégrations) n'y figurent jamais. Tri par nom puis prénom, sans distinction de casse. Une seule page : `nextCursor` vaut toujours `null`.",
        "operationId": "GET_api_partner_v1_staff",
        "security": [
          {
            "partner": [
              "org:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "org:read"
          ]
        },
        "x-treats-controller": "PartnerStaffController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Limite la lecture à ce site ; `siteIds` ne contient alors que lui. Le paramètre l'emporte sur l'en-tête.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "role",
            "in": "query",
            "description": "Ne garde que les membres de ce rôle. Valeur lue sans distinction de casse (`vet` vaut `VET`).",
            "schema": {
              "type": "string",
              "enum": [
                "VET",
                "ASV",
                "ADMIN",
                "SUPER_ADMIN",
                "VIEWER",
                "CLUSTER_MANAGER",
                "REGIONAL_DIRECTOR",
                "GROUP_ADMIN"
              ]
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les membres de l'équipe.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StaffMember"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_SITE_ID · INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              },
              {
                "code": "INVALID_FIELD",
                "description": "`role` ne fait pas partie des valeurs possibles (`details[].field` = `role`)."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/staff"
          },
          "response": {
            "body": {
              "items": [
                {
                  "firstName": "Super",
                  "id": "e6a4d8ea-1a5a-4061-9bd7-953e8864e456",
                  "lastName": "Admin",
                  "primarySiteId": "61b796e2-921f-428b-b9aa-30c684b4ed91",
                  "role": "SUPER_ADMIN",
                  "siteIds": [
                    "61b796e2-921f-428b-b9aa-30c684b4ed91",
                    "6b4e48ea-0112-4957-a834-121c1d272492",
                    "6b1a3785-35f9-4e82-afaf-032c8a456c36"
                  ]
                },
                {
                  "firstName": "Marie",
                  "id": "b78c90a7-664e-4e4f-9bc2-f961a4ae9d34",
                  "lastName": "Bernard",
                  "primarySiteId": "2519e45e-a05a-41a5-997e-3748eb1a660f",
                  "role": "VET",
                  "siteIds": [
                    "2519e45e-a05a-41a5-997e-3748eb1a660f"
                  ]
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/tasks": {
      "get": {
        "tags": [
          "Notes d'appel et tâches"
        ],
        "summary": "Lister les tâches d'équipe",
        "description": "Rend les tâches d'équipe des sites ouverts à l'application, ou du seul site demandé, triées par date de modification (`updatedAt`) puis par identifiant. Les tâches personnelles des membres de l'équipe ne sont jamais rendues, quel qu'en soit l'auteur. Pagination par `cursor`, synchronisation par `updated_since` ; une écriture n'apparaît dans cette liste qu'au bout de 60 secondes environ.",
        "operationId": "GET_api_partner_v1_tasks",
        "security": [
          {
            "partner": [
              "agenda:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:read"
          ]
        },
        "x-treats-controller": "PartnerTaskController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Limite la lecture à ce site. Sans lui ni l'en-tête : tous les sites ouverts à l'application. Le paramètre l'emporte sur l'en-tête.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Seulement les tâches de ce statut.",
            "schema": {
              "type": "string",
              "enum": [
                "TODO",
                "IN_PROGRESS",
                "DONE",
                "CANCELLED"
              ]
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Tâches modifiées à partir de cet instant (inclus). Années 1900 à 9999.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Curseur opaque rendu par la page précédente (`nextCursor`). Absent : première page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Nombre de tâches par page. Une valeur au-delà de 200 est ramenée à 200.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page de tâches d'équipe, par `updatedAt` puis `id` croissants.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Task"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`status` inconnu, paramètre illisible (instant, `limit` inférieur à 1, `cursor` invalide) ou date hors des années 1900 à 9999. Un détail par paramètre."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/tasks?site_id=84b9ad0c-5917-4d16-a75c-d3dc97abe595&status=DONE&limit=200"
          },
          "response": {
            "body": {
              "items": [
                {
                  "assignees": [
                    {
                      "id": "de346612-a576-4495-b3c2-863730b805ad",
                      "name": "Emma Girard"
                    }
                  ],
                  "clientId": null,
                  "completedAt": "2026-10-10T13:10:00Z",
                  "createdAt": "2026-10-10T11:45:00Z",
                  "createdBy": {
                    "id": "c5280853-b63b-43d1-85ea-88cbd10e3f3a",
                    "name": "Super Admin"
                  },
                  "dateOnly": false,
                  "description": "Aspiration + désinfection surfaces",
                  "dueAt": "2026-10-10T12:45:00Z",
                  "durationMinutes": 25,
                  "id": "1622999c-4ad7-43d8-9cb7-396bd24e3237",
                  "patientId": null,
                  "priority": "LOW",
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "status": "DONE",
                  "title": "Nettoyage salle d'attente",
                  "updatedAt": "2026-10-15T01:55:35.162754Z"
                },
                {
                  "assignees": [
                    {
                      "id": "de346612-a576-4495-b3c2-863730b805ad",
                      "name": "Emma Girard"
                    }
                  ],
                  "clientId": null,
                  "completedAt": "2026-10-13T08:45:00Z",
                  "createdAt": "2026-10-13T07:00:00Z",
                  "createdBy": {
                    "id": "051772bb-58b3-443a-9a47-c39da662bc0d",
                    "name": "Thomas Leroy"
                  },
                  "dateOnly": false,
                  "description": "Vérifier stock et commander manquants",
                  "dueAt": "2026-10-13T08:00:00Z",
                  "durationMinutes": 45,
                  "id": "178e8d8d-09bf-494e-a7a0-b7a8b3c35eb5",
                  "patientId": null,
                  "priority": "MEDIUM",
                  "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
                  "status": "DONE",
                  "title": "Réapprovisionnement pharmacie",
                  "updatedAt": "2026-10-15T01:55:35.162754Z"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      },
      "post": {
        "tags": [
          "Notes d'appel et tâches"
        ],
        "summary": "Créer une tâche d'équipe",
        "description": "Crée une tâche d'équipe sur le site donné par `site_id` ou `Treats-Site-Id` (implicite si un seul site est ouvert), en statut `TODO`, avec l'utilisateur d'intégration pour auteur. Sans assigné, elle va au pool de l'équipe du site ; chaque assigné doit faire partie de l'équipe du site. Une tâche peut être rattachée à un client et à un animal, que l'application doit pouvoir lire ; donnés ensemble, l'animal doit appartenir au client. Une tâche personnelle ne se crée pas par l'API (`isPrivate` refusé). Sans `durationMinutes`, une tâche avec heure dure 30 minutes et une tâche à la journée n'a pas de durée.",
        "operationId": "POST_api_partner_v1_tasks",
        "security": [
          {
            "partner": [
              "agenda:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:write"
          ]
        },
        "x-treats-controller": "PartnerTaskController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Site de la tâche. Obligatoire (ici ou dans l'en-tête) si l'organisation a ouvert plusieurs sites à l'application.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "Titre, non vide.",
                    "maxLength": 255
                  },
                  "description": {
                    "type": "string",
                    "description": "Description.",
                    "maxLength": 5000
                  },
                  "priority": {
                    "type": "string",
                    "enum": [
                      "LOW",
                      "MEDIUM",
                      "HIGH",
                      "URGENT"
                    ],
                    "description": "Priorité.",
                    "default": "MEDIUM"
                  },
                  "dueAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Échéance. Obligatoire avec `dateOnly: true`. Années 1900 à 9999."
                  },
                  "dateOnly": {
                    "type": "boolean",
                    "description": "`true` : seul le jour de `dueAt` compte.",
                    "default": false
                  },
                  "durationMinutes": {
                    "type": "integer",
                    "description": "Durée prévue en minutes.",
                    "minimum": 5,
                    "maximum": 480
                  },
                  "assigneeIds": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Personnes assignées, membres de l'équipe du site (voir `GET /staff`). Les doublons sont ignorés. Absente ou vide : pool de l'équipe."
                  },
                  "patientId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Animal concerné."
                  },
                  "clientId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Client concerné."
                  }
                },
                "required": [
                  "title"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "La tâche créée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "400": {
            "description": "BODY_REQUIRED · INVALID_FIELD · INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "BODY_REQUIRED",
                "description": "Corps absent."
              },
              {
                "code": "INVALID_FIELD",
                "description": "Champ absent, illisible ou inconnu, dont `isPrivate` (un détail par champ) ; `title` vide ou de plus de 255 caractères ; `description` de plus de 5 000 caractères ; priorité inconnue ; `dateOnly: true` sans `dueAt` ; `durationMinutes` hors de 5 à 480 ; `assigneeIds` qui n'est pas une liste d'UUID ; `patientId` qui n'appartient pas à `clientId`."
              },
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND · CLIENT_NOT_FOUND · PATIENT_NOT_FOUND · USER_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              },
              {
                "code": "CLIENT_NOT_FOUND",
                "description": "Client inconnu, d'une autre organisation, ou que l'application ne peut pas lire."
              },
              {
                "code": "PATIENT_NOT_FOUND",
                "description": "Animal inconnu, d'une autre organisation, ou dont le client n'est pas lisible par l'application."
              },
              {
                "code": "USER_NOT_FOUND",
                "description": "Une personne de `assigneeIds` ne fait pas partie de l'équipe du site."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "body": {
              "clientId": "4ce5759e-de4d-42c2-b480-0f2981683141",
              "patientId": "eee033ad-dfdd-4079-8a3a-d82ad2360a3b",
              "title": "Rappeler"
            },
            "method": "POST",
            "url": "/api/partner/v1/tasks"
          },
          "response": {
            "body": {
              "assignees": [],
              "clientId": "4ce5759e-de4d-42c2-b480-0f2981683141",
              "completedAt": null,
              "createdAt": "2026-10-15T02:14:15.376565Z",
              "createdBy": {
                "id": "c60ff856-2a2a-4ebb-8922-87d3e206ceec",
                "name": "Intégration Correctifs un site"
              },
              "dateOnly": false,
              "description": null,
              "dueAt": null,
              "durationMinutes": 30,
              "id": "4c426541-f6cc-4f60-b27b-dad78a270b7f",
              "patientId": "eee033ad-dfdd-4079-8a3a-d82ad2360a3b",
              "priority": "MEDIUM",
              "siteId": "61b796e2-921f-428b-b9aa-30c684b4ed91",
              "status": "TODO",
              "title": "Rappeler",
              "updatedAt": "2026-10-15T02:14:15.375738Z"
            },
            "status": 201
          }
        }
      }
    },
    "/api/partner/v1/tasks/{id}/complete": {
      "post": {
        "tags": [
          "Notes d'appel et tâches"
        ],
        "summary": "Marquer une tâche comme faite",
        "description": "Passe une tâche d'équipe d'un site ouvert à l'application en `status: DONE` et date `completedAt`. Idempotent : une tâche déjà faite est rendue telle quelle, sans nouvelle date. Une tâche personnelle d'un membre de l'équipe répond `404`, comme une tâche inconnue. Aucun corps n'est lu.",
        "operationId": "POST_api_partner_v1_tasks_id_complete",
        "security": [
          {
            "partner": [
              "agenda:write"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:write"
          ]
        },
        "x-treats-controller": "PartnerTaskController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la tâche.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "responses": {
          "200": {
            "description": "La tâche, faite.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "404": {
            "description": "TASK_NOT_FOUND · NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "TASK_NOT_FOUND",
                "description": "Tâche inconnue, personnelle, d'une autre organisation, ou d'un site non ouvert à l'application."
              },
              {
                "code": "NOT_FOUND",
                "description": "`id` n'est pas un UUID."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "POST",
            "url": "/api/partner/v1/tasks/2b6313a1-cefd-4c81-b356-237f4166bf20/complete"
          },
          "response": {
            "body": {
              "assignees": [],
              "clientId": "31ba4810-584b-4ebb-9f4e-b99cc9007c2d",
              "completedAt": "2026-10-15T02:04:35.725883Z",
              "createdAt": "2026-10-15T02:04:31.789509Z",
              "createdBy": {
                "id": "14deba83-08d1-4fd3-98c4-7908063dee3b",
                "name": "Intégration Agenda"
              },
              "dateOnly": false,
              "description": null,
              "dueAt": "2027-06-21T07:00:00Z",
              "durationMinutes": 30,
              "id": "2b6313a1-cefd-4c81-b356-237f4166bf20",
              "patientId": null,
              "priority": "HIGH",
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "status": "DONE",
              "title": "Rappeler le client",
              "updatedAt": "2026-10-15T02:04:35.726717Z"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/waiting-room": {
      "get": {
        "tags": [
          "Agenda"
        ],
        "summary": "Lire la salle d'attente",
        "description": "Rend les animaux en salle d'attente d'un site : rendez-vous pointés arrivés (`workflow: WAITING`), ni annulés, ni non honorés, ni d'un animal décédé, arrivés depuis moins de 12 heures, du plus ancien arrivé au plus récent. Un animal appelé en consultation n'y figure plus. Instantané en une seule page : `nextCursor` vaut toujours `null`, et l'enveloppe porte en plus le site lu et le nombre d'animaux en attente.",
        "operationId": "GET_api_partner_v1_waiting_room",
        "security": [
          {
            "partner": [
              "agenda:read"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "agenda:read"
          ]
        },
        "x-treats-controller": "PartnerWaitingRoomController",
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "description": "Site lu. Obligatoire (ici ou dans l'en-tête) si l'organisation a ouvert plusieurs sites à l'application.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Treats-Site-Id",
            "in": "header",
            "description": "Équivalent de `site_id`, lu quand le paramètre est absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Les animaux en attente, par heure d'arrivée croissante.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WaitingRoomEntry"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    },
                    "siteId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "Site lu."
                    },
                    "totalWaiting": {
                      "type": "integer",
                      "description": "Nombre d'animaux en attente (taille de `items`)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_SITE_ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_SITE_ID",
                "description": "`site_id` (ou `Treats-Site-Id`) n'est pas un UUID."
              }
            ]
          },
          "404": {
            "description": "SITE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "SITE_NOT_FOUND",
                "description": "Le site demandé est inconnu, d'une autre organisation, ou non ouvert à l'application."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/waiting-room?site_id=84b9ad0c-5917-4d16-a75c-d3dc97abe595"
          },
          "response": {
            "body": {
              "items": [
                {
                  "appointmentId": "27dcb082-4847-4221-89a3-8ec172203eb1",
                  "appointmentTypeId": "d2982217-f30f-4d2e-bec2-4bda58da3ca0",
                  "arrivedAt": "2026-10-15T02:04:19.865132Z",
                  "clientId": "79a787b6-2151-4b68-ba30-7969038773f5",
                  "clientName": "Margot Martinez",
                  "patientId": "79bc79a5-74b8-472d-a467-e9c1ac2c77c2",
                  "patientName": "Bugs",
                  "startAt": "2027-06-21T12:00:00Z",
                  "urgent": false,
                  "vetId": "bf5c3a90-ea0b-4483-832b-d9fe907fa99e",
                  "vetName": "Claire Moreau",
                  "waitingMinutes": 0
                }
              ],
              "nextCursor": null,
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "totalWaiting": 1
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/webhooks/deliveries": {
      "get": {
        "tags": [
          "Notifications"
        ],
        "summary": "Lister les livraisons",
        "description": "Rend les livraisons de la connexion du jeton (cette organisation, cette application, cette autorisation), pings compris, triées par date de dernier changement puis identifiant, de la plus ancienne à la plus récente. Après une déconnexion puis une reconnexion de l'organisation, les livraisons de l'ancienne connexion ne sont plus listées. Les livraisons terminées (`DELIVERED`, `DEAD`, `MUTED`, `CANCELLED`) sont supprimées 30 jours après leur production. Le corps envoyé n'est pas rendu ; `data` le résume.",
        "operationId": "GET_api_partner_v1_webhooks_deliveries",
        "security": [
          {
            "partner": [
              "webhooks"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "webhooks"
          ]
        },
        "x-treats-controller": "WebhooksController",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Ne garder que les livraisons dans cet état (casse indifférente).",
            "schema": {
              "type": "string",
              "enum": [
                "PENDING",
                "SENDING",
                "DELIVERED",
                "FAILED",
                "DEAD",
                "MUTED",
                "CANCELLED"
              ]
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "description": "Ne garder que les livraisons changées à cet instant ou après.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Curseur opaque rendu par la page précédente (`nextCursor`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Taille de la page. Au-delà de 200, ramené à 200.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page de livraisons.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookDelivery"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "INVALID_FIELD",
                "description": "`status` hors de la liste, `updated_since` illisible, `limit` non entier ou inférieur à 1, curseur illisible (un détail par paramètre)."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/webhooks/deliveries?status=MUTED"
          },
          "response": {
            "body": {
              "items": [
                {
                  "attempts": 0,
                  "createdAt": "2026-10-15T02:11:30.677067Z",
                  "data": {
                    "id": "b20e070e-d235-4a5b-8bb4-491ce38ff5eb",
                    "object": "client",
                    "publicId": "cli_sr5xxrz59"
                  },
                  "deliveredAt": null,
                  "id": "506b3690-f8d7-454f-b912-723e5b2a78ab",
                  "lastAttemptAt": null,
                  "lastError": "ORGANIZATION_MUTED : organisation en sourdine, rien n'est envoyé à une application LIVE.",
                  "lastStatusCode": null,
                  "nextAttemptAt": null,
                  "occurredAt": "2026-10-15T02:11:27.598642Z",
                  "siteId": "61b796e2-921f-428b-b9aa-30c684b4ed91",
                  "status": "MUTED",
                  "type": "client.created",
                  "updatedAt": "2026-10-15T02:11:30.677067Z"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/webhooks/deliveries/{id}/replay": {
      "post": {
        "tags": [
          "Notifications"
        ],
        "summary": "Rejouer une livraison",
        "description": "Remet une livraison de la connexion du jeton en file, quel que soit son état (sauf pendant un envoi) : elle repart dans les secondes qui suivent, avec le même identifiant et le même corps, et ses tentatives repartent de zéro (six tentatives de nouveau, en-tête `Treats-Attempt` à partir de 1). Les droits, le site, l'abonnement et l'endpoint sont revérifiés à l'envoi : une livraison dont la cause d'annulation demeure est de nouveau annulée. Ne marche que pour une livraison encore conservée (30 jours après sa production une fois terminée).",
        "operationId": "POST_api_partner_v1_webhooks_deliveries_id_replay",
        "security": [
          {
            "partner": [
              "webhooks"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "webhooks"
          ]
        },
        "x-treats-controller": "WebhooksController",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Identifiant de la livraison (`id` de `GET /webhooks/deliveries`, égal à `Treats-Event-Id`).",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "responses": {
          "202": {
            "description": "Livraison remise en file : `status` `PENDING`, `attempts` 0.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                }
              }
            }
          },
          "404": {
            "description": "WEBHOOK_DELIVERY_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "WEBHOOK_DELIVERY_NOT_FOUND",
                "description": "Aucune livraison de cet identifiant pour la connexion du jeton (autre organisation, autre application, ancienne connexion, ou livraison supprimée)."
              }
            ]
          },
          "409": {
            "description": "WEBHOOK_DELIVERY_IN_PROGRESS",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "WEBHOOK_DELIVERY_IN_PROGRESS",
                "description": "La livraison est en cours d'envoi : réessayer dans un instant."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "POST",
            "url": "/api/partner/v1/webhooks/deliveries/3e35752b-dbf2-48d9-8724-bda42ee576dc/replay"
          },
          "response": {
            "body": {
              "attempts": 0,
              "createdAt": "2026-10-15T02:10:35.497594Z",
              "data": {
                "id": "2b9a08d9-0bd2-4fba-ad7b-170162485864",
                "object": "client",
                "publicId": "cli_9syv469me"
              },
              "deliveredAt": null,
              "id": "3e35752b-dbf2-48d9-8724-bda42ee576dc",
              "lastAttemptAt": "2026-10-15T02:11:01.549016Z",
              "lastError": null,
              "lastStatusCode": 500,
              "nextAttemptAt": "2026-10-15T02:11:02.152357Z",
              "occurredAt": "2026-10-15T02:10:33.801455Z",
              "siteId": "84b9ad0c-5917-4d16-a75c-d3dc97abe595",
              "status": "PENDING",
              "type": "client.created",
              "updatedAt": "2026-10-15T02:11:02.152357Z"
            },
            "status": 202
          }
        }
      }
    },
    "/api/partner/v1/webhooks/endpoint": {
      "get": {
        "tags": [
          "Notifications"
        ],
        "summary": "Lire l'endpoint de l'application",
        "description": "Rend l'endpoint de l'application : adresse, types abonnés, état (actif ou désactivé après des échecs), série d'échecs en cours et fin de recouvrement d'une rotation. L'endpoint appartient à l'application, pas à l'organisation : tout jeton de l'application qui porte `webhooks` lit le même. Le secret n'est jamais rendu ici.",
        "operationId": "GET_api_partner_v1_webhooks_endpoint",
        "security": [
          {
            "partner": [
              "webhooks"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "webhooks"
          ]
        },
        "x-treats-controller": "WebhooksController",
        "responses": {
          "200": {
            "description": "L'endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "404": {
            "description": "WEBHOOK_ENDPOINT_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "WEBHOOK_ENDPOINT_NOT_FOUND",
                "description": "Aucun endpoint enregistré : en créer un par `PUT /webhooks/endpoint`."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/webhooks/endpoint"
          },
          "response": {
            "body": {
              "active": false,
              "createdAt": "2026-10-15T02:09:24.773700Z",
              "disabledAt": "2026-10-15T02:12:31.505434Z",
              "disabledReason": "Désactivé le 2026-10-15T02:12:31.505434Z après des échecs continus depuis le 2026-10-11T02:12:27.549346Z (dernier : HTTP 503).",
              "failingSince": "2026-10-11T02:12:27.549346Z",
              "id": "adfd37fa-b44e-4a1b-bb39-6e2d8c01a9f5",
              "previousSecretValidUntil": null,
              "types": [
                "appointment.created",
                "appointment.updated"
              ],
              "updatedAt": "2026-10-15T02:12:31.505434Z",
              "url": "http://127.0.0.1:8145/fail/A"
            },
            "status": 200
          }
        }
      },
      "put": {
        "tags": [
          "Notifications"
        ],
        "summary": "Créer ou modifier l'endpoint de l'application",
        "description": "Crée l'endpoint de l'application (201, avec le secret de signature rendu une seule fois) ou modifie celui qui existe (200, sans secret). Une modification remplace l'adresse et les types, ne touche pas au secret (voir `POST /webhooks/endpoint/rotate-secret`), réactive un endpoint désactivé et remet à zéro sa série d'échecs. L'adresse doit être en `https://`, sans identifiants ni fragment, 2 048 caractères au plus, et chacune des adresses IP vers lesquelles son nom se résout doit être publique (adresses privées, locales, réservées et de documentation refusées). Elle est revérifiée avant chaque envoi.\n\nPas d'`Idempotency-Key` (PUT). Deux premières créations simultanées donnent un seul 201 avec secret ; l'autre rend 200 sans secret. L'endpoint vaut pour toutes les organisations connectées ; il ne reçoit que les événements produits après son enregistrement.",
        "operationId": "PUT_api_partner_v1_webhooks_endpoint",
        "security": [
          {
            "partner": [
              "webhooks"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "webhooks"
          ]
        },
        "x-treats-controller": "WebhooksController",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "Adresse HTTPS publique qui recevra les `POST`. Le chemin et les paramètres sont libres.",
                    "maxLength": 2048
                  },
                  "types": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Types à recevoir (valeurs de `GET /webhooks/event-types`). Absent ou `null` : tous les types, y compris ceux ajoutés plus tard. `[\"*\"]` : tous les types. Liste vide refusée ; `ping` refusé (ce n'est pas un abonnement)."
                  }
                },
                "required": [
                  "url"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Endpoint modifié : `secret` à null, `created` à false.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointConfigured"
                }
              }
            }
          },
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointConfigured"
                }
              }
            },
            "description": "Endpoint créé : `secret` rendu une seule fois, `created` à true."
          },
          "400": {
            "description": "WEBHOOK_URL_INVALID · WEBHOOK_URL_FORBIDDEN · WEBHOOK_URL_UNRESOLVABLE · INVALID_FIELD",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "WEBHOOK_URL_INVALID",
                "description": "Adresse illisible, trop longue, sans hôte, autre que `https://`, avec identifiants (`user:pass@`) ou fragment (`#…`)."
              },
              {
                "code": "WEBHOOK_URL_FORBIDDEN",
                "description": "L'hôte est local (`localhost`…) ou l'une de ses adresses IP est privée, locale, réservée ou de documentation."
              },
              {
                "code": "WEBHOOK_URL_UNRESOLVABLE",
                "description": "Le nom de l'hôte ne se résout pas."
              },
              {
                "code": "INVALID_FIELD",
                "description": "`url` absent ou vide ; `types` qui n'est pas une liste de textes, liste vide, type inconnu ou `ping` (un détail par erreur)."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "body": {
              "url": "http://127.0.0.1:8145/hooks/B"
            },
            "method": "PUT",
            "url": "/api/partner/v1/webhooks/endpoint"
          },
          "response": {
            "body": {
              "created": true,
              "endpoint": {
                "active": true,
                "createdAt": "2026-10-15T02:09:32.017629Z",
                "disabledAt": null,
                "disabledReason": null,
                "failingSince": null,
                "id": "049d3d02-7bae-474c-a040-68b836921373",
                "previousSecretValidUntil": null,
                "types": [
                  "appointment.created",
                  "appointment.updated",
                  "appointment.cancelled"
                ],
                "updatedAt": "2026-10-15T02:09:32.017629Z",
                "url": "http://127.0.0.1:8145/hooks/B"
              },
              "secret": "…"
            },
            "status": 201
          }
        }
      }
    },
    "/api/partner/v1/webhooks/endpoint/rotate-secret": {
      "post": {
        "tags": [
          "Notifications"
        ],
        "summary": "Changer le secret de signature",
        "description": "Génère un nouveau secret et le rend une seule fois. L'ancien secret signe encore pendant 24 heures : chaque envoi porte alors deux signatures `v1`, la nouvelle d'abord, ce qui laisse le temps de déployer le nouveau secret sans coupure. Une seconde rotation dans ces 24 heures remplace l'ancien par le secret en place à ce moment : seuls les deux derniers secrets signent. La rotation ne change ni l'adresse, ni les types, et ne réactive pas un endpoint désactivé. Aucun corps n'est lu.\n\nLa même `Idempotency-Key` rejoue la réponse avec `secret: null` et `secretRedacted: true` : le secret n'est jamais conservé en clair. Si la réponse a été perdue, refaites une rotation avec une nouvelle clé.",
        "operationId": "POST_api_partner_v1_webhooks_endpoint_rotate_secret",
        "security": [
          {
            "partner": [
              "webhooks"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "webhooks"
          ]
        },
        "x-treats-controller": "WebhooksController",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "responses": {
          "200": {
            "description": "Le nouveau secret et la fin de validité de l'ancien.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSecret"
                }
              }
            }
          },
          "404": {
            "description": "WEBHOOK_ENDPOINT_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "WEBHOOK_ENDPOINT_NOT_FOUND",
                "description": "Aucun endpoint enregistré."
              }
            ]
          }
        },
        "x-treats-example": {
          "request": {
            "method": "POST",
            "url": "/api/partner/v1/webhooks/endpoint/rotate-secret"
          },
          "response": {
            "body": {
              "previousSecretValidUntil": "2026-10-16T02:11:03.264023Z",
              "secret": "…"
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/webhooks/event-types": {
      "get": {
        "tags": [
          "Notifications"
        ],
        "summary": "Lister les types d'événement",
        "description": "Rend les 17 types d'événement auxquels un endpoint peut s'abonner, avec la ressource que chacun désigne et les droits qui permettent de le recevoir. La liste est la même pour toutes les applications : elle ne dit pas ce que votre connexion reçoit (comparez `scopes` à ceux de `GET /me`). Une seule page, `nextCursor` toujours `null`. Le type `ping` n'y figure pas : il n'est produit que par `POST /webhooks/test`.",
        "operationId": "GET_api_partner_v1_webhooks_event_types",
        "security": [
          {
            "partner": [
              "webhooks"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "webhooks"
          ]
        },
        "x-treats-controller": "WebhooksController",
        "responses": {
          "200": {
            "description": "Les types publiés, dans un ordre fixe.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "nextCursor"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookEventType"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "Curseur opaque de la page suivante, à repasser tel quel dans `cursor`. `null` : dernière page."
                    }
                  }
                }
              }
            }
          }
        },
        "x-treats-example": {
          "request": {
            "method": "GET",
            "url": "/api/partner/v1/webhooks/event-types"
          },
          "response": {
            "body": {
              "items": [
                {
                  "object": "appointment",
                  "scopes": [
                    "agenda:read"
                  ],
                  "type": "appointment.created"
                },
                {
                  "object": "appointment",
                  "scopes": [
                    "agenda:read"
                  ],
                  "type": "appointment.updated"
                }
              ],
              "nextCursor": null
            },
            "status": 200
          }
        }
      }
    },
    "/api/partner/v1/webhooks/test": {
      "post": {
        "tags": [
          "Notifications"
        ],
        "summary": "Envoyer un ping à l'endpoint",
        "description": "Envoie tout de suite un événement `ping`, signé comme les autres, à l'endpoint de l'application, pour l'organisation du jeton, et rend son verdict. Délai total de 5 secondes pour votre endpoint ; la réponse arrive en 7 secondes au plus. Pas de reprise : un échec rend `DEAD` avec `lastStatusCode` ou `lastError`. Aucun corps n'est lu.\n\nLes règles d'envoi s'appliquent : avec une organisation de test, une application de production obtient `MUTED` (rien n'est envoyé) ; une application de recette n'envoie qu'à une organisation de test, sinon `CANCELLED` (`SANDBOX_ORG_LIVE`). Un seul ping à la fois par application. La même `Idempotency-Key` rejoue la première réponse sans renvoyer de ping. Dans un cas extrême (verdict pas encore écrit), le statut rendu est `SENDING` et le verdict se lit dans `GET /webhooks/deliveries`.",
        "operationId": "POST_api_partner_v1_webhooks_test",
        "security": [
          {
            "partner": [
              "webhooks"
            ]
          }
        ],
        "x-treats-access": {
          "kind": "partner",
          "scopes": [
            "webhooks"
          ]
        },
        "x-treats-controller": "WebhooksController",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Clé unique de l'envoi (un UUID). Le même envoi répété avec la même clé n'est appliqué qu'une fois pendant 24 h."
          }
        ],
        "responses": {
          "200": {
            "description": "La livraison du ping et son verdict (`DELIVERED`, `DEAD`, `MUTED`, `CANCELLED`, exceptionnellement `SENDING`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                }
              }
            }
          },
          "404": {
            "description": "WEBHOOK_ENDPOINT_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "WEBHOOK_ENDPOINT_NOT_FOUND",
                "description": "Aucun endpoint enregistré."
              }
            ]
          },
          "409": {
            "description": "WEBHOOK_ENDPOINT_DISABLED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "WEBHOOK_ENDPOINT_DISABLED",
                "description": "Endpoint désactivé après des échecs continus : le réactiver par `PUT /webhooks/endpoint`."
              }
            ]
          },
          "503": {
            "description": "WEBHOOK_TEST_IN_PROGRESS · WEBHOOK_TEST_BUSY",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-treats-error-codes": [
              {
                "code": "WEBHOOK_TEST_IN_PROGRESS",
                "description": "Un ping de l'application est déjà en cours. En-tête `Retry-After: 5`. Rien n'est écrit : réessayer avec la même `Idempotency-Key`."
              },
              {
                "code": "WEBHOOK_TEST_BUSY",
                "description": "Trop de pings en cours chez Treats. En-tête `Retry-After: 5`. Rien n'est écrit : réessayer avec la même `Idempotency-Key`."
              }
            ]
          }
        },
        "x-treats-webhooks": [
          "ping"
        ],
        "x-treats-example": {
          "request": {
            "method": "POST",
            "url": "/api/partner/v1/webhooks/test"
          },
          "response": {
            "body": {
              "attempts": 0,
              "createdAt": "2026-10-15T02:11:34.719142Z",
              "data": {
                "id": null,
                "object": "ping",
                "publicId": null
              },
              "deliveredAt": null,
              "id": "a01bccd1-1a15-45a5-8417-9308665ed80a",
              "lastAttemptAt": null,
              "lastError": "ORGANIZATION_MUTED : organisation en sourdine, rien n'est envoyé à une application LIVE.",
              "lastStatusCode": null,
              "nextAttemptAt": null,
              "occurredAt": "2026-10-15T02:11:34.715089Z",
              "siteId": null,
              "status": "MUTED",
              "type": "ping",
              "updatedAt": "2026-10-15T02:11:34.723329Z"
            },
            "status": 200
          }
        }
      }
    }
  },
  "x-treats-stats": {
    "operations": 84,
    "paths": 72,
    "scopes": [
      "agenda:read",
      "agenda:write",
      "billing:read",
      "catalog:read",
      "clients:read",
      "clients:write",
      "consultations:notes",
      "consultations:read",
      "consultations:write",
      "documents:read",
      "documents:write",
      "imaging:write",
      "lab:write",
      "org:read",
      "patients:read",
      "patients:write",
      "prescriptions:read",
      "webhooks"
    ],
    "examples": 82
  }
}