{"openapi":"3.0.3","info":{"title":"es-epgdoctor Partner API","version":"1.9.60","description":"API REST pour un accès partenaire externe en lecture (chaînes, EPG, qualité, charge publicitaire) et en action limitée (rafraîchissement de chaîne). Un serveur MCP sera posé par-dessus cette même API dans un second temps, avec son propre système de jetons — access_modes est déjà prêt pour cette dissociation (voir models/api-keys.js)."},"servers":[{"url":"https://es-epgdoctor.cedric-monnier.workers.dev/v1/api"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Clé API partenaire (préfixe epgd_api_), créée depuis l’onglet Utilisateurs > Clés API partenaire. Chaque clé porte ses propres scopes nommés — aucun scope n’est jamais implicite."}}},"security":[{"bearerAuth":[]}],"paths":{"/channels":{"get":{"operationId":"list_channels","summary":"Liste des chaînes mémorisées","description":"Renvoie chaque chaîne mémorisée disposant d’un channel_id réel (une source jamais vérifiée n’en a pas encore). Ne renvoie jamais l’URL du flux XML source. channel_id (valeur brute du provider) et channel_uuid (identifiant canonique de l’app, persistance garantie même si le provider renomme un jour la chaîne) sont tous deux exposés — chaque route channels/* accepte l’un ou l’autre.","x-scope":"channels:read","security":[{"bearerAuth":[]}],"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","example":{"data":{"channels":[{"channel_id":"raw_provider_id_123","channel_uuid":"3f1a2b4c-5d6e-7f89-a0b1-c2d3e4f56789","name":"Example Channel","provider":"OKAST","logo_url":"https://…","format":"xmltv","last_checked_at":"2026-08-24T10:00:00.000Z","has_errors":false}]},"meta":{"count":1}}}}}},"400":{"description":"Paramètres invalides ou manquants"},"401":{"description":"Clé API invalide, révoquée ou absente"},"403":{"description":"Scope insuffisant pour cette opération"}}}},"/channels/programs":{"get":{"operationId":"get_channel_programs","summary":"Grille EPG d’une chaîne sur une plage donnée","description":"Programmes connus pour une chaîne entre since et until (ISO 8601). Plage maximale : 31 jours. Un scope content:read supplémentaire (jamais requis, jamais un gate sur l’accès aux programmes eux-mêmes) enrichit chaque programme d’un champ iab_tags — les tags IAB Content Taxonomy 2.2 associés au TITRE de ce programme (pas à cette diffusion précise), destinés à enrichir le contexte passé au décision process de l’Ad Server pour le ciblage contextuel. Absent du tout sans ce scope ; un tableau vide (pas absent) si le scope est présent mais que le titre n’a encore aucun tag connu.","x-scope":"channels:read","security":[{"bearerAuth":[]}],"parameters":[{"name":"channel_id","in":"query","required":false,"schema":{"type":"string"},"description":"channel_id ou channel_uuid requis (l’un des deux)"},{"name":"channel_uuid","in":"query","required":false,"schema":{"type":"string"},"description":"identifiant canonique de la chaîne, persistance garantie contrairement à channel_id — préférable pour un stockage long terme côté partenaire"},{"name":"since","in":"query","required":true,"schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","required":true,"schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","example":{"data":{"channel_id":"raw_provider_id_123","programs":[{"title":"Example Show","subtitle":null,"description":null,"start":"2026-08-24T10:00:00.000Z","end":"2026-08-24T10:30:00.000Z","category":["Documentaire"],"image_url":"https://example.test/programme.jpg","iab_tags":[{"taxonomy_version":"3.1","code":"640","label":"Television"}]}]},"meta":{"count":1,"since":"2026-08-24T00:00:00.000Z","until":"2026-08-25T00:00:00.000Z"}}}}}},"400":{"description":"Paramètres invalides ou manquants"},"401":{"description":"Clé API invalide, révoquée ou absente"},"403":{"description":"Scope insuffisant pour cette opération"}}}},"/channels/refresh":{"post":{"operationId":"refresh_channel","summary":"Relance un contrôle sur une chaîne déjà mémorisée","description":"Refait un fetch + parse réel de la source associée à ce channel_id et persiste le résultat — une vraie action avec effet de bord, pas une lecture passive. Nécessite le scope channels:write, jamais accordé par channels:read seul. channel_id ou channel_uuid requis dans le corps (l’un des deux).","x-scope":"channels:write","security":[{"bearerAuth":[]}],"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","example":{"channel_id":"raw_provider_id_123"}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","example":{"data":{"channel_id":"raw_provider_id_123","checked_at":"2026-08-24T10:00:00.000Z","program_count":42,"issues_count":0,"valid":true,"latest_epg_date":"2026-09-06T18:00:00.000Z"}}}}}},"400":{"description":"Paramètres invalides ou manquants"},"401":{"description":"Clé API invalide, révoquée ou absente"},"403":{"description":"Scope insuffisant pour cette opération"}}}},"/channels/quality":{"get":{"operationId":"get_channel_quality","summary":"Analyse ADN d’une chaîne (structure de grille, diversité de titres, catégories)","description":"range=week nécessite week_start (une date ISO, alignée automatiquement sur le lundi de cette semaine-là) ; range=90d couvre toujours la fenêtre de rétention complète, aucun paramètre supplémentaire.","x-scope":"quality:read","security":[{"bearerAuth":[]}],"parameters":[{"name":"channel_id","in":"query","required":false,"schema":{"type":"string"},"description":"channel_id ou channel_uuid requis (l’un des deux)"},{"name":"channel_uuid","in":"query","required":false,"schema":{"type":"string"},"description":"identifiant canonique de la chaîne, persistance garantie contrairement à channel_id — préférable pour un stockage long terme côté partenaire"},{"name":"range","in":"query","required":true,"schema":{"type":"string","enum":["week","90d"]}},{"name":"week_start","in":"query","required":false,"schema":{"type":"string","format":"date"},"description":"requis si range=week"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","example":{"data":{"channel_id":"raw_provider_id_123","dna":{"catalogue_size":128,"top_category":{"name":"Documentaire","pct":62}},"range":{"since":"2026-08-24T00:00:00.000Z","until":"2026-08-31T00:00:00.000Z"}},"meta":{"truncated":false}}}}}},"400":{"description":"Paramètres invalides ou manquants"},"401":{"description":"Clé API invalide, révoquée ou absente"},"403":{"description":"Scope insuffisant pour cette opération"}}}},"/channels/heatmap":{"get":{"operationId":"get_channel_heatmap","summary":"Grille hebdomadaire densité/fraîcheur/qualité, jour par jour et heure par heure","description":"Même moteur que la «Vue hebdomadaire» de l’onglet Analyse.","x-scope":"quality:read","security":[{"bearerAuth":[]}],"parameters":[{"name":"channel_id","in":"query","required":false,"schema":{"type":"string"},"description":"channel_id ou channel_uuid requis (l’un des deux)"},{"name":"channel_uuid","in":"query","required":false,"schema":{"type":"string"},"description":"identifiant canonique de la chaîne, persistance garantie contrairement à channel_id — préférable pour un stockage long terme côté partenaire"},{"name":"week_start","in":"query","required":true,"schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","example":{"data":{"channel_id":"raw_provider_id_123","heatmap":{"days":["Lun 24 août","…"],"cells":[]},"range":{"since":"2026-08-24T00:00:00.000Z","until":"2026-08-31T00:00:00.000Z"}},"meta":{"truncated":false}}}}}},"400":{"description":"Paramètres invalides ou manquants"},"401":{"description":"Clé API invalide, révoquée ou absente"},"403":{"description":"Scope insuffisant pour cette opération"}}}},"/channels/ad-load":{"get":{"operationId":"get_channel_ad_load","summary":"Statistiques de charge publicitaire (quotidien/horaire)","description":"Historique complet mergé (import quotidien et/ou coupures détaillées), pas une plage bornée — mêmes données que la carte «Ad Load Profile».","x-scope":"ad-load:read","security":[{"bearerAuth":[]}],"parameters":[{"name":"channel_id","in":"query","required":false,"schema":{"type":"string"},"description":"channel_id ou channel_uuid requis (l’un des deux)"},{"name":"channel_uuid","in":"query","required":false,"schema":{"type":"string"},"description":"identifiant canonique de la chaîne, persistance garantie contrairement à channel_id — préférable pour un stockage long terme côté partenaire"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","example":{"data":{"channel_id":"raw_provider_id_123","stats":{"period":{"start":"2026-08-01","end":"2026-08-24","days":24},"avails_per_day_avg":96},"hourly_trend":{"series":[],"avg_avails_per_hour":4}}}}}}},"400":{"description":"Paramètres invalides ou manquants"},"401":{"description":"Clé API invalide, révoquée ou absente"},"403":{"description":"Scope insuffisant pour cette opération"}}}},"/channels/ad-fill-rate":{"get":{"operationId":"get_channel_ad_fill_rate","summary":"Taux de remplissage Ad Server (coupures programmées vs décisions réelles)","description":"Best-effort : si l’Ad Server est injoignable, une heatmap réelle revient quand même (tout en NO_REQUEST), avec ad_serving_warning expliquant pourquoi plutôt qu’une erreur.","x-scope":"ad-load:read","security":[{"bearerAuth":[]}],"parameters":[{"name":"channel_id","in":"query","required":false,"schema":{"type":"string"},"description":"channel_id ou channel_uuid requis (l’un des deux)"},{"name":"channel_uuid","in":"query","required":false,"schema":{"type":"string"},"description":"identifiant canonique de la chaîne, persistance garantie contrairement à channel_id — préférable pour un stockage long terme côté partenaire"},{"name":"week_start","in":"query","required":true,"schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","example":{"data":{"channel_id":"raw_provider_id_123","heatmap":{"days":[],"cells":[]},"range":{"since":"2026-08-24T00:00:00.000Z","until":"2026-08-31T00:00:00.000Z"}},"meta":{"ad_serving_warning":null}}}}}},"400":{"description":"Paramètres invalides ou manquants"},"401":{"description":"Clé API invalide, révoquée ou absente"},"403":{"description":"Scope insuffisant pour cette opération"}}}},"/channels/context":{"get":{"operationId":"get_channel_context","summary":"Snapshot complet programmes + coupures + tags IAB, pour préchargement par un décision process externe","description":"Conçu pour le préchargement — jamais appelé pendant une requête VAST en direct. Retourne un instantané COMPLET, non paginé et autoritatif de [since, until[ : tout programme ou coupure connu qui chevauche la fenêtre est inclus (un programme commencé avant since mais qui se termine après since est inclus). Plage maximale : 7 jours. Une correction ou dépublication de grille est observable dès le prochain appel — un programme qui n\\u2019a pas été reconfirmé par la vérification la plus récente de la chaîne n\\u2019est plus jamais retourné, quelle que soit la fenêtre demandée. program_id et break_id sont des identifiants publics stables (voir program_id : assignation interne UUID + correspondance titre/horaire tant qu\\u2019aucun fournisseur EPG n\\u2019expose son propre identifiant persistant ; break_id : identifiant interne stable, ou l\\u2019identifiant persistant OKAST lui-même dès qu\\u2019il sera disponible dans les exports réels). Les tags IAB (taxonomie IAB Content 3.1) sont une classification publicitaire structurée, distincte des catégories éditoriales. keywords (Ad Server, 2026-09-08) : termes éditoriaux extraits du titre/sous-titre/description de CE programme — jamais mappés vers IAB ici, ce mapping reste entièrement du côté Ad Server, à partir de son propre dictionnaire keyword → IAB Content 3.1 ; les tags IAB natifs (champ iab), quand ils existent, restent prioritaires. series_id (optionnel) : identifiant stable de série/émission récurrente quand la source en fournit un ; null pour la grande majorité des flux aujourd\\u2019hui — jamais dérivé du titre. Supporte ETag/If-None-Match (304 si la fenêtre n\\u2019a pas changé) — keywords et series_id sont inclus dans ce calcul, un changement de l\\u2019un ou l\\u2019autre invalide le cache.","x-scope":"channels:read","security":[{"bearerAuth":[]}],"parameters":[{"name":"channel_id","in":"query","required":false,"schema":{"type":"string"},"description":"channel_id ou channel_uuid requis (l’un des deux)"},{"name":"channel_uuid","in":"query","required":false,"schema":{"type":"string"},"description":"identifiant canonique de la chaîne, persistance garantie contrairement à channel_id — préférable pour un stockage long terme côté partenaire"},{"name":"since","in":"query","required":true,"schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","required":true,"schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","example":{"data":{"channel_id":"raw_provider_id_123","programs":[{"program_id":"3f1a2b4c-5d6e-7f89-a0b1-c2d3e4f56789","series_id":null,"start":"2026-08-25T18:00:00.000Z","end":"2026-08-25T19:00:00.000Z","title":"Example Show","subtitle":null,"description":"An optional editorial synopsis.","categories":["Documentaire"],"image_url":"https://example.test/programme.jpg","iab":{"taxonomy":"IAB_CONTENT","version":"3.1","cattax":9,"tags":[{"term_id":"653","label":"Travel","source":"epg_doctor_manual","confidence":null}]},"keywords":["synopsis","editorial","documentaire"],"updated_at":"2026-08-25T10:12:30.000Z"}],"ad_breaks":[{"break_id":"7c8d9e0f-1a2b-3c4d-5e6f-7a8b9c0d1e2f","program_id":"3f1a2b4c-5d6e-7f89-a0b1-c2d3e4f56789","start":"2026-08-25T18:22:00.000Z","end":"2026-08-25T18:24:00.000Z","duration_seconds":120,"position":"unknown","updated_at":"2026-08-25T10:12:30.000Z"}]},"meta":{"since":"2026-08-25T00:00:00.000Z","until":"2026-09-01T00:00:00.000Z","generated_at":"2026-08-25T10:15:00.000Z","source_updated_at":"2026-08-25T10:12:30.000Z","coverage_until":"2026-09-06T18:00:00.000Z","program_count":1,"ad_break_count":1}}}}}},"400":{"description":"Paramètres invalides ou manquants"},"401":{"description":"Clé API invalide, révoquée ou absente"},"403":{"description":"Scope insuffisant pour cette opération"}}}}}}