{"openapi":"3.1.0","info":{"title":"HésiaSun, API publique","version":"2026-09-01","description":"API REST pour installateurs photovoltaïques : création de projets, dimensionnement et étude économique, suivi du pipeline, documents. Toutes les réponses sont enveloppées dans `{ api_version, data }` ou `{ api_version, error }`. Les identifiants sont des UUID ; les dates sont en ISO 8601 (UTC).","contact":{"name":"Support HésiaSun","email":"support@hesiasun.example"}},"servers":[{"url":"/api/v1","description":"Même origine que l’application"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Projets","description":"Cycle de vie d’un projet : création, calcul, pipeline"},{"name":"Documents","description":"Pièces générées (synthèse, CERFA, plans)"},{"name":"Compte","description":"Quotas et consommation"},{"name":"Méta","description":"Documentation"}],"paths":{"/projects":{"get":{"tags":["Projets"],"operationId":"listProjects","summary":"Lister les projets","description":"Liste paginée, triée par date de création décroissante. Scope `projects:read`.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["prospect","sent","won","lost"]}},{"name":"q","in":"query","description":"Recherche insensible à la casse sur le nom du client","schema":{"type":"string","maxLength":200}},{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"per_page","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}}],"responses":{"200":{"description":"Liste paginée","headers":{"X-RateLimit-Remaining":{"description":"Requêtes restantes dans la fenêtre glissante d’une minute","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["api_version","data"],"properties":{"api_version":{"type":"string","example":"2026-09-01"},"data":{"$ref":"#/components/schemas/ProjectList"}}}}}},"401":{"description":"Clé API manquante, invalide, révoquée ou expirée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"unauthorized","message":"Clé API invalide"}}}}},"402":{"description":"Quota mensuel atteint ou API non incluse dans le plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"quota_exceeded","message":"Quota mensuel d’appels API atteint (5000/5000).","details":{"used":5000,"limit":5000}}}}}},"403":{"description":"Scope insuffisant pour cette clé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"forbidden","message":"Scope requis : projects:write"}}}}},"422":{"description":"Entrées invalides (détail par champ)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"validation_error","message":"Entrées invalides","details":[{"path":"client.name","message":"Nom du client requis"}]}}}}},"429":{"description":"Rate limit dépassé (60 requêtes / minute / clé). Voir Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"rate_limited","message":"Trop de requêtes : 60 par minute et par clé"}}}}},"500":{"description":"Erreur interne","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"internal_error","message":"Erreur interne"}}}}}}},"post":{"tags":["Projets"],"operationId":"createProject","summary":"Créer un projet et lancer le calcul","description":"Consomme 1 unité du quota « projets » du mois (402 sinon). Géocode l’adresse si lat/lon absents (BAN), interroge PVGIS puis le moteur. Si le calcul échoue, le projet est tout de même créé et renvoyé avec `calculation: null` et `calculation_error`. Émet le webhook `project.created`. Scope `projects:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectInput"},"example":{"client":{"name":"Marie Dupont","email":"marie.dupont@example.fr","phone":"+33 6 12 34 56 78"},"location":{"address":"12 rue de la République","postal_code":"81600","city":"Gaillac"},"roof":{"type":"tuiles","tilt_percent":30,"orientation":"S","surface_m2":40},"consumption":{"annual_kwh":6000,"profile":"RES1","contract":"base","price_kwh":0.25},"battery":{"wanted":false},"objective":"autoconso_max","financing":null,"pipeline":{"status":"prospect","estimated_amount":8900,"follow_up_date":"2026-09-22","notes":"Rappeler après devis"}}}}},"responses":{"201":{"description":"Projet créé (avec ou sans calcul)","headers":{"X-RateLimit-Remaining":{"description":"Requêtes restantes dans la fenêtre glissante d’une minute","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["api_version","data"],"properties":{"api_version":{"type":"string","example":"2026-09-01"},"data":{"$ref":"#/components/schemas/ApiProjectCreated"}}}}}},"401":{"description":"Clé API manquante, invalide, révoquée ou expirée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"unauthorized","message":"Clé API invalide"}}}}},"402":{"description":"Quota mensuel atteint ou API non incluse dans le plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"quota_exceeded","message":"Quota mensuel d’appels API atteint (5000/5000).","details":{"used":5000,"limit":5000}}}}}},"403":{"description":"Scope insuffisant pour cette clé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"forbidden","message":"Scope requis : projects:write"}}}}},"422":{"description":"Entrées invalides (détail par champ)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"validation_error","message":"Entrées invalides","details":[{"path":"client.name","message":"Nom du client requis"}]}}}}},"429":{"description":"Rate limit dépassé (60 requêtes / minute / clé). Voir Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"rate_limited","message":"Trop de requêtes : 60 par minute et par clé"}}}}},"500":{"description":"Erreur interne","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"internal_error","message":"Erreur interne"}}}}}}}},"/projects/{id}":{"parameters":[{"name":"id","in":"path","required":true,"description":"Identifiant UUID du projet","schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Projets"],"operationId":"getProject","summary":"Détail d’un projet","description":"Renvoie le projet et son dernier calcul complet. Scope `projects:read`.","responses":{"200":{"description":"Projet","headers":{"X-RateLimit-Remaining":{"description":"Requêtes restantes dans la fenêtre glissante d’une minute","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["api_version","data"],"properties":{"api_version":{"type":"string","example":"2026-09-01"},"data":{"$ref":"#/components/schemas/ApiProject"}}}}}},"401":{"description":"Clé API manquante, invalide, révoquée ou expirée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"unauthorized","message":"Clé API invalide"}}}}},"402":{"description":"Quota mensuel atteint ou API non incluse dans le plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"quota_exceeded","message":"Quota mensuel d’appels API atteint (5000/5000).","details":{"used":5000,"limit":5000}}}}}},"403":{"description":"Scope insuffisant pour cette clé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"forbidden","message":"Scope requis : projects:write"}}}}},"404":{"description":"Projet introuvable dans cette organisation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"not_found","message":"Projet introuvable"}}}}},"429":{"description":"Rate limit dépassé (60 requêtes / minute / clé). Voir Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"rate_limited","message":"Trop de requêtes : 60 par minute et par clé"}}}}},"500":{"description":"Erreur interne","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"internal_error","message":"Erreur interne"}}}}}}},"patch":{"tags":["Projets"],"operationId":"updateProjectPipeline","summary":"Mettre à jour le pipeline","description":"Statut, montant estimé, date de relance, notes. Un changement de statut émet `project.status_changed` (avec `previous_status`) ; le passage à `won` émet aussi `project.won`. Scope `projects:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectPatch"}}}},"responses":{"200":{"description":"Projet mis à jour","headers":{"X-RateLimit-Remaining":{"description":"Requêtes restantes dans la fenêtre glissante d’une minute","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["api_version","data"],"properties":{"api_version":{"type":"string","example":"2026-09-01"},"data":{"$ref":"#/components/schemas/ApiProject"}}}}}},"401":{"description":"Clé API manquante, invalide, révoquée ou expirée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"unauthorized","message":"Clé API invalide"}}}}},"402":{"description":"Quota mensuel atteint ou API non incluse dans le plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"quota_exceeded","message":"Quota mensuel d’appels API atteint (5000/5000).","details":{"used":5000,"limit":5000}}}}}},"403":{"description":"Scope insuffisant pour cette clé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"forbidden","message":"Scope requis : projects:write"}}}}},"404":{"description":"Projet introuvable dans cette organisation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"not_found","message":"Projet introuvable"}}}}},"422":{"description":"Entrées invalides (détail par champ)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"validation_error","message":"Entrées invalides","details":[{"path":"client.name","message":"Nom du client requis"}]}}}}},"429":{"description":"Rate limit dépassé (60 requêtes / minute / clé). Voir Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"rate_limited","message":"Trop de requêtes : 60 par minute et par clé"}}}}},"500":{"description":"Erreur interne","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"internal_error","message":"Erreur interne"}}}}}}}},"/projects/{id}/calculate":{"parameters":[{"name":"id","in":"path","required":true,"description":"Identifiant UUID du projet","schema":{"type":"string","format":"uuid"}}],"post":{"tags":["Projets"],"operationId":"calculateProject","summary":"Recalculer un projet","description":"Crée une nouvelle ligne de calcul (les anciennes restent consultables côté application). Corps optionnel. Une toiture trop petite renvoie 422 `calculation_error`. Scope `projects:write`.","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalculateBody"}}}},"responses":{"200":{"description":"Nouveau calcul","headers":{"X-RateLimit-Remaining":{"description":"Requêtes restantes dans la fenêtre glissante d’une minute","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["api_version","data"],"properties":{"api_version":{"type":"string","example":"2026-09-01"},"data":{"type":"object","required":["project"],"properties":{"project":{"$ref":"#/components/schemas/ApiProject"}}}}}}}},"401":{"description":"Clé API manquante, invalide, révoquée ou expirée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"unauthorized","message":"Clé API invalide"}}}}},"402":{"description":"Quota mensuel atteint ou API non incluse dans le plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"quota_exceeded","message":"Quota mensuel d’appels API atteint (5000/5000).","details":{"used":5000,"limit":5000}}}}}},"403":{"description":"Scope insuffisant pour cette clé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"forbidden","message":"Scope requis : projects:write"}}}}},"404":{"description":"Projet introuvable dans cette organisation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"not_found","message":"Projet introuvable"}}}}},"422":{"description":"Entrées invalides ou calcul impossible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"calculation_error","message":"Toiture trop petite : 0.4 kWc possible, minimum 0.5 kWc (1 module).","details":{"code":"roof_too_small"}}}}}},"429":{"description":"Rate limit dépassé (60 requêtes / minute / clé). Voir Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"rate_limited","message":"Trop de requêtes : 60 par minute et par clé"}}}}},"500":{"description":"Erreur interne","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"internal_error","message":"Erreur interne"}}}}}}}},"/projects/{id}/documents":{"parameters":[{"name":"id","in":"path","required":true,"description":"Identifiant UUID du projet","schema":{"type":"string","format":"uuid"}}],"get":{"tags":["Documents"],"operationId":"listProjectDocuments","summary":"Documents d’un projet","description":"Pièces générées avec URL signée valable 1 heure. Liste vide tant qu’aucun document n’a été généré. Scope `docs:read`.","responses":{"200":{"description":"Documents","headers":{"X-RateLimit-Remaining":{"description":"Requêtes restantes dans la fenêtre glissante d’une minute","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["api_version","data"],"properties":{"api_version":{"type":"string","example":"2026-09-01"},"data":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Document"}}}}}}}}},"401":{"description":"Clé API manquante, invalide, révoquée ou expirée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"unauthorized","message":"Clé API invalide"}}}}},"402":{"description":"Quota mensuel atteint ou API non incluse dans le plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"quota_exceeded","message":"Quota mensuel d’appels API atteint (5000/5000).","details":{"used":5000,"limit":5000}}}}}},"403":{"description":"Scope insuffisant pour cette clé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"forbidden","message":"Scope requis : projects:write"}}}}},"404":{"description":"Projet introuvable dans cette organisation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"not_found","message":"Projet introuvable"}}}}},"429":{"description":"Rate limit dépassé (60 requêtes / minute / clé). Voir Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"rate_limited","message":"Trop de requêtes : 60 par minute et par clé"}}}}},"500":{"description":"Erreur interne","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"internal_error","message":"Erreur interne"}}}}}}}},"/usage":{"get":{"tags":["Compte"],"operationId":"getUsage","summary":"Consommation du mois","description":"Projets créés, appels API et stockage du mois courant, avec les limites du plan. Cet appel n’est pas décompté du quota. Scope `projects:read`.","responses":{"200":{"description":"Usage","headers":{"X-RateLimit-Remaining":{"description":"Requêtes restantes dans la fenêtre glissante d’une minute","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","required":["api_version","data"],"properties":{"api_version":{"type":"string","example":"2026-09-01"},"data":{"$ref":"#/components/schemas/Usage"}}},"example":{"api_version":"2026-09-01","data":{"period":"2026-09","plan":"pro","projects":{"used":12,"limit":200},"api_calls":{"used":340,"limit":5000,"enabled":true},"storage":{"used_bytes":18350080,"limit_mb":2048}}}}}},"401":{"description":"Clé API manquante, invalide, révoquée ou expirée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"unauthorized","message":"Clé API invalide"}}}}},"403":{"description":"Scope insuffisant pour cette clé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"forbidden","message":"Scope requis : projects:write"}}}}},"429":{"description":"Rate limit dépassé (60 requêtes / minute / clé). Voir Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"rate_limited","message":"Trop de requêtes : 60 par minute et par clé"}}}}},"500":{"description":"Erreur interne","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"api_version":"2026-09-01","error":{"code":"internal_error","message":"Erreur interne"}}}}}}}},"/openapi.json":{"get":{"tags":["Méta"],"operationId":"getOpenApiSpec","summary":"Cette spécification","security":[],"responses":{"200":{"description":"Document OpenAPI 3.1","content":{"application/json":{"schema":{"type":"object"}}}}}}}},"webhooks":{"project.created":{"post":{"summary":"Projet créé via l’API ou l’application","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPayload"}}}},"responses":{"2xx":{"description":"Accusé de réception. Tout autre code déclenche une nouvelle tentative (5 max, backoff 1 min → 12 h)."}}}},"project.status_changed":{"post":{"summary":"Statut du pipeline modifié","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPayload"}}}},"responses":{"2xx":{"description":"Accusé de réception"}}}},"project.won":{"post":{"summary":"Projet gagné","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPayload"}}}},"responses":{"2xx":{"description":"Accusé de réception"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"bk_live_…","description":"Clé API générée dans Paramètres → API. Envoyer `Authorization: Bearer bk_live_…`. Scopes : projects:read, projects:write, docs:read."}},"schemas":{"ErrorResponse":{"type":"object","required":["api_version","error"],"properties":{"api_version":{"type":"string","example":"2026-09-01"},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["unauthorized","forbidden","not_found","validation_error","quota_exceeded","rate_limited","calculation_error","conflict","internal_error"]},"message":{"type":"string"},"details":{"description":"Détails optionnels (liste des champs invalides, compteurs de quota…)"}}}}},"ProjectInput":{"type":"object","required":["client","location","roof","consumption"],"properties":{"client":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"email":{"type":"string","format":"email","nullable":true},"phone":{"type":"string","maxLength":30,"nullable":true}}},"location":{"type":"object","description":"Fournir lat/lon, ou un code postal, ou une adresse (géocodage BAN sinon).","properties":{"address":{"type":"string","maxLength":300,"nullable":true},"postal_code":{"type":"string","pattern":"^[0-9]{5}$","nullable":true},"city":{"type":"string","maxLength":120,"nullable":true},"insee_code":{"type":"string","pattern":"^([0-9]{5}|2[AB][0-9]{3})$","nullable":true},"lat":{"type":"number","minimum":-90,"maximum":90,"nullable":true},"lon":{"type":"number","minimum":-180,"maximum":180,"nullable":true}}},"roof":{"type":"object","properties":{"type":{"type":"string","enum":["tuiles","bac_acier","terrasse","sol"],"nullable":true},"tilt_percent":{"type":"number","minimum":0,"maximum":200,"description":"Pente en % (30 % ≈ 16,7°)","nullable":true},"orientation":{"description":"Point cardinal (N, NE, E, SE, S, SO, O, NO) ou aspect PVGIS en degrés (S = 0, E = -90, O = +90). Défaut : S.","oneOf":[{"type":"string","enum":["N","NE","E","SE","S","SO","O","NO"]},{"type":"number","minimum":-180,"maximum":180}],"default":"S"},"surface_m2":{"type":"number","exclusiveMinimum":0,"maximum":10000,"nullable":true},"target_kwc":{"type":"number","exclusiveMinimum":0,"maximum":500,"description":"Puissance cible ; sinon optimiseur","nullable":true}}},"consumption":{"type":"object","description":"annual_kwh ou monthly_kwh (12 valeurs) requis.","properties":{"annual_kwh":{"type":"number","minimum":0,"maximum":1000000,"nullable":true},"monthly_kwh":{"type":"array","items":{"type":"number","minimum":0},"minItems":12,"maxItems":12,"nullable":true},"profile":{"type":"string","enum":["RES1","RES2"],"default":"RES1"},"contract":{"type":"string","enum":["base","hphc"],"default":"base"},"price_kwh":{"type":"number","exclusiveMinimum":0,"maximum":2,"nullable":true},"price_kwh_hp":{"type":"number","exclusiveMinimum":0,"maximum":2,"nullable":true},"price_kwh_hc":{"type":"number","exclusiveMinimum":0,"maximum":2,"nullable":true}}},"battery":{"type":"object","properties":{"wanted":{"type":"boolean","default":false},"capacity_kwh":{"type":"number","exclusiveMinimum":0,"maximum":100,"description":"Absent = dimensionnement automatique","nullable":true}}},"objective":{"type":"string","enum":["autoconso_max","power_max"],"default":"autoconso_max"},"financing":{"type":"object","properties":{"rate":{"type":"number","minimum":0,"maximum":0.3,"nullable":true},"years":{"type":"integer","minimum":1,"maximum":30,"nullable":true},"deposit":{"type":"number","minimum":0,"nullable":true}},"nullable":true},"pipeline":{"$ref":"#/components/schemas/ProjectPatch","nullable":true}},"example":{"client":{"name":"Marie Dupont","email":"marie.dupont@example.fr","phone":"+33 6 12 34 56 78"},"location":{"address":"12 rue de la République","postal_code":"81600","city":"Gaillac"},"roof":{"type":"tuiles","tilt_percent":30,"orientation":"S","surface_m2":40},"consumption":{"annual_kwh":6000,"profile":"RES1","contract":"base","price_kwh":0.25},"battery":{"wanted":false},"objective":"autoconso_max","financing":null,"pipeline":{"status":"prospect","estimated_amount":8900,"follow_up_date":"2026-09-22","notes":"Rappeler après devis"}}},"ProjectPatch":{"type":"object","description":"Au moins un champ requis.","minProperties":1,"properties":{"status":{"type":"string","enum":["prospect","sent","won","lost"]},"estimated_amount":{"type":"number","minimum":0,"nullable":true},"follow_up_date":{"type":"string","format":"date","pattern":"^\\d{4}-\\d{2}-\\d{2}$","nullable":true},"notes":{"type":"string","maxLength":5000,"nullable":true}},"example":{"status":"sent","estimated_amount":8900,"follow_up_date":"2026-09-22"}},"CalculateBody":{"type":"object","properties":{"reuse_production":{"type":"boolean","default":false,"description":"Réutilise la production PVGIS du dernier calcul si les paramètres sont inchangés."}}},"Variant":{"type":"object","properties":{"battery_kwh":{"type":"number"},"energy":{"type":"object","properties":{"self_consumed_kwh":{"type":"number"},"exported_kwh":{"type":"number"},"imported_kwh":{"type":"number"},"autoconso_rate":{"type":"number","description":"Part de la production autoconsommée (0-1)"},"autarky_rate":{"type":"number","description":"Part de la consommation couverte (0-1)"},"monthly":{"type":"object","properties":{"prod":{"type":"array","items":{"type":"number"}},"conso":{"type":"array","items":{"type":"number"}},"autoconso":{"type":"array","items":{"type":"number"}},"surplus":{"type":"array","items":{"type":"number"}}}}}},"economics":{"type":"object","properties":{"capex":{"type":"number","description":"Investissement TTC (€)"},"capex_breakdown":{"type":"object","properties":{"pv":{"type":"number"},"battery":{"type":"number"},"source":{"type":"string","enum":["grid","price_per_wc"]}}},"effective_price_kwh":{"type":"number"},"savings_year1":{"type":"number"},"surplus_revenue_year1":{"type":"number"},"payback_years":{"type":"number","nullable":true},"irr":{"type":"number","nullable":true},"total_gain_horizon":{"type":"number"},"horizon_years":{"type":"integer"},"yearly":{"type":"array","items":{"type":"object","properties":{"year":{"type":"integer"},"savings":{"type":"number"},"surplus_revenue":{"type":"number"},"cashflow":{"type":"number"},"cumulative":{"type":"number"}}}}}},"financing":{"type":"object","properties":{"rate":{"type":"number"},"years":{"type":"integer"},"deposit":{"type":"number"},"capital":{"type":"number"},"monthly_payment":{"type":"number"},"avg_monthly_effort_during_loan":{"type":"number"},"neutrality_year":{"type":"integer","nullable":true},"cumul_at_loan_end":{"type":"number"}},"nullable":true}}},"CalcResult":{"type":"object","description":"Résultat complet du moteur (immuable, versionné par engine_version + calc_version).","properties":{"engine_version":{"type":"string"},"calc_version":{"type":"string"},"computed_at":{"type":"string","format":"date-time"},"sizing":{"type":"object","properties":{"recommended_kwc":{"type":"number"},"selected_kwc":{"type":"number"},"selection_reason":{"type":"string","enum":["optimizer","target","roof_max","power_max"]},"modules":{"type":"object","properties":{"count":{"type":"integer"},"power_wc":{"type":"integer"},"area_m2":{"type":"number"}}},"roof_max_kwc":{"type":"number","nullable":true},"max_kwc":{"type":"number"},"capped_by_offer":{"type":"boolean"},"inverter":{"type":"object","properties":{"ac_kw":{"type":"number"},"dc_ac_ratio":{"type":"number"},"mppt":{"type":"integer"}}},"scenarios":{"type":"array","items":{"type":"object","properties":{"kwc":{"type":"number"},"label":{"type":"string"},"panels":{"type":"integer"},"is_optimum":{"type":"boolean"},"autoconso_rate":{"type":"number"},"autarky_rate":{"type":"number"},"savings_year1":{"type":"number"},"capex":{"type":"number"}}}}}},"production":{"type":"object","properties":{"source":{"type":"string","enum":["pvgis","fallback"]},"annual_kwh":{"type":"number"},"monthly_kwh":{"type":"array","items":{"type":"number"}},"specific_yield_kwh_per_kwc":{"type":"number"},"e_m_1kwc":{"type":"array","items":{"type":"number"}},"params":{"type":"object","properties":{"lat":{"type":"number"},"lon":{"type":"number"},"loss":{"type":"number"},"angle":{"type":"number"},"aspect":{"type":"number"},"peakpower":{"type":"integer","const":1}}}}},"consumption":{"type":"object","properties":{"annual_kwh":{"type":"number"},"monthly_kwh":{"type":"array","items":{"type":"number"}},"profile":{"type":"string","enum":["RES1","RES2"]},"contract":{"type":"string","enum":["base","hphc"]},"price_kwh":{"type":"number"},"price_kwh_hp":{"type":"number","nullable":true},"price_kwh_hc":{"type":"number","nullable":true}}},"variants":{"type":"object","properties":{"without_battery":{"$ref":"#/components/schemas/Variant"},"with_battery":{"$ref":"#/components/schemas/Variant","nullable":true}}},"assumptions_used":{"type":"object","description":"Hypothèses effectives (organisation ⊕ défauts nationaux)","additionalProperties":true},"warnings":{"type":"array","items":{"type":"string"}}}},"CalculationSummary":{"type":"object","description":"Résumé du dernier calcul (liste). Le résultat complet est sur GET /projects/{id}.","properties":{"id":{"type":"string","format":"uuid"},"engine_version":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"summary":{"type":"object","properties":{"selected_kwc":{"type":"number","nullable":true},"annual_production_kwh":{"type":"number","nullable":true},"autoconso_rate":{"type":"number","nullable":true},"savings_year1":{"type":"number","nullable":true},"capex":{"type":"number","nullable":true},"payback_years":{"type":"number","nullable":true}}}}},"ApiProjectBase":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"reference":{"type":"string","example":"SOL-2026-0042"},"status":{"type":"string","enum":["prospect","sent","won","lost"]},"client_name":{"type":"string"},"client_email":{"type":"string","nullable":true},"client_phone":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"postal_code":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"lat":{"type":"number","nullable":true},"lon":{"type":"number","nullable":true},"estimated_amount":{"type":"number","nullable":true},"follow_up_date":{"type":"string","format":"date","nullable":true},"notes":{"type":"string","nullable":true},"source":{"type":"string","enum":["app","api"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"won_at":{"type":"string","format":"date-time","nullable":true},"lost_at":{"type":"string","format":"date-time","nullable":true},"input":{"$ref":"#/components/schemas/ProjectInput"}}},"ApiProject":{"allOf":[{"$ref":"#/components/schemas/ApiProjectBase"},{"type":"object","properties":{"calculation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"engine_version":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"result":{"$ref":"#/components/schemas/CalcResult"}},"nullable":true}}}]},"ApiProjectCreated":{"allOf":[{"$ref":"#/components/schemas/ApiProject"},{"type":"object","properties":{"calculation_error":{"type":"object","description":"Présent uniquement si le projet a été créé mais que le calcul a échoué (ex. toiture trop petite).","properties":{"code":{"type":"string","example":"roof_too_small"},"message":{"type":"string"}}}}}]},"ApiProjectListItem":{"allOf":[{"$ref":"#/components/schemas/ApiProjectBase"},{"type":"object","properties":{"calculation":{"$ref":"#/components/schemas/CalculationSummary","nullable":true}}}]},"ProjectList":{"type":"object","required":["items","page","per_page","total"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ApiProjectListItem"}},"page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"}}},"Document":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["synthese","cerfa_dp","cerfa_json","notice","plan_situation","plan_masse","dossier_dp"]},"mime_type":{"type":"string","example":"application/pdf"},"file_size_bytes":{"type":"integer","nullable":true},"watermarked":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"url":{"type":"string","format":"uri","description":"URL signée valable 1 heure","nullable":true}}},"Usage":{"type":"object","properties":{"period":{"type":"string","example":"2026-09","description":"Mois courant (YYYY-MM)"},"plan":{"type":"string","enum":["free","pro"]},"projects":{"type":"object","properties":{"used":{"type":"integer"},"limit":{"type":"integer"}}},"api_calls":{"type":"object","properties":{"used":{"type":"integer"},"limit":{"type":"integer"},"enabled":{"type":"boolean"}}},"storage":{"type":"object","properties":{"used_bytes":{"type":"integer"},"limit_mb":{"type":"integer"}}}}},"WebhookPayload":{"type":"object","description":"Corps envoyé (POST JSON) à votre URL de webhook.","properties":{"event":{"type":"string","enum":["project.created","project.status_changed","project.won"]},"occurred_at":{"type":"string","format":"date-time"},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"reference":{"type":"string"},"client_name":{"type":"string"},"status":{"type":"string","enum":["prospect","sent","won","lost"]},"previous_status":{"type":"string","enum":["prospect","sent","won","lost"],"description":"Présent sur status_changed et won"},"estimated_amount":{"type":"number","nullable":true},"kwc":{"type":"number","nullable":true},"pdf_urls":{"type":"array","items":{"type":"string","format":"uri"}}}}}}}}}