{
  "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.",
    "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" }
        ],
        "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 du jour",
        "parameters": [{ "name": "code_course", "in": "query", "schema": { "type": "string", "pattern": "^R\\d{1,2}C\\d{1,2}$" } }],
        "responses": { "200": { "$ref": "#/components/responses/OK" }, "default": { "$ref": "#/components/responses/Err" } }
      }
    },
    "/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" }],
        "responses": { "200": { "$ref": "#/components/responses/OK" }, "default": { "$ref": "#/components/responses/Err" } }
      }
    },
    "/v1/courses/{date}/{rc}/cotes": {
      "get": {
        "summary": "Cotes PMU en direct + evolution 5 min",
        "parameters": [{ "$ref": "#/components/parameters/date" }, { "$ref": "#/components/parameters/rc" }, { "$ref": "#/components/parameters/formatCsv" }],
        "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",
        "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}/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" }
        ],
        "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" }
        ],
        "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" } }
        ],
        "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" } }
        ],
        "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" } }
      }
    }
  }
}
