{
  "openapi": "3.0.3",
  "info": {
    "title": "API turf.bzh",
    "version": "1.0.0",
    "description": "Acces programmatique aux donnees hippiques turf.bzh (programme, partants exacts, cotes PMU en direct, arrivees, indicateurs premium) et a ChatBZH. Reservee aux abonnes actifs titulaires de la Licence API (paiement unique). Une cle, deux usages : les endpoints DONNEES sont illimites (rate limit technique 60/min, 10 000/jour) ; POST /chat consomme les credits IA du compte. Aucune reponse ne garantit un gain : jeu responsable. Depuis le 03/08/2026 : une journee entiere de partants en un appel (/v1/journees/{date}/partants), des exports mensuels de la base entiere (/v1/exports), et un dictionnaire des champs avec leur taux de remplissage reel (/v1/schema).",
    "termsOfService": "https://www.turf.bzh/cgv.php",
    "contact": {
      "url": "https://www.turf.bzh/api-docs.php"
    }
  },
  "servers": [
    {
      "url": "https://www.turf.bzh/api"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Cle personnelle tbz_live_... a generer sur https://www.turf.bzh/api-cle.php"
      }
    },
    "parameters": {
      "date": {
        "name": "date",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "date"
        },
        "example": "2026-07-03"
      },
      "rc": {
        "name": "rc",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^R\\d{1,2}C\\d{1,2}$"
        },
        "example": "R1C4"
      },
      "formatCsv": {
        "name": "format",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "csv"
          ]
        },
        "description": "Sortie CSV (UTF-8 BOM, separateur ;) pour tableurs"
      }
    },
    "schemas": {
      "Envelope": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "description": "Charge utile de l'endpoint"
          },
          "meta": {
            "type": "object",
            "properties": {
              "generated_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "example": "subscription_required"
              },
              "message": {
                "type": "string"
              },
              "status": {
                "type": "integer"
              },
              "doc": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "responses": {
      "OK": {
        "description": "Succes",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Envelope"
            }
          }
        }
      },
      "Err": {
        "description": "Erreur (401 cle, 402 credits, 403 licence/abonnement, 404, 422 parametres, 429 rate limit, 5xx)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/v1/me": {
      "get": {
        "summary": "Etat du compte (licence, abonnement, cle, credits IA, usage API)",
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/statut": {
      "get": {
        "summary": "Fraicheur de la base (healthcheck) : fresh / stale / empty",
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/programme": {
      "get": {
        "summary": "Programme du jour (reunions, courses, heures, Quinte+)",
        "parameters": [
          {
            "name": "filter",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "upcoming",
                "completed"
              ],
              "default": "all"
            }
          },
          {
            "$ref": "#/components/parameters/formatCsv"
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Mettre csv pour une sortie tableur : une ligne par course du programme.",
            "schema": {
              "type": "string",
              "example": "csv"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/quinte": {
      "get": {
        "summary": "La course du Quinte+ du jour (source officielle turf.bzh)",
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/resultats": {
      "get": {
        "summary": "Arrivees et rapports des courses terminees d'une journee (aujourd'hui ou une date passee)",
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "description": "Journee servie. Defaut : aujourd'hui. Toute date passee presente en base est acceptee ; une date future renvoie 422.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "code_course",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^R\\d{1,2}C\\d{1,2}$"
            }
          },
          {
            "name": "hippodrome",
            "in": "query",
            "description": "Filtre sur le nom de l'hippodrome, recherche partielle et insensible a la casse (ex: enghien). 40 caracteres maximum.",
            "schema": {
              "type": "string",
              "maxLength": 40
            }
          },
          {
            "name": "arrivee",
            "in": "query",
            "description": "complete (defaut) = ordre d'arrivee entier ; top5 = les cinq premiers seulement",
            "schema": {
              "type": "string",
              "enum": [
                "complete",
                "top5"
              ],
              "default": "complete"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/courses/{date}/{code_course}/rapports": {
      "get": {
        "summary": "Rapports definitifs collectes d'une course, les deux masses PMU",
        "description": "Lit les rapports collectes chaque nuit depuis le 1er janvier, et non le flux PMU en direct : tout l'historique est donc disponible, la ou /arrivee ne sert que la journee en cours. Chaque ligne porte DEUX montants : rapport_pour_1_euro, ce que le PMU publie, et rapport_pour_la_mise_de_base, ce qu'un ticket encaisse reellement. La mise de base n'est pas la meme partout (1 EUR au simple gagnant en ligne, 2 EUR au point de vente, 3 EUR au Multi et au 2 sur 4) : servir un seul des deux obligerait chaque consommateur a refaire la multiplication.",
        "parameters": [
          {
            "name": "date",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "code_course",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^R\\d{1,2}C\\d{1,2}$"
            }
          },
          {
            "name": "masse",
            "in": "query",
            "description": "Masse PMU servie. Le flux nu rend le point de vente, ?specialisation=INTERNET la masse en ligne (types prefixes E_). Elles ne paient pas pareil : releve du 20/07/2026, couple place 15-7 a 17,80 EUR au point de vente contre 30,10 EUR en ligne. Sans ce parametre, les deux sont servies.",
            "schema": {
              "type": "string",
              "enum": [
                "en_ligne",
                "point_de_vente"
              ]
            }
          },
          {
            "name": "pari",
            "in": "query",
            "description": "Filtre sur le type de pari, sans distinction de masse : TRIO retient aussi E_TRIO. Exemples : SIMPLE_GAGNANT, COUPLE_PLACE, DEUX_SUR_QUATRE, TRIO, TIERCE, QUARTE_PLUS, QUINTE_PLUS, MULTI, MINI_MULTI.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "payants",
            "in": "query",
            "description": "1 = ne garder que les rapports qui ont paye. Par defaut TOUT ce que le PMU a publie est servi, y compris les rapports a zero : ils prouvent que la formule etait proposee et que personne ne l'a trouvee, ce qui n'est pas une absence.",
            "schema": {
              "type": "string",
              "enum": [
                "0",
                "1"
              ]
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "csv pour un tableau (UTF-8 BOM, separateur point-virgule).",
            "schema": {
              "type": "string",
              "enum": [
                "csv"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/rapports": {
      "get": {
        "summary": "Rapports definitifs collectes d'une journee entiere",
        "description": "Meme source et memes champs que la route par course, sur toute une journee : environ 340 rapports sur 40 courses. Le meta porte nb_courses, paris_presents et les bornes de la collecte.",
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "description": "Defaut : aujourd'hui. Une date future renvoie 422.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "code_course",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^R\\d{1,2}C\\d{1,2}$"
            }
          },
          {
            "name": "masse",
            "in": "query",
            "description": "Masse PMU servie. Le flux nu rend le point de vente, ?specialisation=INTERNET la masse en ligne (types prefixes E_). Elles ne paient pas pareil : releve du 20/07/2026, couple place 15-7 a 17,80 EUR au point de vente contre 30,10 EUR en ligne. Sans ce parametre, les deux sont servies.",
            "schema": {
              "type": "string",
              "enum": [
                "en_ligne",
                "point_de_vente"
              ]
            }
          },
          {
            "name": "pari",
            "in": "query",
            "description": "Filtre sur le type de pari, sans distinction de masse : TRIO retient aussi E_TRIO. Exemples : SIMPLE_GAGNANT, COUPLE_PLACE, DEUX_SUR_QUATRE, TRIO, TIERCE, QUARTE_PLUS, QUINTE_PLUS, MULTI, MINI_MULTI.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "payants",
            "in": "query",
            "description": "1 = ne garder que les rapports qui ont paye. Par defaut TOUT ce que le PMU a publie est servi, y compris les rapports a zero : ils prouvent que la formule etait proposee et que personne ne l'a trouvee, ce qui n'est pas une absence.",
            "schema": {
              "type": "string",
              "enum": [
                "0",
                "1"
              ]
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "csv pour un tableau (UTF-8 BOM, separateur point-virgule).",
            "schema": {
              "type": "string",
              "enum": [
                "csv"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/performances": {
      "get": {
        "summary": "Taux de couverture mesure de nos selections, et palmares",
        "description": "Le taux de couverture est la part des courses ou la combinaison gagnante d'un pari etait entierement contenue dans nos N premiers chevaux. Il est mesure sur TOUTES les courses de la periode ou le PMU a publie cette formule, jamais sur une selection des meilleures. Chaque ligne porte le nombre de combinaisons a jouer (tickets) et leur cout (engage_eur) : un taux sans son prix ne veut rien dire. Ce n'est ni un rendement ni une promesse ; le meta porte l'avertissement, qui n'est pas decoratif. Meme source que la page /meilleures-performances.php.",
        "parameters": [
          {
            "name": "fenetre",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "7j",
                "14j",
                "30j",
                "90j",
                "annee"
              ],
              "default": "30j"
            }
          },
          {
            "name": "famille",
            "in": "query",
            "description": "Filtre : SIMPLE_GAGNANT, COUPLE_GAGNANT, COUPLE_PLACE, DEUX_SUR_QUATRE, TRIO, TIERCE, QUARTE_PLUS, QUINTE_PLUS, MULTI, MINI_MULTI.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "palmares",
            "in": "query",
            "description": "1 pour joindre les plus gros rapports couverts. Absent par defaut : la mesure pese moins que la vitrine.",
            "schema": {
              "type": "string",
              "enum": [
                "0",
                "1"
              ]
            }
          },
          {
            "name": "n",
            "in": "query",
            "description": "Lignes de palmares, 1 a 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "csv pour un tableau (UTF-8 BOM, separateur point-virgule).",
            "schema": {
              "type": "string",
              "enum": [
                "csv"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/journees/{date}/partants": {
      "get": {
        "summary": "Tous les partants d'une journee, en un seul appel",
        "description": "Sert la journee entiere : tous les partants de toutes les courses, avec la cote de depart, les indicateurs et le rang d'arrivee. C'est l'endpoint des etudes sur plusieurs jours. Reconstituer la base course par course demande environ 28 000 appels, soit trois jours compte tenu du plafond de 10 000 appels quotidiens ; par journee, 548 appels et une dizaine de minutes. Les plages de dates sont refusees : bouclez sur les dates, ou prenez /v1/exports. La cote est vide pour la journee en cours, et pour la toute derniere journee de la base : elle arrive au rafraichissement du lendemain matin. /v1/statut donne la derniere date exploitable.",
        "tags": [
          "Donnees"
        ],
        "parameters": [
          {
            "name": "date",
            "in": "path",
            "required": true,
            "description": "Jour demande, au format YYYY-MM-DD.",
            "schema": {
              "type": "string",
              "example": "2026-08-01"
            }
          },
          {
            "name": "discipline",
            "in": "query",
            "required": false,
            "description": "Filtre : plat, trot attele, trot monte, obstacle, ou le code d'une lettre (P, A, M, H, S, C).",
            "schema": {
              "type": "string",
              "example": "plat"
            }
          },
          {
            "name": "hippodrome",
            "in": "query",
            "required": false,
            "description": "Filtre partiel, insensible a la casse.",
            "schema": {
              "type": "string",
              "example": "Vincennes"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Mettre csv pour une sortie tableur ; memes colonnes d'une journee a l'autre.",
            "schema": {
              "type": "string",
              "example": "csv"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Succes"
          },
          "422": {
            "description": "Parametre invalide"
          },
          "429": {
            "description": "Limite technique atteinte"
          }
        }
      }
    },
    "/v1/exports": {
      "get": {
        "summary": "Liste des exports mensuels",
        "description": "La base entiere, en CSV compresse, un fichier par mois, avec taille, nombre de lignes et empreinte SHA-256. Mesure exacte : 83,8 Mo en CSV, 28,4 Mo compresses. Le mois en cours porte partiel: true, il est incomplet et sa derniere journee n'a pas encore ses cotes de depart. A preferer a toute extraction massive.",
        "tags": [
          "Donnees"
        ],
        "responses": {
          "200": {
            "description": "Succes"
          },
          "422": {
            "description": "Parametre invalide"
          },
          "429": {
            "description": "Limite technique atteinte"
          }
        }
      }
    },
    "/v1/exports/{fichier}": {
      "get": {
        "summary": "Telechargement d'un export mensuel",
        "description": "Rend le fichier gzip. Le nom doit avoir la forme partants-AAAA-MM.csv.gz ; la liste est sur /v1/exports. Le telechargement passe par l'API et reste donc soumis a la cle.",
        "tags": [
          "Donnees"
        ],
        "parameters": [
          {
            "name": "fichier",
            "in": "path",
            "required": true,
            "description": "Nom du fichier, tel que donne par /v1/exports.",
            "schema": {
              "type": "string",
              "example": "partants-2026-07.csv.gz"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Succes"
          },
          "422": {
            "description": "Parametre invalide"
          },
          "429": {
            "description": "Limite technique atteinte"
          }
        }
      }
    },
    "/v1/schema": {
      "get": {
        "summary": "Dictionnaire des champs et taux de remplissage",
        "description": "Pour chaque champ servi : le nom expose, le nom en base, le type, l'unite, une description, et le taux de remplissage reel mesure sur toute la table. Sur 80 colonnes, 38 sont renseignees a 100 % et 14 le sont a moins de 65 % : Rapport_SG a 8,5 %, Cote_BZH a 50,3 %. Cinq champs changent de nom entre la base et l'API : Course devient code_course, Numero devient num, Cheval devient name, Driver devient jockey_driver, Entraineur devient trainer. A lire avant de batir une hypothese.",
        "tags": [
          "Compte"
        ],
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Mettre csv pour une sortie tableur.",
            "schema": {
              "type": "string",
              "example": "csv"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Succes"
          },
          "422": {
            "description": "Parametre invalide"
          },
          "429": {
            "description": "Limite technique atteinte"
          }
        }
      }
    },
    "/v1/courses/{date}/{rc}": {
      "get": {
        "summary": "Fiche course + partants exacts (fonctionne sur tout l'historique)",
        "parameters": [
          {
            "$ref": "#/components/parameters/date"
          },
          {
            "$ref": "#/components/parameters/rc"
          },
          {
            "$ref": "#/components/parameters/formatCsv"
          },
          {
            "name": "hippodrome",
            "in": "query",
            "required": false,
            "description": "Dix courses de l'historique, reparties sur trois journees (2025-11-15, 2025-11-16, 2026-07-17), portent le meme code RxCy sous deux hippodromes, soit 0,036 % des codes. Sans ce parametre la reponse melange les deux pelotons et porte ambigu: true, avec le nom de l'hippodrome dont le peloton est complet.",
            "schema": {
              "type": "string",
              "example": "Dieppe"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Mettre csv pour une sortie tableur : une ligne par partant. Refuse en 422 sur une course ambigue (deux reunions au meme code) : precisez hippodrome, un tableau ne peut pas porter l'avertissement.",
            "schema": {
              "type": "string",
              "example": "csv"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/courses/{date}/{rc}/cotes": {
      "get": {
        "summary": "Cotes PMU en direct, ecart depuis le matin et sur quelques minutes",
        "description": "Pour chaque partant : cote (temps reel), cote_ref (cote de reference de la matinee), evolution_ouverture / evolution_ouverture_pct / sens_ouverture (ecart avec la cote du matin, le seul recul qui couvre la journee entiere), cote_5min_ago / evolution_5min (ecart avec une mesure plus ancienne) et comparaison_age_sec qui donne l'age REEL de cette mesure en secondes. Le champ se nomme cote_5min_ago pour raison de compatibilite, mais l'ecart peut porter sur plus de 5 minutes : lisez comparaison_age_sec avant d'annoncer une duree. Ces deux champs valent null tant qu'aucune mesure assez ancienne n'existe pour la course. tendance et tendance_pct viennent du PMU et mesurent l'ecart depuis son dernier rapport, dont la date n'est pas publiee : ne leur attribuez aucune duree.",
        "parameters": [
          {
            "$ref": "#/components/parameters/date"
          },
          {
            "$ref": "#/components/parameters/rc"
          },
          {
            "$ref": "#/components/parameters/formatCsv"
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Mettre csv pour une sortie tableur : une ligne par partant, cote du moment comprise.",
            "schema": {
              "type": "string",
              "example": "csv"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/courses/{date}/{rc}/cotes/mouvements": {
      "get": {
        "summary": "Plus gros mouvements de cote sur une fenetre glissante",
        "description": "Chaque ligne porte cote, cote_avant, delta, delta_pct et sens, plus cote_ouverture / delta_ouverture / delta_ouverture_pct pour le recul depuis le matin. La reponse contient fenetre_reelle_sec : la duree effectivement couverte, qui peut depasser le parametre fenetre si les mesures intermediaires manquent. Une cote qui baisse signale de l'argent qui rentre sur le cheval, ce n'est pas une prevision.",
        "parameters": [
          {
            "$ref": "#/components/parameters/date"
          },
          {
            "$ref": "#/components/parameters/rc"
          },
          {
            "name": "fenetre",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 2,
              "maximum": 60,
              "default": 5
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 8,
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/courses/{date}/{rc}/cotes/historique": {
      "get": {
        "summary": "Serie complete des cotes de la course, du matin au depart",
        "description": "La courbe, la ou /cotes ne donne que l'instantane et /cotes/mouvements une fenetre de deux heures. Chaque entree de mesures porte t (horodatage unix), heure (Europe/Paris), cotes (cote du moment par numero) et cotes_reference (cote de reference du matin). Un numero absent d'un releve n'avait pas de cote a cet instant : non-partant, ou marche pas encore ouvert (le PMU renvoie 0 dans ce cas, nous ne le publions pas comme une cote car il fausserait toute moyenne ; la cellule CSV est vide). Un releve ou aucun numero n'avait de cote n'apparait pas. Le flux PMU ne publie pas de serie : celle-ci est collectee par turf.bzh depuis le 27/07/2026, une course anterieure repond 404 et le passe n'est pas rattrapable. La cadence se resserre a l'approche du depart (30 min au-dela de 2 h, 10 min entre 2 h et 30 min, 5 min ensuite) mais ne peut pas etre plus fine que la frequence d'appel du collecteur, aujourd'hui de 5 minutes : les dernieres minutes avant le depart sont couvertes par un a deux releves. En format=csv la sortie est en forme longue, une ligne par mesure et par partant, colonnes date;code_course;t;heure;num;cote;cote_ref.",
        "parameters": [
          {
            "$ref": "#/components/parameters/date"
          },
          {
            "$ref": "#/components/parameters/rc"
          },
          {
            "$ref": "#/components/parameters/formatCsv"
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Mettre csv pour une sortie tableur, en forme longue : une ligne par releve et par partant.",
            "schema": {
              "type": "string",
              "example": "csv"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/courses/{date}/{rc}/arrivee": {
      "get": {
        "summary": "Arrivee + rapports (quasi temps reel le jour J)",
        "parameters": [
          {
            "$ref": "#/components/parameters/date"
          },
          {
            "$ref": "#/components/parameters/rc"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/courses/{date}/{rc}/indicateurs": {
      "get": {
        "summary": "Features calculees par partant (ELO trends, forme, affinites, synergie, IMDC...)",
        "parameters": [
          {
            "$ref": "#/components/parameters/date"
          },
          {
            "$ref": "#/components/parameters/rc"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/courses/{date}/{rc}/analyse": {
      "get": {
        "summary": "Classement multi-features (rank_partants) + LigneBZH du jour",
        "parameters": [
          {
            "$ref": "#/components/parameters/date"
          },
          {
            "$ref": "#/components/parameters/rc"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/courses/{date}/{rc}/ecarts": {
      "get": {
        "summary": "Ecarts de victoires/places (par cheval et par numero)",
        "parameters": [
          {
            "$ref": "#/components/parameters/date"
          },
          {
            "$ref": "#/components/parameters/rc"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/courses/{date}/{rc}/renifleur": {
      "get": {
        "summary": "Carnet de notes du Renifleur sur une course",
        "description": "Deux blocs. carnet_de_course : le resume redige par Le Renifleur apres l'arrivee de cette course et le cheval retenu pour la prochaine fois, null si la course n'a pas encore ete debriefee. deja_reperes : les partants engages qui avaient ete reperes a leur derniere sortie, avec la note d'alors, sa date, l'hippodrome et le numero porte ce jour-la. Contenu editorial exclusif turf.bzh, ce n'est PAS un signal de pari. Mesure sur 2 411 rentrees : les chevaux reperes terminent dans les trois premiers 35,9 % du temps contre 27,8 % pour l'ensemble des partants, mais a cote egale l'ecart n'est pas significatif et le retour est de 0,72 EUR pour 1 EUR joue. Chaque reponse porte un champ avertissement qui le rappelle.",
        "parameters": [
          {
            "$ref": "#/components/parameters/date"
          },
          {
            "$ref": "#/components/parameters/rc"
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/tops": {
      "get": {
        "summary": "Top N du jour par indicateur (34 indicateurs du catalogue)",
        "parameters": [
          {
            "name": "indicateur",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "ELO_Cheval"
          },
          {
            "name": "n",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "discipline",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/formatCsv"
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Mettre csv pour une sortie tableur : une ligne par cheval du classement.",
            "schema": {
              "type": "string",
              "example": "csv"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/value-bets": {
      "get": {
        "summary": "Value bets du jour (composite Cote_BZH_fiable)",
        "parameters": [
          {
            "name": "n",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 30,
              "default": 10
            }
          },
          {
            "name": "date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "$ref": "#/components/parameters/formatCsv"
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Mettre csv pour une sortie tableur : une ligne par candidat.",
            "schema": {
              "type": "string",
              "example": "csv"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/chevaux": {
      "get": {
        "summary": "Recherche d'un cheval par nom",
        "parameters": [
          {
            "name": "recherche",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 2
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10,
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/chevaux/{id}/historique": {
      "get": {
        "summary": "Dernieres courses d'un cheval (max 50 par appel)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "discipline",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "debut",
            "in": "query",
            "required": false,
            "description": "Borne basse de la periode, au format YYYY-MM-DD. Ajoute le 03/08/2026 : rend exprimable « ce cheval sur l'hiver 2025 », qui imposait sinon de tout tirer et de filtrer chez soi.",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2025-11-01"
            }
          },
          {
            "name": "fin",
            "in": "query",
            "required": false,
            "description": "Borne haute de la periode, au format YYYY-MM-DD. Ajoute le 03/08/2026 : rend exprimable « ce cheval sur l'hiver 2025 », qui imposait sinon de tout tirer et de filtrer chez soi.",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-02-28"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Mettre csv pour une sortie tableur : une ligne par course courue par le cheval.",
            "schema": {
              "type": "string",
              "example": "csv"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/chevaux/{id}/stats": {
      "get": {
        "summary": "Stats agregees d'un cheval",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "periode_jours",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1825
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/chevaux/{id}/lectures": {
      "get": {
        "summary": "Lectures expertes actives du jour pour un cheval",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/chevaux/{id}/renifleur": {
      "get": {
        "summary": "Toutes les notes du Renifleur sur un cheval",
        "description": "L'historique des notes ecrites sur ce cheval, de la plus recente a la plus ancienne. nb_notes vaut 0 si le cheval n'a jamais ete repere sur la periode conservee, soit 400 jours. Contenu editorial exclusif turf.bzh, ce n'est PAS un signal de pari. Mesure sur 2 411 rentrees : les chevaux reperes terminent dans les trois premiers 35,9 % du temps contre 27,8 % pour l'ensemble des partants, mais a cote egale l'ecart n'est pas significatif et le retour est de 0,72 EUR pour 1 EUR joue. Chaque reponse porte un champ avertissement qui le rappelle.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "n_last",
            "in": "query",
            "description": "Nombre de notes a retourner, 1 a 40. Defaut 10.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 40,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/personnes": {
      "get": {
        "summary": "Recherche jockey/driver/entraineur (semantique + fallback)",
        "parameters": [
          {
            "name": "recherche",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 2
            }
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "jockey",
                "driver",
                "entraineur"
              ],
              "default": "jockey"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10,
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/personnes/{type}/{id}/stats": {
      "get": {
        "summary": "Stats d'un jockey ou d'un entraineur (periode, discipline ; hippodrome pour les jockeys)",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "jockey",
                "entraineur"
              ]
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "periode_jours",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1825,
              "default": 90
            }
          },
          {
            "name": "discipline",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "hippodrome",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "debut",
            "in": "query",
            "required": false,
            "description": "Borne basse de la periode, au format YYYY-MM-DD. Ajoute le 03/08/2026. Des qu'une de ces deux bornes est fournie, periode_jours cesse de s'appliquer et le champ period_days de la reponse porte la duree reellement couverte.",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2025-11-01"
            }
          },
          {
            "name": "fin",
            "in": "query",
            "required": false,
            "description": "Borne haute de la periode, au format YYYY-MM-DD. Ajoute le 03/08/2026. Des qu'une de ces deux bornes est fournie, periode_jours cesse de s'appliquer et le champ period_days de la reponse porte la duree reellement couverte.",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-02-28"
            }
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/methodes": {
      "get": {
        "summary": "Liste des methodes BZH du compte (appliquer/backtester : via POST /v1/chat, forfaits 60/100 cr)",
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    },
    "/v1/chat": {
      "post": {
        "summary": "Poser une question a ChatBZH (consomme les credits IA du compte)",
        "description": "Reponse synchrone (30-180 s possibles). Refus 402 AVANT l'appel si solde insuffisant ou si l'estimation depasse max_credits. Forfaits automatiques : application de methode 60 cr, backtest 100 cr.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "question"
                ],
                "properties": {
                  "question": {
                    "type": "string",
                    "example": "Quels sont les 3 meilleurs ELO du Quinte du jour ?"
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "standard",
                      "expert"
                    ],
                    "default": "standard"
                  },
                  "max_credits": {
                    "type": "integer",
                    "minimum": 5,
                    "maximum": 300,
                    "default": 300
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/OK"
          },
          "default": {
            "$ref": "#/components/responses/Err"
          }
        }
      }
    }
  }
}
