{"openapi":"3.1.0","info":{"title":"build.solar API","version":"1.0.0","summary":"Le dossier solaire de l’installateur photovoltaïque en France : les démarches dans l’ordre, les documents pré-remplis, la preuve de chaque dépôt.","description":"build.solar tient le dossier administratif d’un chantier photovoltaïque résidentiel pour l’installateur, en France (métropole, Corse et outre-mer) : dossier client, étude technique, devis et signature électronique, puis les démarches dans l’ordre — déclaration préalable en mairie et ses pièces, raccordement, CONSUEL, mise en service, contrat d’achat —, avec le Cerfa officiel pré-rempli, le paquet de dépôt de chaque démarche et la preuve gardée au dossier. Pour l’installateur et l’agent qui travaille pour lui, avec ses droits : ce qu’un agent fait avec une clé est tracé comme tel, et les créations critiques acceptent un en-tête Idempotency-Key. Authentification : la clé API du compte (`x-api-key: bsol_live_sk_…` ou `Authorization: Bearer bsol_live_sk_…`), que l’installateur crée depuis son compte connecté (POST /auth/keys, révocable). Serveur MCP du compte : POST /mcp. Sans compte : le registre réglementaire versionné et sourcé (GET /regulatory/snapshot), la simulation d’une installation (POST /simulate) et l’audit d’un devis reçu (POST /quotes/audit). Guide pour les agents : https://build.solar/llms.txt.","termsOfService":"https://build.solar/cgu","contact":{"name":"build.solar","url":"https://build.solar","email":"contact@build.solar"}},"servers":[{"url":"/api","description":"API build.solar"}],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"},"apiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key","description":"API key starting with bsol_live_sk_"}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string","description":"Machine-readable error code"},"code":{"type":"string","description":"Error code (same as error)"},"message":{"type":"string","description":"Human-readable description"},"timestamp":{"type":"string","format":"date-time"}}},"QuoteMentionsMissing":{"type":"object","description":"Refusal of POST /quotes and POST /quotes/generate (HTTP 422, code QUOTE_MENTIONS_MISSING): a mandatory mention of the quote is missing, so nothing is created and no email leaves. Complete each mention with the request named in `completeWith`, then send the same body again. POST /quotes/preview stays open and prints « — » where a mention is missing.","properties":{"error":{"type":"string","enum":["QUOTE_MENTIONS_MISSING"]},"code":{"type":"string","enum":["QUOTE_MENTIONS_MISSING"]},"message":{"type":"string","description":"French sentence naming the missing mentions"},"missingMentions":{"type":"array","items":{"type":"string"},"description":"French labels, in the order of the document (SIRET, forme juridique, capital social, immatriculation (RCS ou RNE), n° de TVA intracommunautaire, adresse du siège, téléphone, assurance décennale, coordonnées de l’assureur et couverture géographique, médiateur de la consommation, modalités de paiement, délais)"},"completeWith":{"type":"array","items":{"type":"object","properties":{"mention":{"type":"string"},"request":{"type":"string","description":"PATCH /api/installers/me, PATCH /api/auth/me, POST /api/installers/me/documents, or the `terms` of the quote body — or the whole `quoteDefaults` object of the profile (PATCH /api/installers/me): it replaces the five default terms, a key left out is erased"},"fields":{"type":"array","items":{"type":"string"}},"body":{"type":"object","additionalProperties":{"type":"string"},"description":"Fixed fields to send with the request, e.g. { kind: INSURANCE_DECENNALE }"}}}},"timestamp":{"type":"string","format":"date-time"}}},"Pagination":{"type":"object","properties":{"page":{"type":"integer"},"limit":{"type":"integer"},"total":{"type":"integer"},"pages":{"type":"integer"}}},"Project":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"type":{"type":"string","enum":["Résidentiel","Commercial","Industriel","Public","Collectif"]},"status":{"type":"string","enum":["DRAFT","SIMULATED","QUOTE_REQUESTED","QUOTE_RECEIVED","SIGNED","INSTALLING","COMPLETED"]},"simulations":{"type":"array","items":{"$ref":"#/components/schemas/Simulation"}}}},"Simulation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"panelCount":{"type":"integer"},"panelModel":{"type":"string"},"powerKwc":{"type":"number"},"annualProdKwh":{"type":"number"},"monthlyProdKwh":{"type":"array","items":{"type":"number"},"description":"12 values (Jan-Dec)"},"annualSavingsEur":{"type":"number"},"systemCostEur":{"type":"number"},"roiYears":{"type":"number"},"co2AvoidedKg":{"type":"number"},"solarScore":{"type":"integer","minimum":0,"maximum":100}}},"Quote":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"totalAmountEur":{"type":"number","nullable":true,"description":"Null pour source=EXTERNAL_PDF quand le parsing PDF n'a pas extrait de total."},"validDays":{"type":"integer"},"lineItems":{"type":"array","items":{"type":"object"}},"accepted":{"type":"boolean"},"source":{"type":"string","enum":["PLATFORM","AGENT_SUBMITTED","TEMPLATE","EXTERNAL_PDF"]},"pdfUrl":{"type":"string","nullable":true},"externalQuoteId":{"type":"string","format":"uuid","nullable":true}}},"Installer":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"companyName":{"type":"string"},"city":{"type":"string"},"rating":{"type":"number","minimum":0,"maximum":5},"badge":{"type":"string","enum":["GOLD","SILVER","BRONZE","RGE"]},"certifications":{"type":"array","items":{"type":"string"}},"specialties":{"type":"array","items":{"type":"string"}}}},"InstallerTrustProfile":{"type":"object","description":"Profil de confiance descriptif (non prescriptif, IOBSP-safe). Agrège identité légale (SIRENE), certifications, documents légaux (KBIS/assurances), track record plateforme, événements externes (BODACC) et flags typés.","required":["installerId","generatedAt","version","identity","certifications","legalDocuments","trackRecord","externalEvents","flags","meta"],"properties":{"installerId":{"type":"string","format":"uuid"},"generatedAt":{"type":"string","format":"date-time"},"version":{"type":"string","enum":["v1"]},"identity":{"type":"object","properties":{"companyName":{"type":"string"},"city":{"type":"string"},"postalCode":{"type":"string"},"yearsExperience":{"type":"integer"},"guaranteeYears":{"type":"integer","nullable":true,"description":"Garantie commerciale déclarée par l'installateur (années) — null si non déclarée"},"claimed":{"type":"boolean"},"verified":{"type":"boolean"},"siret":{"type":"string","nullable":true},"sireneStatus":{"type":"string","enum":["active","ceased","unknown"]},"sireneNafCode":{"type":"string","nullable":true},"sireneVerifiedAt":{"type":"string","format":"date-time","nullable":true},"sireneDateCreation":{"type":"string","format":"date-time","nullable":true,"description":"Date d'immatriculation RCS de l'entité légale (distincte de profileCreatedAt qui est la date de création du profil sur build.solar)."},"sireneFormeJuridique":{"type":"string","nullable":true,"description":"Libellé forme juridique (SAS, SARL, EI, Auto-entrepreneur)."},"profileCreatedAt":{"type":"string","format":"date-time"}}},"certifications":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","description":"Libellé libre (ex. \"RGE QualiPV\")."},"status":{"type":"string","enum":["DECLARED","UPLOADED","VERIFIED"]},"issueDate":{"type":"string","format":"date-time","nullable":true},"expiryDate":{"type":"string","format":"date-time","nullable":true},"expired":{"type":"boolean"},"expiringWithin60Days":{"type":"boolean"}}}},"legalDocuments":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["KBIS","INSURANCE_DECENNALE","INSURANCE_RC_PRO","OTHER"]},"status":{"type":"string","enum":["DECLARED","UPLOADED","VERIFIED"]},"issueDate":{"type":"string","format":"date-time","nullable":true},"expiryDate":{"type":"string","format":"date-time","nullable":true},"reference":{"type":"string","nullable":true},"issuer":{"type":"string","nullable":true},"expired":{"type":"boolean"},"expiringWithin60Days":{"type":"boolean"}}}},"trackRecord":{"type":"object","properties":{"projectsCompleted":{"type":"integer"},"projectsActive":{"type":"integer"},"quotesAccepted":{"type":"integer"},"acceptanceRate":{"type":"number","minimum":0,"maximum":1},"medianQuoteResponseDays":{"type":"number","nullable":true,"description":"Null si < 3 dossiers (signal trop faible)."},"medianSignToInstallDays":{"type":"number","nullable":true},"medianInstallToCompleteDays":{"type":"number","nullable":true},"reviewCount":{"type":"integer"},"averageRating":{"type":"number","minimum":0,"maximum":5},"totalRevenueSignedEur":{"type":"number"}}},"flags":{"type":"array","description":"Drapeaux typés stables — citables par les agents. Pas de score agrégé délibérément (IOBSP-friendly + plus actionnable).","items":{"type":"object","required":["code","severity","message"],"properties":{"code":{"type":"string","enum":["recently_created","entity_recently_created","unclaimed_profile","sirene_unverified","sirene_ceased","rge_missing","rge_expired","rge_expiring_soon","insurance_decennale_missing","insurance_decennale_expired","insurance_decennale_expiring_soon","insurance_rc_pro_missing","kbis_missing","kbis_undated","kbis_outdated","low_track_record","bodacc_liquidation","bodacc_redressement","bodacc_sauvegarde","bodacc_procedure_unknown","bodacc_radiation","bodacc_vente_cession_recent"]},"severity":{"type":"string","enum":["high","medium","low","info"]},"message":{"type":"string"}}}},"externalEvents":{"type":"array","description":"Événements externes observés sur l'installateur (BODACC procédures collectives, radiation, vente). Limité aux 20 plus récents (< 18 mois). Pour audit complet, voir InstallerEvent côté admin.","items":{"type":"object","required":["source","kind","severity","observedAt"],"properties":{"source":{"type":"string","enum":["bodacc","inpi","platform"]},"kind":{"type":"string","description":"Ex. liquidation, redressement, sauvegarde, radiation, vente_cession."},"severity":{"type":"string","enum":["high","medium","low","info"]},"summary":{"type":"string","nullable":true},"observedAt":{"type":"string","format":"date-time"},"externalRef":{"type":"string","nullable":true}}}},"meta":{"type":"object","properties":{"iobspDisclaimer":{"type":"string"},"sources":{"type":"array","items":{"type":"string","enum":["sirene","platform","declared","modered","bodacc"]}}}}}},"TrustProfileSnapshot":{"type":"object","description":"Snapshot immuable d'un trust profile à un instant T. Capturé typiquement par un banquier au moment d'une décision underwriting — devient la preuve citable. Tamper-proof via HMAC SHA-256 (signatureValid retourné à la lecture). Pattern Stripe : public-by-id (UUID inguessable).","required":["id","installerId","profileVersion","generatedAt","profile","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"installerId":{"type":"string","format":"uuid"},"profileVersion":{"type":"string","enum":["v1"]},"generatedAt":{"type":"string","format":"date-time"},"profile":{"$ref":"#/components/schemas/InstallerTrustProfile"},"signature":{"type":"string","nullable":true,"description":"HMAC SHA-256 hex du JSON.stringify(profile). Null si TRUST_SNAPSHOT_HMAC_SECRET non configuré côté serveur."},"signatureValid":{"type":"boolean","description":"Vrai si la signature recalculée match. Permet à un consommateur tiers de vérifier l'intégrité."},"reason":{"type":"string","nullable":true,"enum":["banker_review","underwriting_decision","dispute_evidence","admin_audit","agent_capture","other"]},"createdByUserId":{"type":"string","format":"uuid","nullable":true},"createdByApiKeyId":{"type":"string","format":"uuid","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"TrustProfileDiff":{"type":"object","description":"Diff entre un snapshot historique et le trust profile actuel d'un installateur. Use case banquier : 'qu'est-ce qui a changé depuis ma décision'. Pure compute. Lookback non capé côté route (events fetch direct DB sans le cap 18 mois du trust profile standard).","required":["changed","flagsAdded","flagsRemoved","flagsSeverityChanged","newExternalEvents","identityChanged","trackRecordChanged","meta"],"properties":{"changed":{"type":"boolean","description":"Vrai si au moins un changement détecté."},"flagsAdded":{"type":"array","description":"Flags présents dans current mais pas dans snapshot. Tri par severity desc.","items":{"type":"object"}},"flagsRemoved":{"type":"array","description":"Flags présents dans snapshot mais pas dans current (résolus).","items":{"type":"object"}},"flagsSeverityChanged":{"type":"array","description":"Flags présents dans les deux, severity changée.","items":{"type":"object","properties":{"code":{"type":"string"},"fromSeverity":{"type":"string"},"toSeverity":{"type":"string"},"message":{"type":"string"}}}},"newExternalEvents":{"type":"array","description":"Événements BODACC postérieurs à snapshot.generatedAt. NON CAPÉ — fetch direct DB, garantit l'exhaustivité même pour snapshots > 18 mois.","items":{"type":"object"}},"identityChanged":{"type":"object","description":"Changements identitaires structurels banquier-relevant. Cosmétiques (city, postalCode) exclus.","properties":{"sireneStatus":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}}},"sireneFormeJuridique":{"type":"object","properties":{"from":{"type":"string","nullable":true},"to":{"type":"string","nullable":true}}},"sireneCompanyName":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}}},"claimed":{"type":"object","properties":{"from":{"type":"boolean"},"to":{"type":"boolean"}},"description":"Stub revendiqué — transition majeure."},"verified":{"type":"object","properties":{"from":{"type":"boolean"},"to":{"type":"boolean"}},"description":"Profil verified par admin."},"siret":{"type":"object","properties":{"from":{"type":"string","nullable":true},"to":{"type":"string","nullable":true}},"description":"SIRET changé — rare mais critique (rebranding/fusion)."}}},"trackRecordChanged":{"type":"object","description":"Évolution track record (signaux d'activité).","properties":{"projectsCompleted":{"type":"object","properties":{"from":{"type":"integer"},"to":{"type":"integer"},"delta":{"type":"integer"}}},"acceptanceRate":{"type":"object","properties":{"from":{"type":"number"},"to":{"type":"number"}},"description":"Diff seulement si delta absolu > 0.05 (anti-bruit faible volume)."},"reviewCount":{"type":"object","properties":{"from":{"type":"integer"},"to":{"type":"integer"}}}}},"meta":{"type":"object","properties":{"snapshotGeneratedAt":{"type":"string","format":"date-time"},"currentGeneratedAt":{"type":"string","format":"date-time"},"profileVersion":{"type":"string"}}}}},"InstallerDocument":{"type":"object","description":"Document légal d'un installateur (KBIS, assurance décennale, RC pro, autre). Calque sur InstallerCertification — modération admin via status.","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["KBIS","INSURANCE_DECENNALE","INSURANCE_RC_PRO","OTHER"]},"status":{"type":"string","enum":["DECLARED","UPLOADED","VERIFIED"]},"issueDate":{"type":"string","format":"date-time","nullable":true},"expiryDate":{"type":"string","format":"date-time","nullable":true},"reference":{"type":"string","nullable":true,"description":"N° contrat assurance, n° dépôt greffe, etc."},"issuer":{"type":"string","nullable":true,"description":"Émetteur (assureur, greffe…)."},"insurerContact":{"type":"string","nullable":true,"description":"Assurances : coordonnées de l'assureur, imprimées sur le devis (R111-2 9° C. conso.)."},"coverageArea":{"type":"string","nullable":true,"description":"Assurances : couverture géographique du contrat, imprimée sur le devis (R111-2 9°, art. 22-2 loi 96-603)."},"verifiedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"QuoteAuditResult":{"type":"object","description":"Résultat d'audit d'un devis solaire — score, red flags structurés, recommandation actionnable.","properties":{"score":{"type":"integer","minimum":0,"maximum":100},"recommendation":{"type":"string","enum":["KEEP","NEGOTIATE","REJECT"]},"redFlags":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","description":"Code stable, citable par l'agent (NO_SIRET, PRICE_TOO_HIGH, …)"},"severity":{"type":"string","enum":["high","medium","low","info"]},"message":{"type":"string"},"detail":{"type":"object","additionalProperties":true,"nullable":true}}}},"derived":{"type":"object","properties":{"pricePerWcEur":{"type":"number","nullable":true},"expectedPriceWindow":{"type":"object","properties":{"low":{"type":"number"},"median":{"type":"number"},"high":{"type":"number"}}},"eligiblePrimeAutoEur":{"type":"number","nullable":true},"tvaRegimeExpected":{"type":"string","enum":["reduite","normale","unknown"]}}},"summary":{"type":"string"},"engineVersion":{"type":"string"},"regulatoryVersion":{"type":"string"}}},"ExternalQuote":{"type":"object","description":"Devis obtenu hors plateforme, uploadé et parsé dans le dossier du client.","properties":{"id":{"type":"string","format":"uuid"},"projectId":{"type":"string","format":"uuid"},"pdfUrl":{"type":"string","format":"uri"},"fileName":{"type":"string","nullable":true},"pdfSize":{"type":"integer"},"installerName":{"type":"string","nullable":true},"installerSiret":{"type":"string","nullable":true},"totalAmountEur":{"type":"number","nullable":true},"powerKwc":{"type":"number","nullable":true},"validDays":{"type":"integer","nullable":true},"lineItems":{"type":"array","nullable":true,"items":{"type":"object","properties":{"description":{"type":"string"},"quantity":{"type":"number","nullable":true},"unitPrice":{"type":"number","nullable":true},"total":{"type":"number","nullable":true},"category":{"type":"string","nullable":true}}}},"notes":{"type":"string","nullable":true},"quoteNumber":{"type":"string","nullable":true,"description":"N° du devis tel qu'imprimé (le mandat et le PV le citent)."},"quoteDate":{"type":"string","format":"date-time","nullable":true,"description":"Date d'émission lue sur le devis — jamais la date d'import."},"vatRatePercent":{"type":"number","nullable":true,"description":"Taux de TVA appliqué, en pourcentage (5.5, 10, 20)."},"mentionsDecennale":{"type":"boolean","nullable":true,"description":"Assurance décennale mentionnée (false : parcouru sans la trouver ; null : illisible)."},"mentionsRge":{"type":"boolean","nullable":true,"description":"Qualification RGE / QualiPV mentionnée."},"hasWithdrawalForm":{"type":"boolean","nullable":true,"description":"Bordereau de rétractation présent."},"parseStatus":{"type":"string","enum":["PENDING","SUCCESS","PARTIAL","FAILED"]},"parseError":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"Communication":{"type":"object","description":"Message inter-comptes : acte typé porté par un canal rôle-agnostique, utilisable par les humains comme par leurs agents.","properties":{"id":{"type":"string","format":"uuid","description":"UUID. Pour le 1er message d'un fil, id === threadId."},"threadId":{"type":"string","format":"uuid","description":"Identifiant du fil de communication."},"parentId":{"type":"string","format":"uuid","nullable":true,"description":"Réponse imbriquée à un message précédent du même fil."},"act":{"type":"string","enum":["MESSAGE","PROPOSAL","COUNTER","ACCEPT","REJECT"],"description":"Type d'acte de langage."},"subjectType":{"type":"string","nullable":true,"description":"Polymorphe : Project, Quote, AdminStep, …"},"subjectId":{"type":"string","nullable":true,"description":"ID de la ressource sujet du fil."},"payload":{"type":"object","nullable":true,"additionalProperties":true,"description":"Données structurées typées code-side selon `act`. Pour PROPOSAL : { priceEur, installationDate, equipment, … }."},"note":{"type":"string","nullable":true,"description":"Texte libre rattaché à l'acte (pattern Stripe : action typée + note libre)."},"scope":{"type":"string","enum":["THREAD","FINANCIAL","TECHNICAL","PEER","ADMIN"],"default":"THREAD","description":"Visibilité du message dans le fil. THREAD = tous les participants ; les autres scopes filtrent par rôle."},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string","format":"uuid","description":"userId humain (ou propriétaire de la clé API)."},"apiKeyId":{"type":"string","format":"uuid","nullable":true,"description":"Si l'acte vient d'un agent (cohérent Quote.apiKeyId, Notification.apiKeyId)."}},"required":["id","threadId","act","scope","createdAt","createdBy"]},"CommunicationParticipant":{"type":"object","description":"Membre d'un fil de communication. Modèle thread-level : un user rejoint le fil une fois et voit tous les messages dans son scope autorisé.","properties":{"id":{"type":"string","format":"uuid"},"threadId":{"type":"string","format":"uuid"},"userId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["CUSTOMER","INSTALLER","ADMIN"],"description":"Rôle au moment de l'adhésion au fil."},"joinedAt":{"type":"string","format":"date-time"},"leftAt":{"type":"string","format":"date-time","nullable":true,"description":"null = participation active."},"acknowledgedAt":{"type":"string","format":"date-time","nullable":true,"description":"Dernier accusé lecture sur le fil."}},"required":["id","threadId","userId","role","joinedAt"]}},"parameters":{"page":{"name":"page","in":"query","schema":{"type":"integer","default":1,"minimum":1}},"limit":{"name":"limit","in":"query","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}},"idempotencyKey":{"name":"Idempotency-Key","in":"header","schema":{"type":"string","format":"uuid"},"description":"Unique key for idempotent requests. Responses cached 24h."},"regulatoryVersion":{"name":"v","in":"query","schema":{"type":"string"},"description":"Version du barème réglementaire à servir (ex: T2-2026). Si différente de la version courante, l'API renvoie 410 Gone avec l'URL canonique.","example":"T2-2026"},"ifNoneMatch":{"name":"If-None-Match","in":"header","schema":{"type":"string"},"description":"ETag du barème réglementaire déjà mis en cache (≡ \"<version>\"). Renvoie 304 Not Modified si inchangé.","example":"\"T2-2026\""}},"responses":{"RegulatoryGone":{"description":"Version réglementaire demandée non disponible — corps JSON contient currentVersion + canonicalUrl.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","enum":["VERSION_NOT_AVAILABLE"]},"requestedVersion":{"type":"string"},"currentVersion":{"type":"string"},"canonicalUrl":{"type":"string"}}}}}},"RegulatoryNotModified":{"description":"Le barème réglementaire est inchangé depuis la dernière consultation (ETag identique)."}}},"paths":{"/auth/login":{"post":{"summary":"Login","tags":["Auth"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string"}}}}}},"responses":{"200":{"description":"JWT token + user data"},"401":{"description":"Invalid credentials"}}}},"/auth/register":{"post":{"summary":"Register","tags":["Auth"],"description":"Crée un compte. `acceptTerms` doit valoir `true` : accepter les Conditions Générales d'Utilisation et la Politique de confidentialité est une condition de création (consentement horodaté sur le compte) — absent ou `false`, la route répond 400. L'inscription est réservée aux installateurs : `profileType` vaut INSTALLATEUR, valeur prise quand il est omis ; `company` et `city` créent le profil installateur minimal, et l'essai Pro démarre. Le lien de vérification de l'adresse part par courriel, jamais dans la réponse : la clé API et les gestes sortants (devis, invitations, partages) attendent une adresse vérifiée.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password","firstName","lastName","acceptTerms"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8,"description":"Au moins 8 caractères avec une majuscule, une minuscule et un chiffre — ou assez long pour s'en passer : 50 bits d'entropie au moins, comptés sur les familles présentes (minuscules 26, majuscules 26, chiffres 10, autres 33). Refus : 400, le message nomme ce qui manque."},"firstName":{"type":"string","minLength":1},"lastName":{"type":"string","minLength":1},"profileType":{"type":"string","enum":["INSTALLATEUR"],"default":"INSTALLATEUR","description":"Profil du compte : installateur, seule inscription ouverte (omis, c'est lui)."},"company":{"type":"string","maxLength":200,"description":"Raison sociale (installateur)."},"city":{"type":"string","maxLength":120,"description":"Ville (installateur)."},"acceptTerms":{"type":"boolean","const":true,"description":"Consentement aux CGU et à la politique de confidentialité : requis, seule la valeur `true` est acceptée."},"acquisitionSource":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$","description":"Provenance du compte (étiquette courte), facultative."},"acquisitionCampaign":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$","description":"Campagne (étiquette courte), facultative."}}}}}},"responses":{"201":{"description":"Compte créé : `user` et `token` (JWT, aussi posé en cookie de session). Le courriel de vérification est envoyé ; aucune URL de vérification dans la réponse."},"400":{"description":"Champ manquant ou invalide — `acceptTerms` absent ou `false`, mot de passe hors règle, adresse invalide (message en français)."},"403":{"description":"Profil autre qu'installateur : l'inscription est réservée aux installateurs."},"409":{"description":"Un compte existe déjà avec cette adresse."},"429":{"description":"Trop de tentatives (par adresse IP et par adresse email)."}}}},"/auth/me":{"get":{"summary":"Get current user","tags":["Auth"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Profil du compte, avec `planInfo` : plan, essai en cours (`isTrialing`, `trialEndsAt`, `trialDaysRemaining`), `hasStripeCustomer` et `subscription` — l'abonnement en cours (`status` ACTIVE | TRIALING | PAST_DUE, `currentPeriodEnd` = prochaine échéance, `cancelAt` = date de fin d'une résiliation programmée, sinon null), ou null sans abonnement (plan gratuit, essai, plan accordé hors paiement). Installateur : `capInfo` (dossiers actifs, plafond du plan gratuit).","responses":{"200":{"description":"User profile"}}},"patch":{"summary":"Update profile","tags":["Auth"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"phone":{"type":"string"},"city":{"type":"string"},"company":{"type":"string"},"currentPassword":{"type":"string","description":"Requis avec newPassword ou email"},"newPassword":{"type":"string","minLength":8,"description":"Au moins 8 caractères avec une majuscule, une minuscule et un chiffre — ou assez long pour s'en passer : 50 bits d'entropie au moins, comptés sur les familles présentes (minuscules 26, majuscules 26, chiffres 10, autres 33). Refus : 400, le message nomme ce qui manque."},"email":{"type":"string","format":"email","description":"Corriger l'adresse d'un compte NON vérifié (faute de frappe à l'inscription). Exige currentPassword ; l'adresse est écrite en minuscules, un nouveau lien de vérification part vers elle (verificationSent dans la réponse) et le lien envoyé à l'ancienne cesse de valoir. 409 si l'adresse du compte est déjà vérifiée, si la nouvelle est déjà celle d'un compte, ou si un abonnement y est rattaché."}}}}}},"responses":{"200":{"description":"Updated user (+ verificationSent quand l'adresse a été corrigée)"},"400":{"description":"Validation, mot de passe actuel requis ou incorrect, adresse inchangée"},"409":{"description":"Adresse déjà vérifiée, déjà utilisée, ou rattachée à un abonnement"},"429":{"description":"Trop de corrections d'adresse (même limiteur que le renvoi du lien)"}}}},"/user/export":{"get":{"summary":"Export toutes les données personnelles (RGPD art. 15)","tags":["Auth","RGPD"],"security":[{"bearerAuth":[]}],"description":"Retourne un JSON téléchargeable contenant l'intégralité des données personnelles liées au compte. Secrets (hashes, tokens) exclus. Limité à 3 exports par 24h. Session de la personne seulement : une clé API reçoit 403.","responses":{"200":{"description":"JSON téléchargeable (Content-Disposition: attachment)"},"401":{"description":"Authentification requise"},"403":{"description":"Demandé avec une clé API"},"429":{"description":"Rate limit dépassé"}}}},"/user/delete":{"delete":{"summary":"Supprimer le compte et anonymiser les données (RGPD art. 17)","tags":["Auth","RGPD"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Soft-delete le compte puis anonymise les PII sur User, Leads convertis, Projects (+ clients offline), QuoteRequests (message/building/timeline), Simulations (inputsSnapshot) et supprime les ProjectNotes. Désactive l'InstallerProfile le cas échéant. Les Quotes émis par les installateurs sont conservés pour traçabilité B2B. Exige confirm=\"SUPPRIMER\" et, pour les comptes password, le mot de passe actuel.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["confirm"],"properties":{"confirm":{"type":"string","enum":["SUPPRIMER"]},"password":{"type":"string","description":"Obligatoire pour les comptes password-based"}}}}}},"responses":{"200":{"description":"Compte anonymisé, cookie effacé, résumé des mutations"},"400":{"description":"Confirmation ou mot de passe manquant"},"401":{"description":"Authentification requise"},"403":{"description":"Mot de passe incorrect"},"429":{"description":"Rate limit dépassé"}}}},"/projects":{"get":{"summary":"List projects","tags":["Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Dossiers du lecteur : client propriétaire, ou installateur (dossiers hors ligne créés par lui, self-owned, devis accepté). Par défaut les dossiers actifs pour sa partie ; `archived=true` liste ceux qu'il a archivés, `archived=all` tout. L'archivage est propre à chaque partie (client propriétaire / installateur) : archiver ne change rien pour l'autre.","parameters":[{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/limit"},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"q","in":"query","schema":{"type":"string"},"description":"Recherche texte : nom du dossier, adresse, ville, client"},{"name":"archived","in":"query","schema":{"type":"string","enum":["false","true","all"]},"description":"Absent ou false : dossiers actifs pour la partie du lecteur ; true : ses dossiers archivés ; all : tout"}],"responses":{"200":{"description":"Paginated project list"}}},"post":{"summary":"Create project","tags":["Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/idempotencyKey"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["name","address"],"properties":{"name":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"type":{"type":"string"}}}}}},"responses":{"201":{"description":"Created project"}}}},"/projects/{id}":{"get":{"summary":"Get project (read-only, accessible to owner, admin, and installers in bidding phase)","tags":["Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Project with simulations + quote requests"},"404":{"description":"Not visible to caller"}}},"patch":{"summary":"Update project (owner, offline-installer, or installer with ACCEPTED quote)","tags":["Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"description":"Strict schema. Champs autorisés : name, address, city, postalCode, latitude, longitude, type, clientName, clientEmail, clientPhone, cadastrePrefixe/Section/Parcelle/SuperficieM2, pdl, archived. Le PDL est validé (14 chiffres) et normalisé (espaces/tirets retirés) avant écriture. La validation tourne AVANT le findFirst d'accès — un body invalide retourne 400 même sur un projet inconnu. `archived: true` archive le dossier pour la partie de l'appelant (client propriétaire → archivedByOwnerAt, installateur → archivedByInstallerAt) : il quitte ses listes, ses comptes, ses urgences et ses alertes, reste consultable, et l'autre partie n'est pas concernée ; `archived: false` le restaure. Idempotent. Un dossier archivé par l'installateur ne compte plus dans le plafond du plan Gratuit ; la restauration d'un dossier initié par lui (hors ligne ou self-owned, actif) repasse par ce plafond : 403 INSTALLER_PROJECT_CAP_REACHED si atteint.","requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","minLength":1},"address":{"type":"string","minLength":1},"city":{"type":"string"},"postalCode":{"type":"string"},"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180},"type":{"type":"string"},"clientName":{"type":"string"},"clientEmail":{"type":"string","format":"email"},"clientPhone":{"type":"string"},"cadastrePrefixe":{"type":"string","maxLength":3,"nullable":true},"cadastreSection":{"type":"string","maxLength":2,"nullable":true},"cadastreParcelle":{"type":"string","maxLength":4,"nullable":true},"cadastreSuperficieM2":{"type":"integer","minimum":0,"maximum":99999999,"nullable":true},"pdl":{"type":"string","description":"PDL Enedis 14 chiffres. Espaces/tirets acceptés en saisie, normalisés serveur. '' ou null pour reset.","nullable":true},"archived":{"type":"boolean","description":"true : archiver le dossier pour la partie de l'appelant (réversible, sans effet sur l'autre partie) ; false : le restaurer"}}}}}},"responses":{"200":{"description":"Project mis à jour"},"400":{"description":"Validation Zod (clé inconnue, format) ou PDL invalide (≠ 14 chiffres)"},"403":{"description":"archived : rôle sans partie (ni client propriétaire ni installateur), ou plafond du plan Gratuit atteint à la restauration (INSTALLER_PROJECT_CAP_REACHED)"},"404":{"description":"Projet non accessible (inexistant ou caller pas owner/installateur ACCEPTED)"}}},"delete":{"summary":"Supprimer un dossier (suppression logique) tant qu'aucun échange n'a eu lieu","tags":["Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"description":"Mêmes droits d'écriture que PATCH (client propriétaire, installateur sur son dossier hors ligne, installateur avec devis accepté). Règle « aucun échange » : le dossier se supprime tant qu'aucun devis n'a été émis, aucune signature demandée, aucune pièce déposée, aucune demande de devis envoyée aux installateurs (dossier plateforme), aucune invitation ni lien de partage créé, aucun message envoyé au client. Le statut ne décide pas : un dossier créé au palier « Devis signé » par erreur reste supprimable, un dossier simulé dont le devis est parti ne l'est plus. Suppression logique (deletedAt) : le dossier disparaît des listes, des quotas et des partages ; aucune restauration par l'API. Un dossier engagé reste consultable.","responses":{"200":{"description":"{ success: true }"},"401":{"description":"Authentification requise"},"404":{"description":"Projet non accessible en écriture (inexistant, supprimé ou appelant sans droit)"},"409":{"description":"Un échange a eu lieu — le message les nomme (« un devis a été émis, une pièce a été déposée »)"}}}},"/projects/{id}/simulations":{"post":{"summary":"Run simulation","tags":["Simulations"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Seuls panelCount et panelModel sont requis : production, économies, coût, retour sur investissement et CO₂ sont calculés côté serveur (un champ fourni est ignoré). La production vient de PVGIS (Commission européenne) pour l'inclinaison et l'orientation dites — face à l'équateur par défaut : sud, nord à La Réunion et à Mayotte —, du modèle du moteur quand PVGIS ne répond pas. Pas d'Idempotency-Key sur cette route.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["panelCount","panelModel"],"properties":{"panelCount":{"type":"integer"},"panelModel":{"type":"string"},"panelWattage":{"type":"integer"},"powerKwc":{"type":"number"},"annualProdKwh":{"type":"number"},"monthlyProdKwh":{"type":"array","items":{"type":"number"}},"annualSavingsEur":{"type":"number"},"systemCostEur":{"type":"number"},"roiYears":{"type":"number"},"co2AvoidedKg":{"type":"number"}}}}}},"responses":{"201":{"description":"Simulation results saved"}}}},"/projects/{id}/technical-study":{"patch":{"summary":"Declare the technical study (the installer's own sizing)","tags":["Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Déclare les chiffres du dimensionnement fait avec vos propres outils : ils deviennent la source des devis pré-remplis (`POST /quotes/generate`, page dossier) et de tous les documents (Cerfa DP, récapitulatifs Enedis, CONSUEL, mise en service, EDF OA, fiche technique), avant toute simulation. La saisie accomplit l'étape `simulation` (Étude technique) — journal StepActivity, source `technical_study_declared`. `azimuth` en boussole (0 = nord, 180 = sud). Batterie : `hasBattery` explicite (vrai ou faux), `batteryCapKwh` (énergie stockable), `batteryPowerKw` (puissance maximale, charge et décharge) et `batteryCoupling` (`DC` : onduleur hybride, côté continu ; `AC` : onduleur-chargeur séparé, côté alternatif) — le CONSUEL passe en attestation violette et dossier SC 144C (en bus AC, le certificat EN 50549 de l'onduleur-chargeur s'ajoute), le récap de raccordement porte le bloc stockage demandé par Enedis, le récap EDF OA rappelle que l'énergie stockée doit venir des seuls panneaux, le devis pré-rempli ajoute la ligne batterie et passe toute l'opération au taux normal de TVA ; GET admin-steps sert `storage` sur ces étapes. Batterie ajoutée à une installation déjà en service : `existingInstallation` vrai (avec `hasBattery`) — les panneaux déclarés sont ceux de l'existant, le devis pré-rempli ne chiffre que la batterie (pose et mise en service en forfaits, taux normal), la DP et la TVA réduite deviennent sans objet (`storage.notApplicable`, à marquer « Non applicable » — l'API refuse ce statut sur la DP de tout autre dossier), les récapitulatifs décrivent la déclaration à Enedis (CACSI seule sans injection, modification de l'installation avec vente de surplus), le SC 144C limité à la partie neuve, l'enregistrement par Enedis après visa et la déclaration à EDF OA sur le contrat en cours. Le corps remplace l'étude déclarée en entier (pas de fusion champ à champ). Une étude qui change après le dépôt de la déclaration préalable : l'étude d'avant — celle des pièces déposées — est gardée sur l'étape `dp` au premier changement (`metadata.studyAtDeposit`), quand elle est sûre ; l'assistance le remarque (GET /projects/{id}/remarks).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["powerKwc","panelCount","panelModel"],"properties":{"powerKwc":{"type":"number","description":"Puissance crête (kWc)"},"panelCount":{"type":"integer"},"panelModel":{"type":"string","maxLength":120},"panelWattage":{"type":"integer","description":"Wc par panneau (déduit de kWc / nombre si omis)"},"annualProdKwh":{"type":"number"},"inverterModel":{"type":"string","maxLength":120},"microInverters":{"type":"boolean","nullable":true,"description":"Micro-onduleurs (vrai) ou onduleur de chaîne / hybride (faux) : le dossier technique SC 144 du CONSUEL en dépend — SC 144C2-2 avec micro-onduleurs, SC 144A-5 ou SC 144C-5 sinon ; omis : non déclaré, le récap CONSUEL nomme les variantes possibles."},"phase":{"type":"string","enum":["monophase","triphase"],"nullable":true,"description":"Branchement monophasé ou triphasé : Enedis Connect le demande (type de branchement). Ce n'est pas la phase de l'onduleur : sur un branchement monophasé, le dossier technique SC 144 coche « onduleur monophasé » ; sur un triphasé, la phase de l'onduleur se dit à l'étape CONSUEL (`consuelTechnique.inverterPhase`). Omis : non déclaré, les récaps le disent à préciser — jamais déduit de la puissance."},"inverterPowerKva":{"type":"number","nullable":true,"description":"Puissance de l'onduleur côté alternatif, en kVA (somme des micro-onduleurs) : la puissance installée que demande Enedis Connect ; la puissance de raccordement en injection est au plus celle-là, 6 kVA au plus en monophasé (au-delà, branchement triphasé) ; omis : non déclarée."},"azimuth":{"type":"number","minimum":0,"maximum":360},"tilt":{"type":"number","minimum":0,"maximum":90},"hasBattery":{"type":"boolean"},"batteryCapKwh":{"type":"number","description":"Énergie stockable (kWh)"},"batteryPowerKw":{"type":"number","description":"Puissance maximale de la batterie (kW), charge et décharge"},"batteryCoupling":{"type":"string","enum":["DC","AC"],"description":"DC : onduleur hybride (PV sur bus DC) ; AC : onduleur-chargeur séparé (PV sur bus AC)"},"existingInstallation":{"type":"boolean","description":"La batterie s'ajoute à une installation photovoltaïque déjà en service (sans effet sans `hasBattery`) : devis batterie seule, DP et TVA réduite sans objet, déclaration à Enedis au lieu d'une demande, SC 144C limité à la partie neuve, contrat EDF OA en cours"}}}}}},"responses":{"200":{"description":"Étude enregistrée (`declaredStudy`) ; étape `simulation` DONE"},"400":{"description":"Étude invalide : puissance, nombre de panneaux et modèle requis"},"404":{"description":"Dossier introuvable ou inaccessible"}}}},"/projects/{id}/remarks":{"get":{"summary":"What the assistance noticed on the dossier, unasked","tags":["Projects","Agent"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Ce que l'assistance remarque d'elle-même sur le dossier — ce que le brief du matin dit à l'installateur sans qu'il le demande. Chaque remarque : `id`, `stepKey` (l'étape où elle se range), `key` (les faits sur lesquels elle porte, à renvoyer pour y répondre), `status` (`question` : un fait manque ; `piece` : une pièce simple à faire ; `info` : à savoir ; `done` : la pièce est faite), `headline`, `text`, `question` (`text`, `choices`), `piece` (`template`, `label`, `actionLabel`, `made`), `sources` (pages officielles lues, avec leur date), `href`, `toldAt`, `dismissed`. La pièce d'une remarque se fait par POST /projects/{id}/admin-steps/documents/generate avec `{ template: piece.template }` : elle est rangée dans les pièces de l'étape (abonnement Pro). Mêmes parties que la checklist.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`remarks` : les remarques qui s'appliquent au dossier, écartées comprises (`dismissed`)"},"404":{"description":"Dossier introuvable ou inaccessible"}}},"post":{"summary":"Answer or dismiss a remark (installer)","tags":["Projects","Agent"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Répondre à la question d'une remarque (`action: answer`, `value` parmi `question.choices[].value`) ou l'écarter pour ce dossier (`action: dismiss` — « Pas concerné », ou « C'est noté » sur ce qui est à savoir). `key` est celle de la remarque lue : si les faits du dossier ont changé depuis, rien n'est écrit (409, relire GET). La réponse se garde et ne se redemande pas, sauf si les faits changent. Rend `remark` telle qu'elle se dit désormais (null : plus rien à dire). Rôle installateur (ou administrateur).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["remark","action","key"],"properties":{"remark":{"type":"string","enum":["certificat-non-opposition","etude-modifiee-apres-depot","consuel-organisme-inspection"]},"action":{"type":"string","enum":["answer","dismiss"]},"key":{"type":"string","maxLength":200,"description":"La clé de la remarque lue (GET)"},"value":{"type":"string","nullable":true,"description":"Avec `answer` : une des valeurs de `question.choices`"}}}}}},"responses":{"200":{"description":"`remark` (ou null)"},"400":{"description":"Corps invalide (remarque inconnue, action, clé)"},"403":{"description":"Rôle installateur requis"},"404":{"description":"Dossier introuvable ou inaccessible"},"409":{"description":"Les faits du dossier ont changé depuis la lecture : relire GET"},"422":{"description":"Réponse hors des choix, ou remarque sans question"}}}},"/quotes/parse":{"post":{"summary":"Read a signed quote (PDF, or a photo of the signed paper quote) without creating anything","tags":["Quotes","Agent"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Lit un devis (le devis signé avec un autre outil : un PDF, ou une photo du devis signé sur papier — JPEG, PNG, WebP — mise en PDF d'une page, redressée) sans rien créer : `data` (émetteur, SIRET, n° et date du devis, montant, taux de TVA, lignes, puissance, client, adresse du chantier, modules, mentions décennale / RGE / bordereau de rétractation — une valeur illisible reste nulle), `prefill` (ce que la fenêtre « Nouveau dossier client » pré-remplit : client, email, téléphone, adresse, code postal, ville), `proposedStudy` (puissance, nombre et modèle des modules, puissance unitaire, onduleur — ou null), `token`, à renvoyer à POST /projects/offline avec le même fichier pour que rien ne soit relu (jeton lié au compte et à l'empreinte du PDF, deux heures), et `pdf` (photo seulement : le PDF produit, gardé au stockage — `fileUrl`, `fileKey`, `fileName`, `fileSize` — à déclarer à la création tel quel, la photo d'origine étant retirée ; null pour un PDF). Deux corps : **JSON** avec `fileUrl` (et `fileKey`, `fileName`) d'un PDF déjà téléversé dans le stockage build.solar (POST /upload en multipart ≤ 4,5 Mo, ou l'upload client direct de POST /upload/token sans cette limite — le chemin de la fenêtre) ; **multipart/form-data** avec le fichier (`file`), qui transite par la fonction — 4,5 Mo au plus en production (limite de la plateforme), quoi que dise le plafond des documents. Rôle installateur. 20 lectures par heure et par compte (429 `RATE_LIMITED`). Hors quota, comme l'import d'un devis sur un dossier.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileUrl"],"properties":{"fileUrl":{"type":"string","description":"URL rendue par le téléversement — dans le stockage build.solar seulement (Vercel Blob, ou /uploads/ en local)."},"fileKey":{"type":"string","description":"Clé de stockage rendue par le téléversement (`media/AAAA-MM/<id>.pdf`) ; déduite de l'URL si absente."},"fileName":{"type":"string","description":"Nom lisible du fichier."},"fileSize":{"type":"integer","description":"Taille en octets, déclarative : c'est le contenu relu qui est borné (20 Mo)."}}}},"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"application/pdf, ou image/jpeg, image/png, image/webp (photo du devis signé sur papier, mise en PDF) — 4,5 Mo au plus par cet appel (limite de la plateforme) ; au-delà, téléversez d'abord et passez `fileUrl` en JSON."}}}}}},"responses":{"200":{"description":"`status` (SUCCESS | PARTIAL), `model`, `data`, `prefill`, `proposedStudy`, `token`, `pdf` (photo mise en PDF : le fichier à déclarer ensuite ; null pour un PDF)."},"400":{"description":"Fichier absent ou vide ; `fileUrl` hors du stockage build.solar."},"404":{"description":"`fileUrl` ne correspond à aucun fichier du stockage."},"413":{"description":"PDF trop lourd (plafond des documents, 20 Mo)."},"415":{"description":"Ni un PDF ni une photo (JPEG, PNG, WebP), ou ni JSON ni multipart."},"422":{"description":"Devis illisible : compléter à la main, le PDF reste joignable à la création."},"429":{"description":"Plus de 20 lectures dans l'heure pour ce compte (`RATE_LIMITED`)."},"503":{"description":"Lecture IA non activée (`SERVICE_NOT_ACTIVE`)."}}}},"/quotes":{"post":{"summary":"Submit quote (installer only)","tags":["Quotes"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/idempotencyKey"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["quoteRequestId","totalAmountEur","lineItems"],"properties":{"quoteRequestId":{"type":"string","format":"uuid"},"totalAmountEur":{"type":"number","description":"Total TTC. Doit rester entre 90 % et 130 % de la somme des lignes (tolérance HT ↔ TTC / remise), sinon 400."},"validDays":{"type":"integer","default":30},"lineItems":{"type":"array","minItems":1,"description":"Au moins une ligne, au schéma normalisé (services/quote-line-items).","items":{"type":"object","required":["category","label","unitPriceEur","totalEur"],"properties":{"category":{"type":"string","enum":["PANELS","INVERTER","BATTERY","MOUNTING","INSTALLATION","ADMIN_FEES","OTHER"]},"label":{"type":"string","maxLength":200},"quantity":{"type":"number","minimum":0.01,"default":1},"unitPriceEur":{"type":"number","minimum":0},"totalEur":{"type":"number","minimum":0},"brand":{"type":"string","maxLength":80},"model":{"type":"string","maxLength":120},"warrantyYears":{"type":"integer","minimum":0,"maximum":50}}}},"notes":{"type":"string"},"terms":{"type":"object","description":"Conditions du devis, imprimées sur le PDF (section « Délais et paiement »). Une clé absente reprend la condition par défaut du profil (InstallerProfile.quoteDefaults) ; une clé à null ou vide reste vide (le devis imprime « — »). Un délai global ne suffit pas : livraison, pose, démarches et mise en service sont distincts.","additionalProperties":false,"properties":{"paymentTerms":{"type":"string","maxLength":300,"nullable":true,"description":"Modalités de paiement (aucun paiement avant 7 jours pour un contrat hors établissement, art. L221-10)."},"deliveryDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai de livraison du matériel."},"installationDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai de pose."},"adminDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai des démarches administratives (déclaration préalable, raccordement)."},"commissioningDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai de raccordement et de mise en service."}}}}}}}},"responses":{"201":{"description":"Quote submitted. The issuer's legal mentions, the quote terms and its VAT rate are frozen at issue (the PDF regenerated later prints what the client received). `missingMentions` is always empty: a quote missing a mandatory mention is refused with 422 QUOTE_MENTIONS_MISSING before anything is created (field kept for compatibility). `qualificationReminder` is a sentence when no photovoltaic qualification (RGE QualiPV, RGE Qualifelec SPV, Qualibat…) is declared on the installer profile, null otherwise: the reduced VAT rate and the purchase contract depend on it — it is not a consumer-law mention and never appears in `missingMentions`. VAT: one rate for the whole operation — 20 % above 9 kWc, when a battery line is present, for a non-residential project or when the « tva-reduite » step is NOT_APPLICABLE; none under the VAT franchise (vatExempt). `delivery` tells what actually reached the client: emailSent true | false | null (null = client with an account, notified in-app), reason NO_CLIENT_EMAIL | EMAIL_UNAVAILABLE when false. The same truth is recorded on the quote with the address and the time — `quote.delivery` = { emailSent, reason, to, at } (null on quotes issued before 2026-10-28 or imported) — and read back by the dossier."},"403":{"description":"Installer role required"},"422":{"description":"A mandatory mention of the quote is missing: nothing is created, no email leaves (code QUOTE_MENTIONS_MISSING).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteMentionsMissing"}}}}}}},"/quotes/preview":{"post":{"summary":"Preview the quote PDF before sending (installer only)","tags":["Quotes","Agent"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Le PDF tel que le client le recevra, sans rien enregistrer ni envoyer : même corps, même validation et même assemblage que POST /quotes (émetteur, conditions, taux de TVA du dossier). Seul le n° manque — il est attribué à l'envoi — et chaque page porte « Aperçu — non envoyé ». Ni email vérifié exigé, ni quota consommé.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["quoteRequestId","totalAmountEur","lineItems"],"properties":{"quoteRequestId":{"type":"string","format":"uuid"},"totalAmountEur":{"type":"number","description":"Total TTC. Doit rester entre 90 % et 130 % de la somme des lignes (tolérance HT ↔ TTC / remise), sinon 400."},"validDays":{"type":"integer","default":30},"lineItems":{"type":"array","minItems":1,"description":"Au moins une ligne, au schéma normalisé (services/quote-line-items).","items":{"type":"object","required":["category","label","unitPriceEur","totalEur"],"properties":{"category":{"type":"string","enum":["PANELS","INVERTER","BATTERY","MOUNTING","INSTALLATION","ADMIN_FEES","OTHER"]},"label":{"type":"string","maxLength":200},"quantity":{"type":"number","minimum":0.01,"default":1},"unitPriceEur":{"type":"number","minimum":0},"totalEur":{"type":"number","minimum":0},"brand":{"type":"string","maxLength":80},"model":{"type":"string","maxLength":120},"warrantyYears":{"type":"integer","minimum":0,"maximum":50}}}},"notes":{"type":"string"},"terms":{"type":"object","description":"Conditions du devis, imprimées sur le PDF (section « Délais et paiement »). Une clé absente reprend la condition par défaut du profil (InstallerProfile.quoteDefaults) ; une clé à null ou vide reste vide (le devis imprime « — »). Un délai global ne suffit pas : livraison, pose, démarches et mise en service sont distincts.","additionalProperties":false,"properties":{"paymentTerms":{"type":"string","maxLength":300,"nullable":true,"description":"Modalités de paiement (aucun paiement avant 7 jours pour un contrat hors établissement, art. L221-10)."},"deliveryDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai de livraison du matériel."},"installationDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai de pose."},"adminDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai des démarches administratives (déclaration préalable, raccordement)."},"commissioningDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai de raccordement et de mise en service."}}}}}}}},"responses":{"200":{"description":"application/pdf — Content-Disposition inline, `apercu-devis-<client>.pdf`","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Corps invalide (mêmes messages que POST /quotes) ou demande fermée"},"403":{"description":"Installer role required, ou demande réservée à un autre installateur"},"404":{"description":"Demande de devis introuvable ou dossier hors de portée"}}}},"/quotes/request":{"get":{"summary":"List quote requests","tags":["Quotes"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"Quote requests with quotes"}}},"post":{"summary":"Request quotes","tags":["Quotes"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["projectId"],"properties":{"projectId":{"type":"string","format":"uuid"},"installerId":{"type":"string","format":"uuid"},"message":{"type":"string"}}}}}},"responses":{"201":{"description":"Quote request created"}}}},"/quotes/last":{"get":{"summary":"Dernier devis émis par l'installateur (lignes à reprendre)","tags":["Quotes","Agent"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Le dernier devis émis depuis la plateforme par l'installateur authentifié (grille, lignes éditées ou agent — jamais un PDF importé), avec ses lignes normalisées : « Reprendre les lignes de mon dernier devis » dans l'éditeur. `{ quote: null }` sans devis.","responses":{"200":{"description":"`quote` (id, number, createdAt, totalAmountEur, projectName, clientName, lineItems) ou null"},"403":{"description":"Installer role required"}}}},"/quotes/{id}":{"get":{"summary":"Get quote detail","tags":["Quotes"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Quote with installer and project info"}}},"patch":{"summary":"Accept quote","tags":["Quotes"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"accepted":{"type":"boolean"},"status":{"type":"string","enum":["ACCEPTED"]}}}}}},"responses":{"200":{"description":"Quote accepted, project status updated"}}}},"/quotes/request/{id}":{"get":{"summary":"Get quote request detail","tags":["Quotes"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Quote request with quotes"}}},"patch":{"summary":"Update quote request status (state machine enforced)","tags":["Quotes"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"description":"Transitions: PENDING→QUOTED|EXPIRED|DECLINED, QUOTED→ACCEPTED|DECLINED|EXPIRED. Terminal states (ACCEPTED/DECLINED/EXPIRED) cannot be changed. Caller must own the quote request (client) or be the targeted/broadcast installer or admin.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["QUOTED","ACCEPTED","DECLINED","EXPIRED"]}}}}}},"responses":{"200":{"description":"Updated"},"400":{"description":"Invalid state transition"},"403":{"description":"Not authorized for this quote request"}}}},"/installers/{id}":{"get":{"summary":"Get installer profile","tags":["Installers"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Profil entreprise de l’installateur : vue publique (certifications modérées, contact sans email), profil complet pour son propriétaire et l’ADMIN"},"404":{"description":"Installateur introuvable"}}},"patch":{"summary":"Update installer profile (installer role + ownership required)","tags":["Installers"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"description":"Si `siret` est fourni, validé Luhn (rejet 400 si invalide, AVANT le hit DB). Si `verify_sirene` n'est pas false, la route appelle SIRENE pour enrichir sireneCompanyStatus / sireneNafCode / sireneVerifiedAt — limité à 10 verifs/min/userId (au-delà, SIRET sauvé sans snapshot). `siret: \"\"` ou `null` reset le SIRET et son snapshot.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"companyName":{"type":"string"},"description":{"type":"string"},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$"},"address":{"type":"string","description":"Rue du siège (mention émetteur du devis) — '' pour effacer."},"city":{"type":"string"},"postalCode":{"type":"string"},"certifications":{"type":"array","items":{"type":"string"}},"specialties":{"type":"array","items":{"type":"string"}},"guaranteeYears":{"type":"integer","minimum":1,"maximum":50,"nullable":true,"description":"Garantie installateur imprimée sur le devis (années) ; null la retire — non déclarée, le devis n'en dit rien."},"serviceRadius":{"type":"integer","minimum":5,"maximum":200},"avgResponseTime":{"type":"string"},"website":{"type":"string","format":"uri","nullable":true},"aiAgentUrl":{"type":"string","format":"uri","nullable":true},"logoUrl":{"type":"string","format":"uri","nullable":true},"siret":{"type":"string","description":"SIRET 14 chiffres + Luhn. Espaces/tirets acceptés en saisie, normalisés serveur. '' ou null pour reset.","nullable":true},"verify_sirene":{"type":"boolean","description":"Default true. Passe à false pour bypasser le lookup SIRENE (testing, ou API down)."},"legalForm":{"type":"string","maxLength":80,"nullable":true,"description":"Forme juridique imprimée sur le devis (« SAS », « SARL », « EI »…) — à défaut, celle relevée par SIRENE. '' ou null efface."},"shareCapitalEur":{"type":"integer","minimum":1,"nullable":true,"description":"Capital social en euros (sociétés, R123-238 C. com.)."},"registry":{"type":"string","enum":["RCS","RNE"],"nullable":true,"description":"Registre d'immatriculation (R111-2 3°) : RCS (avec rcsCity) ou RNE (métiers et artisanat)."},"rcsCity":{"type":"string","maxLength":80,"nullable":true,"description":"Ville du greffe (mention « RCS Ville SIREN »)."},"vatNumber":{"type":"string","maxLength":24,"nullable":true,"description":"N° de TVA intracommunautaire FR (clé contrôlée contre le SIREN du SIRET) — jamais calculé."},"vatExempt":{"type":"boolean","description":"Franchise en base de TVA (art. 293 B du CGI) : les devis ne décomposent aucune TVA."},"mediatorName":{"type":"string","maxLength":200,"nullable":true,"description":"Médiateur de la consommation (L616-1)."},"mediatorWebsite":{"type":"string","maxLength":300,"nullable":true,"description":"Site du médiateur (R616-1) — « https:// » ajouté si absent."},"quoteDefaults":{"type":"object","description":"Conditions par défaut des devis (pré-remplissent chaque devis). null efface.","additionalProperties":false,"properties":{"paymentTerms":{"type":"string","maxLength":300,"nullable":true,"description":"Modalités de paiement (aucun paiement avant 7 jours pour un contrat hors établissement, art. L221-10)."},"deliveryDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai de livraison du matériel."},"installationDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai de pose."},"adminDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai des démarches administratives (déclaration préalable, raccordement)."},"commissioningDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai de raccordement et de mise en service."}},"nullable":true},"priceList":{"type":"object","description":"« Mes prix » — catalogue personnel (voir le schéma) ; les seules valeurs renseignées sont gardées, null efface tout.","properties":{"panelModel":{"type":"string","maxLength":120,"nullable":true,"description":"Panneau habituel (proposé par défaut à l'étude technique)."},"panelWattage":{"type":"integer","minimum":50,"maximum":2000,"nullable":true,"description":"Puissance du panneau habituel (Wc)."},"panelUnitEur":{"type":"number","minimum":0,"nullable":true,"description":"Un panneau, fourni (€ TTC)."},"inverterModel":{"type":"string","maxLength":120,"nullable":true,"description":"Onduleur ou micro-onduleurs habituels."},"inverterPerKwcEur":{"type":"number","minimum":0,"nullable":true,"description":"Onduleur, fourni (€ TTC par kWc installé)."},"cablingPerKwcEur":{"type":"number","minimum":0,"nullable":true,"description":"Câblage, protections, fixations (€ TTC par kWc)."},"laborPerPanelEur":{"type":"number","minimum":0,"nullable":true,"description":"Pose (€ TTC par panneau)."},"commissioningEur":{"type":"number","minimum":0,"nullable":true,"description":"Mise en service et démarches (€ TTC, forfait)."},"batteryEur":{"type":"number","minimum":0,"nullable":true,"description":"Batterie fournie et posée (€ TTC, forfait) — sans valeur, prix de marché de l'option."},"evChargerEur":{"type":"number","minimum":0,"nullable":true,"description":"Borne de recharge (€ TTC, forfait) — sans valeur, prix de marché de l'option."}},"nullable":true}}}}}},"responses":{"200":{"description":"Updated profile"},"400":{"description":"SIRET Luhn-invalide, n° de TVA incohérent avec le SIRET, site du médiateur invalide ou autre champ rejeté"},"403":{"description":"Insufficient role or not the profile owner"},"404":{"description":"Installer not found"}}},"post":{"summary":"Submit review","tags":["Installers"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["rating","text"],"properties":{"rating":{"type":"integer","minimum":1,"maximum":5},"text":{"type":"string","minLength":10}}}}}},"responses":{"201":{"description":"Review created, rating updated"}}}},"/installers/me":{"get":{"summary":"Get the authenticated installer's own profile","tags":["Installers","Agent"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Self-referent shortcut — resolves the installerProfile.id from the auth payload server-side. Saves a /auth/me round-trip when an agent only knows the API key. Installer role required.","responses":{"200":{"description":"Installer profile"},"403":{"description":"Not an installer account"},"404":{"description":"Profil installateur requis"}}},"patch":{"summary":"Update the authenticated installer's own profile","tags":["Installers","Agent"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Même body que PATCH /installers/{id} (siret + verify_sirene inclus). Évite à un agent de connaître son installerProfile.id.","responses":{"200":{"description":"Updated profile"},"400":{"description":"Validation"},"403":{"description":"Not an installer account"},"404":{"description":"Profil installateur requis"}}}},"/installers/{id}/documents":{"get":{"summary":"Lister les documents légaux d'un installateur (publics — UPLOADED/VERIFIED uniquement)","tags":["Installers","Trust"],"security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"description":"Liste publique filtrée — les documents en statut DECLARED ne sont pas exposés (signal trop faible). Le PDF lui-même n'est pas inclus dans la liste : le téléchargement passera par une route dédiée avec contrôle d'accès.","responses":{"200":{"description":"Liste des documents","content":{"application/json":{"schema":{"type":"object","properties":{"documents":{"type":"array","items":{"$ref":"#/components/schemas/InstallerDocument"}}}}}}},"404":{"description":"Installateur introuvable"}}},"post":{"summary":"Upserter un document légal (installateur propriétaire)","tags":["Installers","Trust"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"description":"Upsert sur (installerId, kind). Tout changement de pdfUrl invalide le statut VERIFIED (re-modération). Spreads conditionnels : un POST partiel ne ré-écrit pas les champs absents.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["kind"],"properties":{"kind":{"type":"string","enum":["KBIS","INSURANCE_DECENNALE","INSURANCE_RC_PRO","OTHER"]},"pdfUrl":{"type":"string","format":"uri","nullable":true},"pdfKey":{"type":"string","nullable":true},"pdfSize":{"type":"integer","nullable":true},"issueDate":{"type":"string","format":"date-time","nullable":true},"expiryDate":{"type":"string","format":"date-time","nullable":true},"reference":{"type":"string","nullable":true},"issuer":{"type":"string","nullable":true},"insurerContact":{"type":"string","maxLength":200,"nullable":true,"description":"Coordonnées de l'assureur (imprimées sur le devis)."},"coverageArea":{"type":"string","maxLength":120,"nullable":true,"description":"Couverture géographique (imprimée sur le devis)."},"notes":{"type":"string","nullable":true}}}}}},"responses":{"201":{"description":"Document upserté","content":{"application/json":{"schema":{"type":"object","properties":{"document":{"$ref":"#/components/schemas/InstallerDocument"}}}}}},"401":{"description":"Auth requise"},"403":{"description":"Pas le propriétaire"},"404":{"description":"Installateur introuvable"}}}},"/installers/{id}/documents/{docId}":{"delete":{"summary":"Supprimer un document légal (installateur propriétaire)","tags":["Installers","Trust"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"docId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"description":"Le PDF associé est libéré en best-effort côté storage.","responses":{"200":{"description":"Supprimé"},"401":{"description":"Auth requise"},"403":{"description":"Pas le propriétaire"},"404":{"description":"Document ou installateur introuvable"}}}},"/installers/{id}/documents/{docId}/download":{"get":{"summary":"Télécharger le PDF d'un document légal (auth requise)","tags":["Installers","Trust"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"docId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"description":"Proxy authentifié vers le PDF stocké. Owner installateur et ADMIN voient tous statuts ; tout autre rôle authentifié voit UPLOADED/VERIFIED uniquement (les DECLARED restent privés). Headers `Cache-Control: private, no-store` — auth re-vérifiée à chaque accès.","responses":{"200":{"description":"PDF binaire (application/pdf)"},"401":{"description":"Auth requise"},"404":{"description":"Document introuvable ou non visible (DECLARED côté tiers non-owner/admin)"},"422":{"description":"Document sans PDF associé"},"502":{"description":"PDF introuvable côté storage"}}}},"/sirene/{siret}":{"get":{"summary":"Lookup public SIRENE (annuaire-entreprises.gouv.fr)","tags":["Installers","Agent"],"security":[],"parameters":[{"name":"siret","in":"path","required":true,"schema":{"type":"string"},"description":"SIRET 14 chiffres. Validé format + Luhn avant lookup."}],"description":"Vérification publique d'un SIRET dans le registre SIRENE — pas d'auth, données 100 % publiques. Réponse enrichie : date d'immatriculation RCS, forme juridique (SAS/SARL/EI/Auto-entrepreneur), bloc JSON-LD schema.org/Organization avec foundingDate. Rate-limit 30 req/min/IP. Cache HTTP 24h. Caveat RGPD : pour les auto-entrepreneurs, l'adresse du siège peut être un domicile.","responses":{"200":{"description":"Snapshot SIRENE + JSON-LD","content":{"application/json":{"schema":{"type":"object","properties":{"siret":{"type":"string"},"companyName":{"type":"string"},"status":{"type":"string","enum":["active","ceased"]},"nafCode":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"dateCreation":{"type":"string","format":"date-time","nullable":true,"description":"Date d'immatriculation RCS — parsée depuis date_creation/unite_legale.date_creation/siege.date_creation (defensive)."},"formeJuridique":{"type":"string","nullable":true,"description":"Libellé humain (SAS, SARL, EI, Auto-entrepreneur)."},"fetchedAt":{"type":"string","format":"date-time"},"_meta":{"type":"object"},"@context":{"type":"string"},"@type":{"type":"string"},"name":{"type":"string"},"taxID":{"type":"string"},"foundingDate":{"type":"string","nullable":true}}}}}},"400":{"description":"SIRET invalide (format ou Luhn)"},"404":{"description":"SIRET non trouvé dans SIRENE"},"429":{"description":"Rate-limit (30/min/IP)"},"502":{"description":"Erreur amont SIRENE"},"504":{"description":"Timeout SIRENE (5s)"}}}},"/notifications":{"get":{"summary":"Get notifications","tags":["Notifications"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"unread","in":"query","schema":{"type":"boolean"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"before","in":"query","schema":{"type":"string","format":"date-time"},"description":"Back-paging : ne renvoie que les notifications strictement antérieures (createdAt < before). Passer le nextBefore de la réponse précédente."}],"responses":{"200":{"description":"Notifications (tri createdAt DESC) + unreadCount (total réel non-lues) + nextBefore (curseur back-paging, null si dernière page)"}}},"patch":{"summary":"Mark notifications read","tags":["Notifications"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Marque cette notification lue (ownership vérifiée)"},"markAllRead":{"type":"boolean","description":"true = marque tout lu. Fournir soit id, soit markAllRead: true — sinon 400."}}}}}},"responses":{"200":{"description":"Success"},"400":{"description":"Body invalide (ni id ni markAllRead: true)"}}}},"/regulatory":{"get":{"summary":"Index of the French solar regulatory registry","tags":["Regulatory","Agent"],"security":[],"description":"Index citation-friendly du registre : tarifs, seuils, procédures, documents officiels, sources juridiques. Versionné — chaque réponse inclut _meta.version + headers ETag.","parameters":[{"$ref":"#/components/parameters/regulatoryVersion"},{"$ref":"#/components/parameters/ifNoneMatch"}],"responses":{"200":{"description":"Index complet avec endpoints disponibles et version courante"},"304":{"$ref":"#/components/responses/RegulatoryNotModified"},"410":{"$ref":"#/components/responses/RegulatoryGone"}}}},"/regulatory/snapshot":{"get":{"summary":"Full regulatory snapshot (rates + thresholds + procedures + docs + sources)","tags":["Regulatory","Agent"],"security":[],"description":"Tout le corpus en un appel — idéal pour synchroniser un agent. Versionné, supporte conditional GET.","parameters":[{"$ref":"#/components/parameters/regulatoryVersion"},{"$ref":"#/components/parameters/ifNoneMatch"}],"responses":{"200":{"description":"Snapshot complet versionné"},"304":{"$ref":"#/components/responses/RegulatoryNotModified"},"410":{"$ref":"#/components/responses/RegulatoryGone"}}}},"/regulatory/rates":{"get":{"summary":"Tarifs réglementaires en vigueur (CRE)","tags":["Regulatory","Agent"],"security":[],"parameters":[{"$ref":"#/components/parameters/regulatoryVersion"},{"$ref":"#/components/parameters/ifNoneMatch"}],"responses":{"200":{"description":"Prime autoconsommation, rachat OA, TVA réduite/normale, MaPrimeRénov"},"304":{"$ref":"#/components/responses/RegulatoryNotModified"},"410":{"$ref":"#/components/responses/RegulatoryGone"}}}},"/regulatory/thresholds":{"get":{"summary":"Seuils réglementaires typés","tags":["Regulatory","Agent"],"security":[],"description":"Seuils transverses (3 kWc TVA, 36 kVA Enedis, 14j rétractation, 20 ans contrat OA, etc.).","parameters":[{"$ref":"#/components/parameters/regulatoryVersion"},{"$ref":"#/components/parameters/ifNoneMatch"},{"name":"category","in":"query","schema":{"type":"string","enum":["power","tax","subsidy","grid","contract","delay","technical"]}},{"name":"key","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste filtrée des seuils"},"304":{"$ref":"#/components/responses/RegulatoryNotModified"},"404":{"description":"Seuil inconnu (avec key)"},"410":{"$ref":"#/components/responses/RegulatoryGone"}}}},"/regulatory/procedures":{"get":{"summary":"Catalogue des procédures officielles","tags":["Regulatory","Agent"],"security":[],"parameters":[{"$ref":"#/components/parameters/regulatoryVersion"},{"$ref":"#/components/parameters/ifNoneMatch"},{"name":"category","in":"query","schema":{"type":"string","enum":["urbanisme","reseau","securite","contractuel","aide","commercial"]}},{"name":"verbose","in":"query","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"Liste résumée ou complète selon verbose"},"304":{"$ref":"#/components/responses/RegulatoryNotModified"},"410":{"$ref":"#/components/responses/RegulatoryGone"}}}},"/regulatory/procedures/{slug}":{"get":{"summary":"Détail d'une procédure officielle","tags":["Regulatory","Agent"],"security":[],"description":"Documents et sources résolus inline pour éviter un second appel agent. Inclut motifs de rejet fréquents + remèdes.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","enum":["declaration-prealable","raccordement-enedis","visa-consuel","mise-en-service-enedis","contrat-edf-oa","signature-devis"]}},{"$ref":"#/components/parameters/regulatoryVersion"},{"$ref":"#/components/parameters/ifNoneMatch"}],"responses":{"200":{"description":"Procédure + documents + sources"},"304":{"$ref":"#/components/responses/RegulatoryNotModified"},"404":{"description":"Slug inconnu (suggestions retournées)"},"410":{"$ref":"#/components/responses/RegulatoryGone"}}}},"/regulatory/documents":{"get":{"summary":"Documents officiels (Cerfa, conventions, attestations)","tags":["Regulatory","Agent"],"security":[],"parameters":[{"$ref":"#/components/parameters/regulatoryVersion"},{"$ref":"#/components/parameters/ifNoneMatch"},{"name":"kind","in":"query","schema":{"type":"string","enum":["cerfa","convention","attestation","contrat","formulaire"]}},{"name":"usedInStep","in":"query","schema":{"type":"string"}},{"name":"key","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Liste filtrée"},"304":{"$ref":"#/components/responses/RegulatoryNotModified"},"410":{"$ref":"#/components/responses/RegulatoryGone"}}}},"/regulatory/sources":{"get":{"summary":"Sources juridiques officielles citables (Légifrance, CRE, Enedis)","tags":["Regulatory","Agent"],"security":[],"parameters":[{"$ref":"#/components/parameters/regulatoryVersion"},{"$ref":"#/components/parameters/ifNoneMatch"},{"name":"topic","in":"query","schema":{"type":"string"}},{"name":"kind","in":"query","schema":{"type":"string","enum":["arrete","decret","loi","code","circulaire","deliberation","instruction"]}}],"responses":{"200":{"description":"Liste filtrée des sources avec JORF id et URL Légifrance"},"304":{"$ref":"#/components/responses/RegulatoryNotModified"},"410":{"$ref":"#/components/responses/RegulatoryGone"}}}},"/projects/{id}/sign":{"post":{"summary":"Initier la signature électronique d'un devis (Yousign)","tags":["Quotes","Agent"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Crée une demande Yousign à partir du devis sélectionné. Génère le PDF du devis, transmet au signataire (client) et persiste un signatureRequestId sur l'étape admin \"signature\". Par défaut, la session porte aussi le mandat de représentation (gabarit `mandat-representation`) : le client signe le devis et le mandat d'un geste, et l'installateur dépose ensuite la déclaration préalable, présente la demande de raccordement Enedis et fait les démarches EDF OA en son nom sans nouvelle signature. L'exemplaire signé du mandat est archivé sur l'étape (StepDocument « Mandat de représentation signé (Yousign) »), joint au dossier DP assemblé après le Cerfa et à la copie emailée au client.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["quoteId"],"properties":{"quoteId":{"type":"string","description":"UUID du devis à signer"},"includeMandate":{"type":"boolean","default":true,"description":"Joindre le mandat de représentation à la session de signature (false : devis seul — mandat déjà signé sur papier, client qui refuse de mandater)"},"clientUpdates":{"type":"boolean","description":"La case « Prévenir mon client par email à chaque étape franchie » (Project.clientUpdatesEnabled, vraie par défaut) : posée sur le dossier avant le premier envoi — lien de suivi dans l'email de l'exemplaire signé, puis une nouvelle au nom de l'installateur à chaque jalon, copie pour lui. false : l'installateur prévient lui-même ; le lien reste envoyable à la main (POST /projects/{id}/client-updates). Omise : réglage inchangé."}}}}}},"responses":{"201":{"description":"Demande de signature créée — signatureLink retourné ; `mandate.included` dit si le mandat est parti avec le devis (`mandate.requested` : ce que l'appelant demandait)"},"403":{"description":"Rôle non autorisé pour cette étape"},"409":{"description":"Une signature est déjà en cours ou complétée pour ce projet"},"422":{"description":"Prérequis non remplis (devis introuvable, étapes admin manquantes)"},"503":{"description":"Signature en ligne non activée sur la plateforme (code SERVICE_NOT_ACTIVE — réessayer ne change rien : faire signer le devis PDF, puis passer l'étape \"signature\" en DONE) ; sans ce code, service de signature momentanément indisponible"}}},"get":{"summary":"Statut de la signature électronique","tags":["Quotes","Agent"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"quoteId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Permet de détecter un mismatch si la signature en cours porte sur un autre devis"}],"responses":{"200":{"description":"Statut Yousign + lien de signature + statut de l'étape admin + `mandateIncluded` (la demande porte aussi le mandat de représentation) + `clientUpdates` (la case « Prévenir mon client par email » telle que posée sur le dossier). Tant qu'aucune demande n'est partie (signatureStatus \"not_initiated\"), onlineSignatureAvailable indique si l'envoi en signature peut partir (false : signature en ligne non activée sur la plateforme)"}}}},"/quotes/audit":{"post":{"summary":"Audit a French solar quote (public, no auth)","tags":["Quotes","Agent"],"security":[],"description":"Audit éphémère et public d'un devis solaire. Accepte un PDF (multipart/form-data) ou un payload structuré (application/json : seul `parsed` est requis, un champ absent vaut null — pas trouvé sur le devis —, au moins le total et la puissance, l'installateur ou les lignes). Retourne un score 0-100, des red flags codés (NO_SIRET, PRICE_TOO_HIGH, …) et une recommandation KEEP/NEGOTIATE/REJECT. Aucune persistance. Rate limit strict : 10 req/h/IP.","requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"application/pdf, max 20 MB"},"context":{"type":"string","description":"JSON stringifié — postalCode, medianPricePerWc, p25PricePerWc, p75PricePerWc"}}}},"application/json":{"schema":{"type":"object","required":["parsed"],"properties":{"parsed":{"type":"object","description":"ParsedQuoteData (cf. /projects/{id}/quotes/upload)","properties":{"installerName":{"type":"string","nullable":true},"installerSiret":{"type":"string","nullable":true},"totalAmountEur":{"type":"number","nullable":true},"powerKwc":{"type":"number","nullable":true},"validDays":{"type":"integer","nullable":true},"notes":{"type":"string","nullable":true},"lineItems":{"type":"array","items":{"type":"object"}}}},"context":{"type":"object","properties":{"postalCode":{"type":"string"},"medianPricePerWc":{"type":"number"},"p25PricePerWc":{"type":"number"},"p75PricePerWc":{"type":"number"}}}}}}}},"responses":{"200":{"description":"Audit complet","content":{"application/json":{"schema":{"type":"object","properties":{"audit":{"$ref":"#/components/schemas/QuoteAuditResult"},"parsed":{"type":"object","description":"Présent uniquement si l'entrée est un PDF","nullable":true},"_meta":{"type":"object"}}}}}},"400":{"description":"Payload invalide"},"413":{"description":"PDF trop volumineux"},"415":{"description":"Content-Type non supporté"},"422":{"description":"Échec d'extraction du PDF"},"429":{"description":"Rate limit dépassé (10/h/IP)"}}}},"/quotes/compare":{"get":{"summary":"Compare quotes side by side","tags":["Quotes","Agent"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Compare 2-10 quotes with pricing analytics and value scoring. Returns structured comparison with best-value recommendation.","parameters":[{"name":"ids","in":"query","required":true,"schema":{"type":"string"},"description":"Comma-separated quote UUIDs (2-10)"}],"responses":{"200":{"description":"Quotes compared with analytics, value scores, and NL summary"}}}},"/communications":{"get":{"summary":"List my threads or fetch messages of a thread","tags":["Communications","Agent"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Sans threadId : retourne les fils où l'appelant est participant actif, avec dernier message + flag unread. Avec threadId : retourne tous les messages visibles selon le scope du participant + la liste des participants ; pose acknowledgedAt côté caller.","parameters":[{"name":"threadId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Optional thread UUID. Omit to list your threads."}],"responses":{"200":{"description":"Threads list or thread messages","content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"threads":{"type":"array","items":{"type":"object"}},"total":{"type":"integer"}}},{"type":"object","properties":{"threadId":{"type":"string","format":"uuid"},"messages":{"type":"array","items":{"$ref":"#/components/schemas/Communication"}},"participants":{"type":"array","items":{"$ref":"#/components/schemas/CommunicationParticipant"}}}}]}}}},"401":{"description":"Authentication required"},"403":{"description":"Caller is not an active participant of the requested thread"}}},"post":{"summary":"Create a new thread or reply to an existing thread","tags":["Communications","Agent"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Communications inter-comptes rôle-agnostiques. Sans threadId : crée un fil avec recipientUserId + 2 participants (sender, recipient). Avec threadId : ajoute un message au fil, l'appelant doit être participant actif. Acts : MESSAGE | PROPOSAL | COUNTER | ACCEPT | REJECT. Email vérifié requis.","requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"type":"object","description":"Nouveau fil","required":["recipientUserId","act"],"properties":{"recipientUserId":{"type":"string","format":"uuid"},"act":{"type":"string","enum":["MESSAGE","PROPOSAL","COUNTER","ACCEPT","REJECT"]},"subjectType":{"type":"string","maxLength":64},"subjectId":{"type":"string","maxLength":128},"payload":{"type":"object","additionalProperties":true},"note":{"type":"string","maxLength":10000},"scope":{"type":"string","enum":["THREAD","FINANCIAL","TECHNICAL","PEER","ADMIN"]}}},{"type":"object","description":"Reply à un fil existant","required":["threadId","act"],"properties":{"threadId":{"type":"string","format":"uuid"},"parentId":{"type":"string","format":"uuid"},"act":{"type":"string","enum":["MESSAGE","PROPOSAL","COUNTER","ACCEPT","REJECT"]},"payload":{"type":"object","additionalProperties":true},"note":{"type":"string","maxLength":10000},"scope":{"type":"string","enum":["THREAD","FINANCIAL","TECHNICAL","PEER","ADMIN"]}}}]}}}},"responses":{"200":{"description":"Reply ajouté","content":{"application/json":{"schema":{"type":"object","properties":{"communication":{"$ref":"#/components/schemas/Communication"}}}}}},"201":{"description":"Thread créé","content":{"application/json":{"schema":{"type":"object","properties":{"communication":{"$ref":"#/components/schemas/Communication"},"threadId":{"type":"string","format":"uuid"}}}}}},"400":{"description":"Validation Zod ou self-message refusé"},"401":{"description":"Authentication required"},"403":{"description":"Email non vérifié OU caller pas participant du thread (reply)"},"404":{"description":"Recipient introuvable"}}}},"/health":{"get":{"summary":"Health check","tags":["System"],"responses":{"200":{"description":"Service status"}}}},"/leads":{"get":{"summary":"List leads for installer (with geographic filtering)","tags":["Leads"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Returns quote requests within the installer's service radius, enriched with distance (km), qualification score (0-100), and project details. Supports filtering by status and temporal range.","parameters":[{"name":"since","in":"query","schema":{"type":"string","format":"date-time"},"description":"Only leads after this ISO 8601 timestamp"},{"name":"status","in":"query","schema":{"type":"string","enum":["PENDING","QUOTED","ACCEPTED","DECLINED","EXPIRED"]}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":20}}],"responses":{"200":{"description":"Filtered leads with distanceKm, qualificationScore, serviceRadius"}}}},"/quotes/generate":{"post":{"summary":"Auto-generate a quote from a pricing template","tags":["Quotes"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Generates a complete quote with line items (panels, inverter, labor, commissioning). The budget/standard/premium templates are anchored to the French market price baseline (the same reference used by the quote audit): p25 / median / p75 in EUR per Wc incl. VAT. Line items are customer-facing prices (incl. VAT) summing exactly to the total; the VAT breakdown is returned alongside (5.5% up to 9 kWc on a dwelling; 20% above, with a battery, for a non-residential project or when the reduced-rate step is marked not applicable; none under the VAT franchise). Grid lines never assume a manufacturer warranty, a battery capacity or a charger power; the inverter line names the declared inverter model. Optional `margin` raises the output price relative to the market anchor (0.1 = +10%). Sizing (power, panel count and model) comes from the project's declared technical study (PATCH /projects/{id}/technical-study), falling back to its simulation; without either the call is refused with 422 — no default sizing is ever assumed. A request reserved for another installer is refused with 403. `template: \"mine\"` builds the same five core lines from the installer's own price list (« Mes prix »): panels × unit price, labour × panel count, inverter and cabling × kWc, a commissioning lump sum; options at the catalogue lump sum or, when unset, at the Standard market price; `margin` is ignored; an incomplete price list is refused with 422 naming the missing fields. Installer role required.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["quoteRequestId"],"properties":{"quoteRequestId":{"type":"string","format":"uuid"},"template":{"type":"string","enum":["budget","standard","premium","mine"],"default":"standard","description":"`mine` = the installer's own price list (« Mes prix », PATCH /installers/me `priceList`)."},"margin":{"type":"number","minimum":0,"maximum":0.5,"description":"Price adjustment vs the market anchor (0.1 = +10%)"},"notes":{"type":"string","description":"Printed on the client's quote as is — no default note."},"terms":{"type":"object","description":"Conditions du devis, imprimées sur le PDF (section « Délais et paiement »). Une clé absente reprend la condition par défaut du profil (InstallerProfile.quoteDefaults) ; une clé à null ou vide reste vide (le devis imprime « — »). Un délai global ne suffit pas : livraison, pose, démarches et mise en service sont distincts.","additionalProperties":false,"properties":{"paymentTerms":{"type":"string","maxLength":300,"nullable":true,"description":"Modalités de paiement (aucun paiement avant 7 jours pour un contrat hors établissement, art. L221-10)."},"deliveryDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai de livraison du matériel."},"installationDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai de pose."},"adminDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai des démarches administratives (déclaration préalable, raccordement)."},"commissioningDelay":{"type":"string","maxLength":300,"nullable":true,"description":"Délai de raccordement et de mise en service."}}}}}}}},"responses":{"201":{"description":"Quote generated with line items, VAT breakdown (one rate for the whole operation: a battery line, a non-residential project or a NOT_APPLICABLE « tva-reduite » step means 20 %), `missingMentions` (always empty: a missing mandatory mention is refused with 422 QUOTE_MENTIONS_MISSING) and `qualificationReminder` (a sentence when no photovoltaic qualification is declared on the profile, null otherwise)"},"403":{"description":"Quote request reserved for another installer"},"409":{"description":"Already quoted for this request"},"422":{"description":"No technical study or simulation on the project (declare the study first), `template: \"mine\"` with an incomplete price list (the message names the missing fields), or a mandatory mention of the quote missing (code QUOTE_MENTIONS_MISSING, body QuoteMentionsMissing: nothing is created)","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/QuoteMentionsMissing"}]}}}}}}},"/projects/offline":{"post":{"summary":"Create an offline project for an external client","tags":["Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/idempotencyKey"}],"description":"L'installateur crée un dossier pour un client qui n'est pas sur la plateforme : la demande de devis et les dix étapes naissent avec lui. Rôle installateur requis ; l'adresse vérifiée n'est pas exigée (créer un dossier n'envoie rien à personne). `startStage` dit où en est le chantier et marque faites les étapes déjà passées (elles restent rouvrables, leur date réelle n'est jamais inventée) : c'est le geste de l'agent pour un chantier dont le devis est déjà signé avec un autre outil. Code postal ou coordonnées déduisent le territoire du dossier (gestionnaire de réseau, acheteur, TVA, barème). Plan gratuit : un plafond de dossiers initiés par l'installateur (403 `INSTALLER_PROJECT_CAP_REACHED`, les archivés ne comptent pas). **Démarrer au devis déjà signé** : en JSON, `signedQuote` déclare un PDF déjà téléversé dans le stockage build.solar (`fileUrl`, `fileKey`, `fileName`, `fileSize`, et le `token` de POST /quotes/parse) — le chemin de la fenêtre « Nouveau dossier client », sans la limite de ~4,5 Mo que la plateforme accorde au corps d'une requête ; en multipart/form-data, le PDF du devis (`file`, 4,5 Mo au plus) voyage avec les mêmes champs. Dans les deux cas le dossier naît au palier `quote_signed` (sauf palier explicite), le PDF est joint comme devis du dossier (ExternalQuote + Quote, comme POST /projects/{id}/quotes/upload), et les modules lus proposent l'étude technique sur la première étape (`signedQuote.proposedStudy`), à confirmer par PATCH /projects/{id}/technical-study (`next_steps.confirm_study`). Avec le `token` de POST /quotes/parse, rien n'est relu.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["clientName","name","address"],"properties":{"clientName":{"type":"string","minLength":1},"clientEmail":{"type":"string","format":"email","description":"Sans elle, les documents et le paquet de dépôt n'ont pas d'adresse de client à écrire."},"clientPhone":{"type":"string"},"name":{"type":"string","minLength":1,"description":"Nom du dossier (l'écran propose « Chantier <client> »)."},"address":{"type":"string","minLength":1},"city":{"type":"string"},"postalCode":{"type":"string"},"latitude":{"type":"number"},"longitude":{"type":"number"},"type":{"type":"string","enum":["Résidentiel","Commercial","Industriel","Public","Collectif"]},"message":{"type":"string","description":"Contexte libre, porté par la demande de devis créée avec le dossier."},"buildingType":{"type":"string"},"timeline":{"type":"string"},"startStage":{"type":"string","enum":["quote_signed","installed"],"description":"Où en est le chantier : `quote_signed` = Devis signé (Devis et signature seront marqués faits) ; `installed` = Déjà posé (Devis, signature, déclaration préalable et pose seront marqués faits). Absent : le dossier démarre à la première étape — sauf quand un devis signé est joint (multipart), où le palier vaut `quote_signed`."},"signedQuote":{"type":"object","required":["fileUrl"],"description":"Le devis signé — un PDF, ou une photo du devis signé sur papier (JPEG, PNG, WebP), mise en PDF d'une page —, déjà téléversé dans le stockage build.solar (POST /upload/token ou POST /upload) : joint au dossier tel quel (c'est le contrat ; la photo d'origine est retirée une fois son PDF joint), relu et vérifié (octets, 20 Mo au plus), lu par l'IA ou repris du `token` de POST /quotes/parse (avec une photo : déclarer le `pdf` rendu par la lecture, le jeton est lié à ses octets).","properties":{"fileUrl":{"type":"string","description":"URL rendue par le téléversement — dans le stockage build.solar seulement (Vercel Blob, ou /uploads/ en local)."},"fileKey":{"type":"string","description":"Clé de stockage rendue par le téléversement (`media/AAAA-MM/<id>.pdf`) ; déduite de l'URL si absente."},"fileName":{"type":"string","description":"Nom lisible du fichier."},"fileSize":{"type":"integer","description":"Taille en octets, déclarative : c'est le contenu relu qui est borné (20 Mo)."},"token":{"type":"string","description":"Jeton rendu par POST /quotes/parse pour ce même fichier : les valeurs déjà lues sont reprises, sans seconde lecture."}}}}}},"multipart/form-data":{"schema":{"type":"object","required":["clientName","name","address","file"],"properties":{"clientName":{"type":"string","minLength":1},"clientEmail":{"type":"string","format":"email","description":"Sans elle, les documents et le paquet de dépôt n'ont pas d'adresse de client à écrire."},"clientPhone":{"type":"string"},"name":{"type":"string","minLength":1,"description":"Nom du dossier (l'écran propose « Chantier <client> »)."},"address":{"type":"string","minLength":1},"city":{"type":"string"},"postalCode":{"type":"string"},"latitude":{"type":"number"},"longitude":{"type":"number"},"type":{"type":"string","enum":["Résidentiel","Commercial","Industriel","Public","Collectif"]},"message":{"type":"string","description":"Contexte libre, porté par la demande de devis créée avec le dossier."},"buildingType":{"type":"string"},"timeline":{"type":"string"},"startStage":{"type":"string","enum":["quote_signed","installed"],"description":"Où en est le chantier : `quote_signed` = Devis signé (Devis et signature seront marqués faits) ; `installed` = Déjà posé (Devis, signature, déclaration préalable et pose seront marqués faits). Absent : le dossier démarre à la première étape — sauf quand un devis signé est joint (multipart), où le palier vaut `quote_signed`."},"file":{"type":"string","format":"binary","description":"Le devis signé, application/pdf — ou image/jpeg, image/png, image/webp : la photo du devis signé sur papier, mise en PDF d'une page — 4,5 Mo au plus par cet appel (limite de la plateforme ; au-delà, `signedQuote` en JSON) — joint au dossier tel quel (c'est le contrat), lu par l'IA : montant, lignes, client, adresse du chantier, modules, n° et date du devis, TVA et mentions PV."},"fileName":{"type":"string","description":"Nom lisible du fichier."},"token":{"type":"string","description":"Jeton rendu par POST /quotes/parse pour ce même fichier : les valeurs déjà lues sont reprises, sans seconde lecture."}}}}}},"responses":{"201":{"description":"Dossier créé avec ses dix étapes et sa demande de devis : `project` (dont `id`), `quoteRequest`, `adminSteps`, `next_steps` (dont `sign_mandate` quand le chantier est déclaré signé ou posé : le mandat de représentation n'a pas été signé avec un devis signé ailleurs — le générer, le faire signer sur papier, le joindre à l'étape signature sous le libellé donné, le paquet de dépôt le relit) ; avec le devis (JSON `signedQuote` ou multipart) : `signedQuote` (`externalQuoteId`, `quoteId`, `quoteAccepted` — né accepté : le chantier est déclaré signé, le PDF est le contrat —, `parseStatus`, `proposedStudy`, `review` — la relecture du devis importé aux mots du territoire du dossier : `attention`, `read`, `line`, ou null quand rien n'a été lu) ou `signedQuoteError` (le dossier existe, le devis s'importe depuis « Mon devis »)."},"400":{"description":"Champ manquant ou invalide (message en français)."},"403":{"description":"Rôle installateur requis — ou plafond du plan gratuit atteint (`INSTALLER_PROJECT_CAP_REACHED`) : archiver un dossier terminé libère une place, ou passer Pro."}}}},"/simulate":{"get":{"summary":"Simulation API documentation","tags":["Simulation"],"responses":{"200":{"description":"Schema documentation and example"}}},"post":{"summary":"Direct solar simulation (stateless, no account required)","tags":["Simulation"],"description":"Single-call simulation for AI agents. Optional `postalCode` / `inseeCode` (or the coordinates) resolve the territory — mainland (Enedis, EDF OA, S21 tariff, VAT 5.5/20 %), Corsica and overseas (EDF SEI / EDM, quarterly ZNI tariff and investment premium, local VAT) — echoed in the `territory` block of the result. Returns complete results with confidence intervals and metadata context.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["address"],"properties":{"address":{"type":"string"},"latitude":{"type":"number"},"longitude":{"type":"number"},"panelCount":{"type":"integer","default":18},"panelModel":{"type":"string","default":"longi-himo6-400"},"roofAreaM2":{"type":"number"},"azimuth":{"type":"number","default":180,"description":"Azimut boussole (0 = nord, 180 = sud). Par défaut face à l'équateur : 180 dans l'hémisphère nord, 0 quand la latitude est négative (La Réunion, Mayotte)."},"tilt":{"type":"number","default":30},"battery":{"type":"boolean","default":false},"evCharger":{"type":"boolean","default":false}}}}}},"responses":{"200":{"description":"Simulation results with confidence intervals, solar potential, available panels, and metadata context (assumptions, data sources)"}}}},"/tarifs":{"get":{"summary":"Get regulatory tariffs","tags":["System"],"responses":{"200":{"description":"Current tariffs (EDF OA, MaPrimeRénov, TVA)"}}}},"/stats":{"get":{"summary":"Stats index (endpoints, power brackets, step keys)","tags":["Stats","Agent"],"description":"Index discoverable des statistiques agrégées. Source : Grille interne révisée trimestriellement, servie seule : aucune donnée de dossier n'entre dans ces statistiques.","responses":{"200":{"description":"Index + métadonnées baseline"}}}},"/stats/median-price":{"get":{"summary":"Median installed price per Wc","tags":["Stats","Agent"],"description":"Prix médian €/Wc pour une puissance donnée, filtrable par zone postale. Source : Grille interne révisée trimestriellement, servie seule : aucune donnée de dossier n'entre dans ces statistiques.","parameters":[{"name":"power_kwc","in":"query","required":true,"schema":{"type":"number","minimum":1,"maximum":500},"description":"Puissance cible en kWc."},{"name":"postal_code","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{2,5}$"},"description":"Code postal FR (2-5 chiffres) — préfixe utilisé pour matcher la zone."}],"responses":{"200":{"description":"Prix médian €/Wc, bornes P25/P75, taille d'échantillon, source (baseline|blended|real), meta versionnée."},"400":{"description":"Paramètre invalide"}}}},"/stats/median-delays":{"get":{"summary":"Median delay per administrative step","tags":["Stats","Agent"],"description":"Délai médian (jours) pour compléter une étape admin. Si step omis, retourne l'ensemble des étapes. Source : Grille interne révisée trimestriellement, servie seule : aucune donnée de dossier n'entre dans ces statistiques.","parameters":[{"name":"step","in":"query","required":false,"schema":{"type":"string","enum":["simulation","devis","tva-reduite","signature","dp","raccordement","installation","consuel","mise-en-service","edf-oa"]},"description":"Clé d'étape admin (source : admin-steps-config)."},{"name":"departement","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{2,3}$"},"description":"Numéro de département FR (2-3 chiffres) — matche le préfixe du code postal projet."}],"responses":{"200":{"description":"Délai médian, P90, délai légal si applicable, source, meta versionnée."},"400":{"description":"Paramètre invalide"},"404":{"description":"Étape inconnue"}}}},"/projects/{id}/admin-steps":{"get":{"summary":"List administrative steps for a project","tags":["Projects"],"description":"Chaque étape porte son statut, son issue (`outcome`, `outcomeAt`, `outcomeReason`, `outcomeReasonKeys` — motifs codés, clés des motifs de refus de la procédure au registre —, `submissionChannel`, et sur la DP `communeRequirements` : ce que la mairie exige avant dépôt) et `regulatory` : la règle officielle datée (slug du registre, `verifiedOn` = dernière vérification à la source, `source.reference` + `source.url`) — `null` sur `raccordement`, `mise-en-service` et `edf-oa` quand le gestionnaire n'est pas Enedis (outre-mer, distributeur local) : la règle du registre est celle de la métropole, l'étape porte le portail et le guide du gestionnaire. L'étape `dp` porte en plus `acceptance` (le fait « acceptée du premier coup », lu dans le journal des issues : `kind` first-time | after-complement | after-refusal, `label`) une fois acceptée, et `preDepositChecks` avant dépôt (contrôles nommés d'après les motifs de refus du registre : `checks[].state` passed | to-verify | blocked, `summary` en mots, `inline`). Jamais d'agrégat : un fait par dossier. Les étapes `tva-reduite`, `raccordement`, `consuel` et `edf-oa` portent `storage` quand l'étude déclare une batterie (`capKwh`, `powerKw`, `coupling` DC | AC | null, `existingInstallation`, `line` : la phrase de l'étape — source src/lib/battery-facts.ts) ; batterie ajoutée à une installation déjà en service (`existingInstallation` vrai), `dp`, `installation` et `mise-en-service` le portent aussi, et `storage.notApplicable` vrai dit que l'étape est sans objet pour ce dossier (DP, TVA réduite, EDF OA sans injection) — à marquer « Non applicable » par PATCH, jamais automatique. `territory` dit le territoire déduit de l'adresse du dossier (`code` metropole | corse | guadeloupe | martinique | guyane | reunion | mayotte | saint-pierre-et-miquelon | iles-du-nord | hors-champ | eld, `gestionnaire`, `acheteur`, `portail`, `tarifRegime`, `notice`) ; `eld` = commune de métropole desservie par un distributeur local à la place d'Enedis (`distributeurLocal` : son nom — ÉS, GreenAlp, SRD, Gérédis… ; registre commune → distributeur, référentiel ORE) : les étapes raccordement, mise en service et contrat d'achat parlent alors le distributeur et son acheteur obligé (Code de l'énergie, art. L314-1), barème S21 et TVA inchangés. L'étape `consuel` porte `technique` : ce que demandent le dossier technique SC 144 et le schéma de principe — `study` (le dossier a une étude : sans elle, aucune des deux pièces ne se fait), `sc144` (le formulaire que l'étude fixe : `code` SC 144A-5 | SC 144C-5 | SC 144C2-2, ou null avec `candidates` et `missing` — ce qu'il faut dire dans l'étude), `values` (les valeurs de l'électricien déjà dites, null = pas encore dite), `questions` (celles qui concernent ce dossier, dans l'ordre de l'écran : `field`, `label`, `unit`, `group` essentiel | generateur | onduleur | batterie | alternatif, `rubric` — la rubrique du formulaire, null pour le schéma seul —, `onForm`), `missingOnForm` (ce que le formulaire demande et qui n'est pas encore dit) et `prefill` (le dernier dossier technique pré-rempli joint : variante, version et empreinte du fichier officiel, date). L'étape `raccordement` porte `urbanAuthorization` : l'autorisation de la mairie que la demande attend. Jusqu'à 6 kVA par phase, Enedis accepte le récépissé de dépôt de la déclaration préalable (Enedis-MOP-RAC_045E, annexe 1, pièce 5 ; Enedis-NMO-RAC_013E, § 7.1.2.2 — versions du 15/09/2025, relues le 2026-10-28) : `receiptRule` (`applies`, `reason`, `missing` — ce qu'il faut dire dans l'étude pour trancher —, `maxKvaPerPhase`), lue sur l'étude (branchement, puissance de l'onduleur, micro-onduleurs) et seulement chez Enedis, pour une demande avec injection ; `dpReceipt` (`depositedOn`, `reference` : la date du récépissé et le numéro de la déclaration rapportés sur l'étape `dp`) ; `satisfiedBy` (`dp-settled` : DP réglée, `dp-receipt` : DP déposée, la règle valant — le prérequis « déclaration préalable » est alors satisfait et la demande peut partir —, `dp-not-applicable` : DP sans objet, batterie ajoutée à une installation en service, aucune autorisation à joindre, `null` : pas encore) ; `raccordementFiled`, `piece` (la pièce à joindre), `line` et `notice` (les phrases de l'étape), `alert` (chez Enedis seulement : DP refusée ou en demande de pièces après le dépôt de la demande, prévenir Enedis), `source` (`reference`, `url`, `readOn` ; `null` hors du réseau d'Enedis, dont la note n'est citée que chez lui).","security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Steps with statuses, outcome, dated rule (`regulatory`), DP acceptance fact and pre-deposit checks, the urban-planning authorization the connection request awaits (`urbanAuthorization` on `raccordement`), plus progress and the dossier territory (`territory`, with `distributeurLocal` when a local distributor serves the commune)"}}},"post":{"summary":"Batch upsert administrative steps","tags":["Projects"],"description":"`status` optionnel : omis, seuls notes/metadata/dueDate changent (metadata fusionné, jamais écrasé — le statut ne régresse pas). Étapes à capteur (raccordement, consuel) : statut refusé en 422 — piloté par PATCH /api/enedis et /api/consuel. Protocole de l'agent de l'installateur (dépôt, réponse, re-dépôt) et valeurs de l'électricien sur l'étape `consuel` (`metadata.consuelTechnique`) : voir PATCH.","security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["steps"],"properties":{"steps":{"type":"array","maxItems":10,"items":{"type":"object","required":["stepKey"],"properties":{"stepKey":{"type":"string"},"status":{"type":"string","enum":["TODO","IN_PROGRESS","DONE","BLOCKED","NOT_APPLICABLE"]},"outcome":{"type":"string","nullable":true,"enum":["ACCEPTED","TACIT","COMPLEMENT_REQUESTED","REFUSED"],"description":"Issue de la démarche (étape dp) : accord exprès, non-opposition tacite, demande de pièces complémentaires, refus. Sans `status`, le statut se déduit (accord ou tacite : DONE ; complément ou refus : BLOCKED). `null` efface l'issue. Sur raccordement et consuel, l'issue vient de PATCH /api/enedis et /api/consuel (422 ici)."},"outcomeAt":{"type":"string","format":"date","description":"Date du courrier de la mairie, ou du terme du délai pour la tacite (AAAA-MM-JJ) — aujourd'hui si omise, jamais future."},"outcomeReason":{"type":"string","nullable":true,"maxLength":500,"description":"Motif : pièces demandées, motif du refus."},"complementPieces":{"type":"array","items":{"type":"string","enum":["DPC1","DPC2","DPC4","DPC6","DPC7","DPC8","DPC11"]},"description":"Pièces du bordereau réclamées par la mairie — avec l'issue COMPLEMENT_REQUESTED (422 sinon) ; gardées dans la metadata de l'étape (`complementPieces`) et au journal."},"submissionChannel":{"type":"string","nullable":true,"enum":["COUNTER","MAIL","ONLINE"],"description":"Canal de dépôt de la DP : guichet de la mairie, courrier recommandé, portail en ligne de la commune."},"outcomeReasonKeys":{"type":"array","items":{"type":"string"},"description":"Motifs codés de l'issue : les clés (`key`) des motifs de refus (`commonRejectionReasons`) de la procédure de l'étape au registre (GET /regulatory/procedures/{slug}) — avec COMPLEMENT_REQUESTED ou REFUSED (422 sinon, 422 si la clé est d'une autre démarche) ; gardés en colonne et au journal, pour la mémoire des communes. Le texte libre reste `outcomeReason`."},"communeRequirements":{"type":"object","nullable":true,"description":"Ce que la mairie exige avant le dépôt de la DP (étape dp seulement ; `null` efface, l'objet remplace) : `paperCopies` (1 à 6), `extraPieces` (codes du bordereau demandés en plus des pièces dues), `portalUrl` (https://…), `otherRequests` (300 caractères). Avec le canal de dépôt et le code INSEE du dossier : la mémoire des communes.","properties":{"paperCopies":{"type":"integer","nullable":true,"minimum":1,"maximum":6},"extraPieces":{"type":"array","items":{"type":"string","enum":["DPC1","DPC2","DPC4","DPC6","DPC7","DPC8","DPC11"]}},"portalUrl":{"type":"string","nullable":true,"maxLength":300},"otherRequests":{"type":"string","nullable":true,"maxLength":300}}}}}}}}}}},"responses":{"200":{"description":"Steps upserted"},"422":{"description":"Prerequisites not met (for `raccordement`, the DP settled — or deposited when Enedis accepts the receipt, up to 6 kVA per phase) / sensor-managed step status"}}},"patch":{"summary":"Update single administrative step","tags":["Projects"],"description":"`status` optionnel : omis, seuls notes/metadata/dueDate changent (mise à jour partielle — le statut ne régresse jamais). Étapes à capteur (raccordement, consuel) : statut refusé en 422 — piloté par PATCH /api/enedis et /api/consuel. Valeurs de l'électricien (étape `consuel`, `metadata.consuelTechnique`, mêmes champs que `consuelTechnique` de POST /projects/{id}/admin-steps/documents/generate) : fusionnées champ à champ — un champ envoyé remplace, `null` efface, un champ absent reste ; `consuelTechnique: null` efface tout ; un champ inconnu ou une valeur refusée → 400 avec le message ; `metadata.consuelSc144Prefill` est écrit par le serveur seul (ignoré ici). Issue de la DP : `outcome` + `outcomeAt` + `outcomeReason` au passage à DONE ou BLOCKED (le statut se déduit de l'issue si omis) ; une étape rouverte perd son issue courante, le journal d'activité garde l'histoire. Re-dépôt après une demande de pièces : `metadata.complementDepositedAt` (ISO) avec `status: IN_PROGRESS` — le délai d'instruction repart de cette date. Protocole de l'agent de l'installateur — mémoire des démarches (2026-10-17), trois moments, mêmes champs que l'écran, tout tracé par la clé (`apiKeyId`). (1) Dépôt : `submissionChannel`, `metadata.depositedAt` (ISO), `metadata.ref` (le numéro de la déclaration, celui du récépissé, « DP 069 123 26 00045 »), `communeRequirements` (ce que la mairie a exigé : exemplaires, pièces en plus, portail, autre demande) ; le récépissé en pièce (POST /projects/{id}/admin-steps/documents, label « Récépissé de dépôt »). Jusqu'à 6 kVA par phase chez Enedis, la date du récépissé et le numéro ouvrent l'étape `raccordement` sans attendre la réponse de la mairie (GET admin-steps, `urbanAuthorization`), et son échéance se compte depuis ce dépôt. (2) Réponse : `outcome` + `outcomeAt` + `complementPieces` ; le motif en clés `outcomeReasonKeys` (les `commonRejectionReasons[].key` de la procédure, GET /regulatory/procedures/{slug}) ou en clair dans `outcomeReason`, classé alors en arrière-plan dans le même vocabulaire ; le courrier en pièce, label « Réponse de la mairie ». Raccordement et CONSUEL : PATCH /api/enedis et /api/consuel (`status`, `eventAt`, `enedisNotes` ou `consuelNotes`, `reasonKeys`), pièces « Réponse du gestionnaire de réseau » et « Réponse du CONSUEL ». (3) Re-dépôt : `metadata.complementDepositedAt` + `status: IN_PROGRESS`.","security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["stepKey"],"properties":{"stepKey":{"type":"string"},"status":{"type":"string","enum":["TODO","IN_PROGRESS","DONE","BLOCKED","NOT_APPLICABLE"]},"notes":{"type":"string"},"outcome":{"type":"string","nullable":true,"enum":["ACCEPTED","TACIT","COMPLEMENT_REQUESTED","REFUSED"],"description":"Issue de la démarche (étape dp) : accord exprès, non-opposition tacite, demande de pièces complémentaires, refus. Sans `status`, le statut se déduit (accord ou tacite : DONE ; complément ou refus : BLOCKED). `null` efface l'issue. Sur raccordement et consuel, l'issue vient de PATCH /api/enedis et /api/consuel (422 ici)."},"outcomeAt":{"type":"string","format":"date","description":"Date du courrier de la mairie, ou du terme du délai pour la tacite (AAAA-MM-JJ) — aujourd'hui si omise, jamais future."},"outcomeReason":{"type":"string","nullable":true,"maxLength":500,"description":"Motif : pièces demandées, motif du refus."},"complementPieces":{"type":"array","items":{"type":"string","enum":["DPC1","DPC2","DPC4","DPC6","DPC7","DPC8","DPC11"]},"description":"Pièces du bordereau réclamées par la mairie — avec l'issue COMPLEMENT_REQUESTED (422 sinon) ; gardées dans la metadata de l'étape (`complementPieces`) et au journal."},"submissionChannel":{"type":"string","nullable":true,"enum":["COUNTER","MAIL","ONLINE"],"description":"Canal de dépôt de la DP : guichet de la mairie, courrier recommandé, portail en ligne de la commune."},"outcomeReasonKeys":{"type":"array","items":{"type":"string"},"description":"Motifs codés de l'issue : les clés (`key`) des motifs de refus (`commonRejectionReasons`) de la procédure de l'étape au registre (GET /regulatory/procedures/{slug}) — avec COMPLEMENT_REQUESTED ou REFUSED (422 sinon, 422 si la clé est d'une autre démarche) ; gardés en colonne et au journal, pour la mémoire des communes. Le texte libre reste `outcomeReason`."},"communeRequirements":{"type":"object","nullable":true,"description":"Ce que la mairie exige avant le dépôt de la DP (étape dp seulement ; `null` efface, l'objet remplace) : `paperCopies` (1 à 6), `extraPieces` (codes du bordereau demandés en plus des pièces dues), `portalUrl` (https://…), `otherRequests` (300 caractères). Avec le canal de dépôt et le code INSEE du dossier : la mémoire des communes.","properties":{"paperCopies":{"type":"integer","nullable":true,"minimum":1,"maximum":6},"extraPieces":{"type":"array","items":{"type":"string","enum":["DPC1","DPC2","DPC4","DPC6","DPC7","DPC8","DPC11"]}},"portalUrl":{"type":"string","nullable":true,"maxLength":300},"otherRequests":{"type":"string","nullable":true,"maxLength":300}}}}}}}},"responses":{"200":{"description":"{ step, clientUpdate: { label } | null } — `clientUpdate` : la nouvelle partie au client externe (lib/client-updates : étape Terminée qui est un jalon du client, ou date de dépôt de la DP enregistrée), au nom de l'installateur ; null sinon."},"422":{"description":"Prerequisites not met (for `raccordement`, the DP settled — or deposited when Enedis accepts the receipt, up to 6 kVA per phase) / sensor-managed step status"}}}},"/projects/{id}/admin-steps/documents/generate":{"get":{"summary":"Generate a pre-filled document (PDF)","tags":["Projects"],"description":"Sans `template`, liste les gabarits disponibles par étape. Documents de l'étape `dp` : la zone d'urbanisme du dossier est relevée d'abord si elle ne l'a pas été (le geste de GET /urbanisme — jamais bloquant : relevé impossible, le document le dit). Avec `template`, renvoie le PDF : Cerfa DP 16702*03 pré-rempli (`cerfa-dp`), récapitulatifs Enedis / CONSUEL / mise en service / EDF OA, fiche technique, PV de réception, la déclaration d'achèvement (`daact` : Cerfa 13408*14 officiel pré-rempli — déclarant, numéro de la DP lu sur l'étape `dp` (`metadata.ref`, à poser par PATCH admin-steps), date de fin de pose une fois l'étape `installation` terminée ; PDF seul), le plan de situation DPC1 (`plan-situation-dp` : fond Plan IGN par tuiles de la Géoplateforme, parcelle cadastrale surlignée par API Carto, nord, échelle — données publiques ; 422 si l'adresse du dossier ne se géolocalise pas, 503 si une source ne répond pas), le plan de masse DPC2 (`plan-masse-dp` : la parcelle et ses voisines lues par API Carto — chaque limite nommée (parcelle voisine, voie, non cadastré) et cotée en longueur —, les bâtiments et les voies de la BD TOPO de l'IGN, les altitudes du terrain naturel du RGE ALTI ; en toiture, le champ du plan des toitures reporté sur le toit, au sol le champ désigné sur le terrain (réponses gardées sur l'étape `dp`, `metadata.dpSitePlan`) ; les distances du projet à chaque limite et à la voie cotées, une voie inconnue de la BD TOPO n'étant pas cotée — la légende le dit ; 422 sans position, sans étude, sans parcelle lue au cadastre, sans bâtiment lu en toiture ou, au sol, sans champ placé ; 503 si le cadastre ou la BD TOPO ne répondent pas ; avec `format=site`, le terrain en JSON — `site` : parcelle simplifiée, bâtiments repérés A, B…, voies, en mètres autour du centre de la parcelle (`frame`), le champ au sol d'après l'étude et les réponses gardées — pour placer le champ avant de répondre `sitePlan`), le plan des façades et des toitures DPC4 (`plan-toitures-dp` : emprise et hauteurs du bâtiment lues dans la BD TOPO de l'IGN, panneaux de l'étude dessinés et cotés sur le pan équipé, façade côté panneaux et coupe, d'après les réponses gardées sur l'étape `dp` (`metadata.dpRoofPlan`) — défauts justes sans elles ; 422 sans position, sans étude ou sans bâtiment lu à l'adresse, 503 si la BD TOPO ne répond pas), le photomontage DPC6 (`photomontage-dp` : la photo de la maison choisie parmi les pièces de l'étape `dp`, redressée selon son orientation EXIF, avec les panneaux du dernier plan des toitures dessiné ici (`metadata.dpRoofPlanLayout`) posés en perspective sur les quatre coins du pan gardés sur l'étape (`metadata.dpInsertion`) ; 422 sans plan des toitures dessiné ici, sans photo JPEG, PNG, WebP ou AVIF, ou avec des coins qui ne dessinent pas le pan, 503 si la photo ne revient pas du stockage), la notice des matériaux DPC11 (`notice-materiaux-dp` : rédigée depuis l'étude du dossier et les réponses gardées sur l'étape — teinte des modules, mode de pose —, « à préciser » sans elles), et le dossier DP assemblé (`dossier-dp` : couverture de complétude — ce qui est joint, ce qui manque, où chaque document se trouve —, Cerfa pré-rempli aplati, pièces jointes dans l'ordre du bordereau, le mandat de représentation signé joint après le Cerfa quand il existe ; en-têtes `X-Dossier-Page-Count` et `X-Dossier-Warning-Count`), le dossier technique du CONSUEL (`sc144-prerempli` : le formulaire officiel SC 144 du jour — SC 144A-5, SC 144C-5 ou SC 144C2-2 selon l'étude, micro-onduleurs ou onduleur de chaîne, batterie ou non ; au cas de raccordement 4, le SC 144E-1 à la suite dans le même PDF —, téléchargé sur consuel.com et reconnu à son empreinte, sur lequel s'écrit ce que l'étude et les valeurs de l'électricien gardées sur l'étape `consuel` (`metadata.consuelTechnique`) disent, et rien d'autre ; le reste se complète à la main, l'installateur cachette et signe ; 422 sans étude ou quand l'étude ne fixe pas le formulaire, 503 quand le fichier du jour est une version que l'outil n'a pas encore relue — rien n'est écrit, l'équipe est prévenue, le récapitulatif `dossier-consuel` porte les mêmes valeurs à reporter à la main — ou que le site du CONSUEL ne le sert pas), le schéma de principe (`schema-unifilaire` : le schéma unifilaire dessiné depuis l'étude, le mode d'injection noté à l'étape raccordement, le gestionnaire de réseau du territoire et les valeurs de l'électricien, chaque organe repéré par sa rubrique du dossier technique ; il accompagne l'attestation CONSUEL et, avec une batterie déclarée, la demande de raccordement à Enedis ; 422 sans étude), et le mandat de représentation (`mandat-representation` : le client mandate l'installateur pour la DP, Enedis et EDF OA — signé en ligne avec le devis depuis POST /projects/{id}/sign, ou à imprimer et faire signer ; hors mur Pro, il suit la signature). Génération réservée au plan Pro côté installateur (403 PLAN_REQUIRED), sauf le mandat.","security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"template","in":"query","required":false,"schema":{"type":"string","enum":["rapport-simulation","mandat-representation","cerfa-dp","plan-situation-dp","plan-masse-dp","plan-toitures-dp","photomontage-dp","notice-materiaux-dp","dossier-dp","dossier-raccordement","dossier-consuel","sc144-prerempli","schema-unifilaire","dossier-mise-en-service","dossier-edf-oa","pv-reception","daact"]},"description":"Identifiant du gabarit (source : document-templates-meta)."},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["text","site"]},"description":"`text` : la version aide-mémoire d'un récapitulatif ; `site` (plan-masse-dp seulement) : le terrain du plan de masse en JSON."}],"responses":{"200":{"description":"PDF (application/pdf, Content-Disposition attachment) — ou la liste des gabarits sans `template`, ou `{ site }` avec `format=site`"},"403":{"description":"PLAN_REQUIRED"},"422":{"description":"Adresse non géolocalisable (plans de situation et des toitures) ; plan des toitures : étude technique absente ou aucun bâtiment lu à l'adresse ; photomontage : plan des toitures non dessiné ici, photo absente ou ni JPEG, ni PNG, ni WebP, ni AVIF, coins qui ne dessinent pas le pan ; dossier technique CONSUEL : étude absente, ou qui ne dit pas micro-onduleurs ou chaîne, batterie ou non (le message dit quoi y dire) ; schéma de principe : étude absente"},"503":{"description":"Source publique indisponible (fond IGN, cadastre, BD TOPO), photo du photomontage non relue depuis le stockage, ou formulaire officiel du CONSUEL du jour pas encore relu ou non servi par consuel.com (rien n'est écrit)"}}},"post":{"summary":"Generate a piece and attach it to its step","tags":["Projects"],"description":"Génère le document et le JOINT à l'étape comme pièce du dossier (StepDocument). `plan-situation-dp` : pièce DPC1 de la déclaration préalable, libellée « DPC1 — Plan de situation du terrain » ; `plan-masse-dp` : pièce DPC2 (plan de masse coté dans les trois dimensions), dessinée depuis le cadastre, le bâti, les voies et les altitudes de l'IGN, l'étude et les réponses `sitePlan` gardées sur l'étape dp — `pose` `toiture` (le champ du plan des toitures reporté sur le toit) ou `sol` (avec `field` : le centre du champ en WGS 84, `heightM` : la hauteur hors tout de la structure, `azimuth` et `rows` facultatifs) ; quand la notice des matériaux a été rédigée, sa pose fait foi ; la réponse porte `sitePlan` : ce que le plan a coté (les distances à chaque limite, `roadDistanceDrawn` quand la voie a été reconnue), gardé sur l'étape (`metadata.dpSitePlanDrawn`) ; `plan-toitures-dp` : pièce DPC4 (plan des façades et des toitures, panneaux dessinés et cotés), dessinée depuis l'étude, le bâti de la BD TOPO de l'IGN et les réponses `roofPlan` (forme du toit, pan, disposition et dimensions des modules, rangées — chacune a une valeur courante) gardées sur l'étape dp, la teinte et le mode de pose repris de la notice quand elle existe — la génération garde aussi sur l'étape dp le résumé de son calepinage (`metadata.dpRoofPlanLayout`) ; `photomontage-dp` : pièce DPC6 (document graphique d'insertion), la photo `insertion.photoDocumentId` (une pièce photo — JPEG, PNG, WebP ou AVIF — de l'étape dp de ce dossier, 404 sinon) avec les panneaux de ce calepinage posés en perspective sur les quatre coins `insertion.corners` désignés dessus — réponses gardées sur l'étape dp ; `notice-materiaux-dp` : pièce DPC11 (notice des matériaux pour l'architecte des Bâtiments de France), rédigée depuis l'étude et les `answers` (teinte des modules, mode de pose, précisions) qui sont gardées sur l'étape dp — la liste de complétude de l'étape reconnaît la pièce et le Cerfa coche sa case sur le bordereau. `sc144-prerempli` : le dossier technique SC 144 écrit sur le formulaire officiel du jour (voir GET), joint à l'étape `consuel` sous « Dossier technique SC 144 » ; la réponse porte `sc144` : la variante (`form`), la version imprimée et l'empreinte du fichier officiel (`version`, `sha256`), le SC 144E-1 joint au cas 4 (`case4Form`), les rubriques écrites (`written`), les valeurs qui n'ont pas tenu sur leur pointillé (`notWritten`, à écrire à la main) et ce qui reste à l'électricien (`remaining`) ; l'étape garde la version sur laquelle la pièce a été écrite (`metadata.consuelSc144Prefill`, écrite par le serveur seul). `schema-unifilaire` : le schéma de principe, joint à l'étape `consuel` sous son titre ; la réponse porte `schema.missing` (ce qui reste à compléter à la main sur la page). Pour ces deux gabarits, `consuelTechnique` porte les valeurs de l'électricien, gardées sur l'étape avant le dessin (même quand la pièce ne se fait pas) et communes aux deux pièces. Une version générée précédente est remplacée ; une pièce téléversée par l'installateur n'est jamais touchée.","security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["template"],"properties":{"template":{"type":"string","enum":["plan-situation-dp","plan-masse-dp","plan-toitures-dp","photomontage-dp","notice-materiaux-dp","sc144-prerempli","schema-unifilaire"]},"consuelTechnique":{"type":"object","description":"Les valeurs que seul l'électricien connaît (cas de raccordement, tableau, câbles, protections, batterie), communes au dossier technique SC 144 et au schéma de principe. Toutes facultatives, sans défaut : chaque champ envoyé est validé puis gardé sur l'étape `consuel` (`metadata.consuelTechnique`), `null` efface la valeur, un champ absent reste tel quel, un champ inconnu est refusé (400). Une valeur jamais dite s'écrit à la main sur les pièces. Les questions qui concernent le dossier (micro-onduleurs ou chaîne, batterie ou non, vente totale) : GET /projects/{id}/admin-steps, étape `consuel`, `technique.questions`.","additionalProperties":false,"properties":{"connectionCase":{"type":"integer","nullable":true,"enum":[1,2,3,4],"description":"Cas de raccordement."},"connectionBoard":{"type":"string","nullable":true,"enum":["principal","divisionnaire"],"description":"Tableau de raccordement."},"iscMaxA":{"type":"number","nullable":true,"description":"Iscmax du générateur (A)."},"uocMaxV":{"type":"number","nullable":true,"description":"Uocmax du générateur (V)."},"stringCount":{"type":"integer","nullable":true,"description":"Nombre de chaînes."},"modulesPerString":{"type":"integer","nullable":true,"description":"Modules par chaîne."},"dcCableSectionMm2":{"type":"number","nullable":true,"description":"Section du câble DC (mm²)."},"dcCableVoltageV":{"type":"number","nullable":true,"description":"Tension assignée du câble DC (V)."},"dcCableTemperature":{"type":"string","nullable":true,"enum":["90","120"],"description":"Température admissible sur l'âme."},"dcSwitchVoltageV":{"type":"number","nullable":true,"description":"Interrupteur-sectionneur DC : tension assignée (V)."},"dcSwitchCurrentA":{"type":"number","nullable":true,"description":"Interrupteur-sectionneur DC : courant assigné (A)."},"surgeProtectionDc":{"type":"boolean","nullable":true,"description":"Parafoudre côté continu."},"pvPolarityEarthed":{"type":"boolean","nullable":true,"description":"Polarité du champ à la terre."},"identicalGeneratorCount":{"type":"integer","nullable":true,"description":"Nombre de générateurs identiques."},"microInverterCount":{"type":"integer","nullable":true,"description":"Nombre de micro-onduleurs."},"decoupling":{"type":"string","nullable":true,"enum":["integre","externe"],"description":"Découplage."},"acBranchCount":{"type":"integer","nullable":true,"description":"Nombre de branches."},"acBreakerRatingA":{"type":"number","nullable":true,"description":"Disjoncteur de la production (A)."},"rcdSensitivityMa":{"type":"integer","nullable":true,"description":"Sensibilité du différentiel (mA)."},"rcdType":{"type":"string","nullable":true,"enum":["AC","A","F","B"],"description":"Type du différentiel."},"acCableSectionMm2":{"type":"number","nullable":true,"description":"Section du câble AC (mm²)."},"surgeProtectionAc":{"type":"boolean","nullable":true,"description":"Parafoudre côté alternatif."},"agcpRatingA":{"type":"number","nullable":true,"description":"Disjoncteur de branchement (AGCP) (A)."},"batteryVoltageV":{"type":"number","nullable":true,"description":"Tension DC de la batterie (V)."},"batteryModuleCount":{"type":"integer","nullable":true,"description":"Nombre de batteries ou de modules."},"batteryDcSwitchVoltageV":{"type":"number","nullable":true,"description":"Interrupteur-sectionneur de la batterie : tension assignée (V)."},"batteryDcSwitchCurrentA":{"type":"number","nullable":true,"description":"Interrupteur-sectionneur de la batterie : courant assigné (A)."},"chargerReference":{"type":"string","nullable":true,"description":"Référence de l'onduleur-chargeur."},"backupCircuits":{"type":"boolean","nullable":true,"description":"Circuits secourus en cas de coupure."},"batteryFamily":{"type":"string","nullable":true,"enum":["lithium","plomb","autre"],"description":"Famille de la batterie."},"inverterPhase":{"type":"string","nullable":true,"enum":["monophase","triphase"],"description":"Onduleur monophasé ou triphasé."}}},"sitePlan":{"type":"object","description":"Plan de masse DPC2 seulement. Au sol, le centre du champ et la hauteur hors tout sont exigés ; l'orientation et les rangées ont une valeur courante (celle de l'étude, deux rangées).","required":["pose"],"properties":{"pose":{"type":"string","enum":["toiture","sol"]},"field":{"type":"object","nullable":true,"required":["lon","lat"],"properties":{"lon":{"type":"number","minimum":-180,"maximum":180},"lat":{"type":"number","minimum":-90,"maximum":90}},"description":"Pose au sol : le centre du champ (WGS 84)."},"heightM":{"type":"number","minimum":0.3,"maximum":6,"nullable":true,"description":"Pose au sol : hauteur hors tout de la structure au-dessus du sol (m)."},"azimuth":{"type":"number","minimum":0,"maximum":360,"nullable":true,"description":"Pose au sol : orientation des panneaux (azimut boussole) ; null : celle de l'étude."},"rows":{"type":"integer","minimum":1,"maximum":10,"nullable":true,"description":"Pose au sol : rangées de modules ; null : deux (une sous quatre modules)."}}},"answers":{"type":"object","description":"Notice DPC11 seulement.","properties":{"teinte":{"type":"string","enum":["noir","bleu","autre"]},"teinteAutre":{"type":"string","maxLength":80},"pose":{"type":"string","enum":["surimposition","integration","sol"]},"precisions":{"type":"string","maxLength":600}},"required":["teinte","pose"]},"roofPlan":{"type":"object","description":"Plan des toitures DPC4 seulement. Chaque champ a sa valeur courante : un toit à deux pans, le pan orienté comme l'étude, des modules debout de 1,72 × 1,13 m, les rangées calculées.","properties":{"shape":{"type":"string","enum":["deux-pans","quatre-pans","monopente","terrasse"],"default":"deux-pans"},"panAzimuth":{"type":"number","minimum":0,"maximum":360,"nullable":true,"description":"Orientation du pan équipé, azimut boussole (0 = nord, 180 = sud) ; null : celle de l'étude."},"moduleOrientation":{"type":"string","enum":["portrait","paysage"],"default":"portrait","description":"portrait : modules debout ; paysage : couchés."},"moduleLengthM":{"type":"number","minimum":0.8,"maximum":2.6,"default":1.72},"moduleWidthM":{"type":"number","minimum":0.5,"maximum":1.5,"default":1.13},"rows":{"type":"integer","minimum":1,"maximum":20,"nullable":true,"description":"Nombre de rangées ; null : calculé."}}},"insertion":{"type":"object","description":"Photomontage DPC6 seulement. La photo et les quatre coins du pan qui reçoit les panneaux, dans l'ordre : égout gauche, égout droit, haut droit, haut gauche — coordonnées normalisées (0 à 1, x vers la droite, y vers le bas) dans la photo telle qu'elle s'affiche, redressée selon son orientation EXIF. Les coins ne se croisent pas, tournent dans le sens trigonométrique à l'écran, l'égout plus bas que le haut ; un pan en triangle (croupe) porte son sommet en troisième et quatrième coin.","required":["photoDocumentId","corners"],"properties":{"photoDocumentId":{"type":"string","description":"Identifiant de la pièce photo (StepDocument JPEG, PNG, WebP ou AVIF de l'étape dp de ce dossier)."},"corners":{"type":"array","minItems":4,"maxItems":4,"items":{"type":"array","minItems":2,"maxItems":2,"items":{"type":"number","minimum":0,"maximum":1}}}}}}}}}},"responses":{"201":{"description":"Pièce jointe : `document` (StepDocument) et `replaced` (versions remplacées) ; `sc144` (dossier technique CONSUEL), `schema` (schéma de principe) ou `sitePlan` (ce que le plan de masse a coté) selon le gabarit"},"400":{"description":"Corps invalide (dont des coins du photomontage qui ne dessinent pas un pan, ou une valeur de l'électricien refusée — le message dit laquelle)"},"403":{"description":"PLAN_REQUIRED"},"404":{"description":"Projet introuvable, ou photo du photomontage qui n'est pas une pièce de l'étape dp du dossier"},"422":{"description":"Gabarit non joignable, adresse non géolocalisable ; plan des toitures : étude technique absente ou aucun bâtiment lu à l'adresse ; photomontage : plan des toitures non dessiné ici, photo ni JPEG ni PNG ou illisible ; dossier technique CONSUEL : étude absente ou qui ne fixe pas le formulaire ; schéma de principe : étude absente"},"503":{"description":"Source publique (fond IGN, cadastre, BD TOPO), formulaire officiel du CONSUEL du jour pas encore relu ou non servi (rien n'est écrit), ou stockage indisponible"}}}},"/projects/{id}/admin-steps/alerts":{"get":{"summary":"Get deadline alerts for a project","tags":["Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Alerts with overdue/warning/info levels"}}}},"/projects/{id}/admin-steps/deposit":{"get":{"summary":"Deposit package of a step — for the installer or their personal agent","tags":["Projects","Agent"],"description":"build.solar ne dépose pas à la place de l'installateur : il prépare, guide et garde la preuve ; l'installateur — ou l'agent personnel qui détient ses accès aux portails — dépose. Avec `step` (`dp`, `raccordement`, `consuel`, `mise-en-service`, `edf-oa`), le paquet de dépôt (pour `dp`, la zone d'urbanisme est relevée d'abord si elle ne l'a pas été — le geste de GET /urbanisme, jamais bloquant) : `pieces` (documents générés ici avec leur `downloadPath`, pièces jointes au dossier, pièces du bordereau manquantes avec quoi envoyer, pièces à fournir ; les pièces que l'outil fabrique et joint — dossier technique SC 144 et schéma de principe à l'étape `consuel`, et le schéma au paquet `raccordement` quand une batterie est déclarée en territoire Enedis — portent `generation` : `state` attached | to-generate | blocked, `blockedBy` (ce que l'étude doit dire), `requests` (les requêtes exactes pour les générer et les joindre, avec un exemple de corps), `missingValues` (les valeurs de l'électricien encore à dire), `remaining` (ce qui reste à la main)), `fields` (les valeurs que le portail va demander — client, lieu, parcelle, PDL, installation, entreprise — chacune avec sa source ; une valeur inconnue est `null` et dit quoi demander : un agent n'invente jamais une valeur), `mandate` (mandat de représentation signé ou à faire signer, téléchargement), `readiness` (prêt à déposer : prérequis, manques bloquants, avertissements), `procedure` (accès requis, avant, gestes dans l'ordre, points de passage humains — signature, identité, argent, décision : jamais franchis par un agent —, vérifier, si ça casse), `experience` (ce que l'outil sait : délais, coûts, règles datées) et `proof` : les requêtes exactes qui rapportent le dépôt (PATCH /enedis, PATCH /consuel, PATCH admin-steps, pièce jointe) — l'étape n'est « déposée » que quand sa preuve est dans le dossier ; Pour `raccordement`, jusqu'à 6 kVA par phase chez Enedis, la DP déposée suffit (récépissé : date et numéro rapportés sur l'étape `dp`) : `readiness` sans manque bloquant pour la déclaration, la pièce « Récépissé de dépôt de la déclaration préalable (numéro et date du dépôt) » (jointe à l'étape `dp`, ou à l'étape `raccordement` depuis sa ligne), le champ `raccordement.urbanAuthorization` (la pièce, sa source) et l'avertissement de prévenir Enedis si la mairie s'oppose ou demande des pièces, ou si un tiers conteste l'autorisation ; au-delà, l'accord de la mairie reste attendu, comme avant ; DP sans objet (étape `dp` « Non applicable ») : `raccordement.urbanAuthorization` vaut « Sans objet : aucune déclaration préalable pour ce dossier », aucune pièce d'urbanisme. `proof.recorded` relit cette preuve depuis le dossier (déposée ou non, date, numéro, canal, pièces jointes, et ce que l'outil attend ensuite — pour la DP, la non-opposition tacite calculée depuis la date du récépissé) : c'est là qu'un agent vérifie que son rapport a été reçu, et ce qui lui manque encore. `cooperation` : la relation coopérative avec l'agent — ce que nous donnons, ce que nous demandons et comment : nous dire une correction ou une expérience de terrain (POST /feedback, sujet `[Dépôt] …`, `context.kind` = `deposit-feedback`, `context.contribution` = `correction` ou `experience`, commune, correctif proposé), nous demander (POST /assist, réponse immédiate et sourcée), écrire au dossier ce qui est au dossier (preuves, pièces, étapes) ; et si votre retour ne nous suffit pas pour agir, notre agent vous pose au plus une question dans la réponse à votre envoi (`question`, `answerWith`). La source se corrige pour tous ; la ligne de l'agent porte « Votre remarque est en production » quand c'est fait. Sans `step`, l'index des cinq démarches du dossier. Lecture seule ; parties engagées ; même mur Pro que les documents pré-remplis côté installateur (403 PLAN_REQUIRED).","security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"step","in":"query","required":false,"schema":{"type":"string","enum":["dp","raccordement","consuel","mise-en-service","edf-oa"]},"description":"Démarche à déposer (source : deposit-procedures). Omis : l'index des démarches."}],"responses":{"200":{"description":"Le paquet de dépôt (ou l'index des démarches sans `step`)"},"400":{"description":"Démarche inconnue"},"403":{"description":"PLAN_REQUIRED"},"404":{"description":"Dossier introuvable ou inaccessible"}}}},"/enedis":{"patch":{"summary":"Report the connection request state (deposited, proposal received, accepted, energized)","tags":["Projects","Agent"],"description":"La preuve du dépôt de la demande de raccordement : `status` `DEPOSITED` avec `eventAt` (AAAA-MM-JJ, la date réelle du dépôt) et `enedisRefNumber` (numéro d'affaire). Puis `proposalReceivedAt` quand la proposition de raccordement arrive (son acceptation reste un geste humain), `ACCEPTED` à la signature du contrat ou de la convention, `ENERGIZED` à la mise sous tension. Réponses défavorables : `INCOMPLETE` (pièces demandées) ou `REFUSED`, avec `enedisNotes` (le courrier) — l'étape se bloque et porte l'issue. `connectionType` fixe la branche du dossier (vente du surplus, vente totale, sans injection). L'étape `raccordement` ne se pilote que par ici, jamais par son statut.","security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["projectId"],"properties":{"projectId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING_DEPOSIT","DEPOSITED","INCOMPLETE","ACCEPTED","REFUSED","ENERGIZED"]},"eventAt":{"type":"string","description":"Date de l'événement (AAAA-MM-JJ) — à défaut, aujourd'hui"},"enedisRefNumber":{"type":"string","description":"Numéro d'affaire attribué par le gestionnaire de réseau"},"connectionType":{"type":"string","enum":["autoconsommation-surplus","vente-totale","autoconsommation-sans-injection"]},"enedisNotes":{"type":"string","maxLength":500,"description":"Le courrier : pièces demandées ou motif du refus"},"proposalReceivedAt":{"type":"string","nullable":true,"description":"Date de réception de la proposition de raccordement (AAAA-MM-JJ) ; null efface"}}}}}},"responses":{"200":{"description":"État mis à jour (étape, metadata, issue)"},"400":{"description":"Corps invalide (aucun champ, date future ou mal formée)"},"404":{"description":"Dossier introuvable ou inaccessible"}}}},"/consuel":{"patch":{"summary":"Report the CONSUEL request state (deposited, visa obtained, refused, with reservations)","tags":["Projects","Agent"],"description":"La preuve de la demande d'attestation de conformité : `status` `DEPOSITED` avec `consuelDepositedAt` (AAAA-MM-JJ, date réelle du dépôt — l'horloge des relances part d'elle) et `consuelRequestNumber` (le numéro de la demande sur Mon Espace CONSUEL — la preuve, avant le visa), puis `PASSED` avec `consuelVisaNumber` quand le visa est obtenu (le CONSUEL le transmet lui-même au gestionnaire de réseau), `RESERVE` (visa avec réserves, listées dans `consuelNotes`) ou `FAILED` (refus, motif dans `consuelNotes`). L'étape `consuel` ne se pilote que par ici, jamais par son statut.","security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["projectId","status"],"properties":{"projectId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["DEPOSITED","PASSED","FAILED","RESERVE"]},"consuelVisaNumber":{"type":"string","maxLength":50},"consuelRequestNumber":{"type":"string","maxLength":50,"description":"Numéro de la demande sur Mon Espace CONSUEL, avec DEPOSITED : la preuve du dépôt (le visa a le sien, consuelVisaNumber)"},"consuelNotes":{"type":"string","maxLength":2000,"description":"Réserves à lever ou motif du refus"},"consuelDepositedAt":{"type":"string","description":"Date du dépôt (AAAA-MM-JJ), jamais dans le futur"}}}}}},"responses":{"200":{"description":"État mis à jour (étape, metadata)"},"400":{"description":"Corps invalide"},"404":{"description":"Dossier introuvable ou inaccessible"}}}},"/projects/{id}/quotes/upload":{"post":{"summary":"Upload a quote obtained outside the platform (PDF, or a photo of the signed paper quote)","tags":["Quotes"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Le PDF d'un devis obtenu hors plateforme — ou la photo du devis signé sur papier (image/jpeg, image/png, image/webp), mise en PDF d'une page (2026-10-23) — (installateur : son devis maison ou déjà signé ; client : un devis tiers) devient le devis du dossier : stocké tel quel (c'est le contrat), relu (`review` : TVA au regard de la puissance et du territoire du dossier, décennale, bordereau de rétractation, RGE — `attention`, `read`, `line`), lu par l'IA (émetteur, montant, lignes), promu en Quote. Sur un dossier dont l'étape Devis est déjà réglée (chantier déclaré « Devis signé » ou « Déjà posé », ou devis coché fait), le devis importé naît accepté — c'est le contrat, date de signature inconnue, personne n'est notifié — et la réponse porte `quoteAccepted: true`. Deux corps : **JSON** avec `fileUrl` d'un PDF déjà téléversé dans le stockage build.solar (POST /upload/token, upload client direct sans limite de taille de requête — le chemin de la carte « Mon devis » ; ou POST /upload) ; **multipart/form-data** avec le fichier, qui transite par la fonction — 4,5 Mo au plus en production. Rend l'ExternalQuote créé avec parseStatus = SUCCESS | PARTIAL | FAILED et `promotion`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["fileUrl"],"properties":{"fileUrl":{"type":"string","description":"URL rendue par le téléversement — dans le stockage build.solar seulement (Vercel Blob, ou /uploads/ en local)."},"fileKey":{"type":"string","description":"Clé de stockage rendue par le téléversement (`media/AAAA-MM/<id>.pdf`) ; déduite de l'URL si absente."},"fileName":{"type":"string","description":"Nom lisible du fichier."},"fileSize":{"type":"integer","description":"Taille en octets, déclarative : c'est le contenu relu qui est borné (20 Mo)."}}}},"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"application/pdf — 4,5 Mo au plus par cet appel (limite de la plateforme) ; au-delà, téléversez d'abord et passez `fileUrl` en JSON."},"fileName":{"type":"string","description":"Original human-readable name"}}}}}},"responses":{"201":{"description":"External quote created and parsed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalQuote"}}}},"400":{"description":"Invalid file (wrong type, too big, missing) ; `fileUrl` hors du stockage build.solar"},"404":{"description":"Projet introuvable ou accès refusé ; `fileUrl` sans fichier"},"413":{"description":"PDF trop lourd (20 Mo)"},"415":{"description":"Pas un PDF"}}}},"/projects/{id}/quotes/external":{"get":{"summary":"List external quotes attached to a project","tags":["Quotes"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"List of ExternalQuote, newest first","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ExternalQuote"}}}}}}}}}},"/projects/{id}/quotes/external/{quoteId}":{"delete":{"summary":"Remove an external quote from a project","tags":["Quotes"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"quoteId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted"},"404":{"description":"External quote or project not found"}}}},"/projects/{id}/quotes/external/{quoteId}/pdf":{"get":{"summary":"Stream the stored external quote PDF (authenticated proxy)","tags":["Quotes"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"quoteId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"PDF binary stream","content":{"application/pdf":{}}},"404":{"description":"External quote or project not found"}}}},"/dashboard/summary":{"get":{"summary":"Dashboard KPIs","tags":["Dashboard"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Returns role-specific KPIs (customer: projects, savings, CO2; installer: leads, quotes, revenue).","responses":{"200":{"description":"KPIs, recent projects/leads"}}}},"/auth/verify-email":{"get":{"summary":"Verify email with token","tags":["Auth"],"description":"Lien ouvert dans un navigateur (email de bienvenue). Vérifie le jeton puis redirige vers la page /auth/verifie, qui dit l'issue : etat=ok (email vérifié), expire (jeton expiré ou invalide — TTL 24 h), incomplet (jeton absent), erreur (panne serveur, le jeton est repassé pour réessayer).","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"303":{"description":"Redirection vers /auth/verifie?etat=ok|expire|incomplet|erreur"}}}},"/auth/reset-password":{"post":{"summary":"Request password reset email","tags":["Auth"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"Reset email sent (always returns 200)"}}},"put":{"summary":"Reset password with token","tags":["Auth"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["token","password"],"properties":{"token":{"type":"string"},"password":{"type":"string","minLength":8,"description":"Au moins 8 caractères avec une majuscule, une minuscule et un chiffre — ou assez long pour s'en passer : 50 bits d'entropie au moins, comptés sur les familles présentes (minuscules 26, majuscules 26, chiffres 10, autres 33). Refus : 400, le message nomme ce qui manque."}}}}}},"responses":{"200":{"description":"Password updated"}}}},"/auth/resend-verification":{"post":{"summary":"Resend email verification","tags":["Auth"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Verification email resent"}}}},"/upload":{"post":{"summary":"Upload file (server-side multipart)","tags":["System"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Upload multipart côté serveur. Limites par type : 10 Mo image, 20 Mo document, 500 Mo vidéo — MAIS sur Vercel la plateforme borne le corps de requête à ~4,5 Mo : au-delà, utilisez l'upload client direct via POST /upload/token (@vercel/blob/client). Champ optionnel `access` : 'private' (défaut — lisible uniquement via les routes de téléchargement authentifiées) ou 'public'. Rate-limit 30/min/utilisateur.","requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"},"access":{"type":"string","enum":["public","private"],"default":"private"}}}}}},"responses":{"201":{"description":"Upload result { url, key, size, contentType }"},"429":{"description":"Rate limit"},"503":{"description":"Storage non configuré"}}}},"/upload/token":{"post":{"summary":"Client-upload token (browser → Vercel Blob direct)","tags":["System"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Génère un token d'upload client direct (@vercel/blob/client, handleUpload) — le fichier ne transite pas par une fonction serverless, ce qui rend atteignables les plafonds réels (10 Mo image / 20 Mo document / 500 Mo vidéo). Pathname imposé au motif media/AAAA-MM/<uuid>.<ext>, content-type verrouillé, suffixe aléatoire ajouté. 501 hors mode Vercel Blob (repli : POST /upload multipart).","responses":{"200":{"description":"Client token (protocole @vercel/blob/client)"},"400":{"description":"Pathname ou type refusé"},"429":{"description":"Rate limit"},"501":{"description":"Storage non-blob — utiliser POST /upload"}}}},"/projects/{id}/documents/{docId}/download":{"get":{"summary":"Stream a shared project document (authenticated proxy)","tags":["Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Sert une pièce ProjectDocument (DOCUMENT_POSTED) via proxy authentifié — les nouveaux uploads sont des blobs privés sans URL directe. Visibilité scope-aware (calque du listing) + anti-IDOR.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"docId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Binary stream"},"404":{"description":"Projet ou document introuvable / hors scope"}}}},"/events/stream":{"get":{"summary":"Server-Sent Events stream (polling-based)","tags":["System"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"SSE stream for notifications. Polls every 5 seconds (not true real-time push). Connection closes after ~4 minutes (event timeout) — client must reconnect. Events: connected, heartbeat, notifications { count, notifications }, timeout. `count` = total réel de non-lues (la liste embarquée est plafonnée aux 5 plus récentes) ; l'event est émis à l'ouverture puis à chaque changement d'état, y compris le passage à zéro.","responses":{"200":{"description":"SSE stream","content":{"text/event-stream":{}}}}}},"/feedback":{"post":{"summary":"Send feedback — a person or an agent tells us what they verified or found wrong","tags":["Feedback","Agent"],"description":"Public, authentification facultative : avec une clé API, le retour est attribué au compte et à la clé (« est-ce moi ou mon agent ? »). Retour sur un paquet de dépôt (l'agent de l'installateur est un contributeur, décision fondateur 2026-10-18) : sujet commençant par `[Dépôt]`, ou `context.kind` = `deposit-feedback`, avec `context.contribution` (`correction` : le paquet est faux ou périmé ; `experience` : ce que vous avez appris sur le terrain, même sans rien à corriger — délai réel, pièce demandée en plus, refus et motif, particularité de la commune ou du portail), `context.step` (`dp`, `raccordement`, `consuel`, `mise-en-service`, `edf-oa`), `context.packageVersion` (la `version` du paquet), `context.commune` (facultatif) et, si vous en avez une, `context.proposedFix` (la correction en une ligne : où, quoi). Pour une question, préférez POST /assist : l'assistance répond tout de suite, sourcée. L'équipe en est prévenue aussitôt ; une ligne de procédure se corrige à la source pour tous les agents, une règle remonte en décision ; quand la correction est en production, la ligne porte « Votre remarque est en production » (`GET /feedback/{id}`). Limite : 5 envois par heure et par adresse.","security":[{},{"bearerAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["subject","body"],"properties":{"category":{"type":"string","enum":["BUG","FEATURE_REQUEST","COMPLAINT","PRAISE","QUESTION","OTHER"],"description":"OTHER si absent"},"subject":{"type":"string","minLength":3,"maxLength":200,"description":"Retour sur un paquet de dépôt : commencer par `[Dépôt] <démarche> — …`"},"body":{"type":"string","minLength":10,"maxLength":5000,"description":"Ce que vous avez constaté, ce que vous attendiez, où, ce que vous avez vérifié et comment"},"url":{"type":"string","format":"uri","nullable":true,"description":"La page du portail ou du paquet concernée"},"context":{"type":"object","nullable":true,"description":"Libre ; pour un paquet de dépôt : `kind`, `step`, `packageVersion`, `proposedFix`"}}}}}},"responses":{"201":{"description":"Retour enregistré (`feedback.id` : à relire sur GET /feedback/{id}). Retour sur un paquet de dépôt envoyé avec une clé ou une session : notre agent le lit aussitôt et, s'il lui manque de quoi agir, pose au plus une question — `question`, avec `answerWith` (POST /feedback/{id}/respond) ; la ligne attend alors votre réponse (AWAITING_USER)."},"400":{"description":"Corps invalide"},"429":{"description":"Trop d'envois depuis cette adresse"}}}},"/feedback/me":{"get":{"summary":"My feedback lines","tags":["Feedback","Agent"],"description":"Les retours envoyés par ce compte (humain ou agent), sans le fil des réponses.","security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"Liste des retours du compte"}}}},"/feedback/{id}/respond":{"post":{"summary":"Answer the question our agent asked about your feedback","tags":["Feedback","Agent"],"description":"Réponse de l'auteur sur sa ligne — en particulier à la question de précision posée par notre agent sur un retour de paquet de dépôt (au plus une). La ligne repasse de AWAITING_USER à TRIAGED ; pour un retour de paquet, l'équipe reçoit la précision aussitôt. Réservé à l'auteur ; une ligne fermée refuse (409).","security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"body":{"type":"string","minLength":1,"maxLength":5000,"description":"Votre réponse"}}}}}},"responses":{"201":{"description":"Réponse enregistrée (`response`, `status`)"},"404":{"description":"Retour introuvable ou inaccessible"},"409":{"description":"Retour fermé"}}}},"/feedback/{id}":{"get":{"summary":"One feedback line with the answers meant for you","tags":["Feedback","Agent"],"description":"Le retour et les réponses visibles de l'équipe — dont « Votre remarque est en production : … » quand la correction est déployée. Réservé à son auteur.","security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Le retour et son fil"},"404":{"description":"Retour introuvable ou inaccessible"}}}},"/mcp":{"post":{"summary":"Serveur MCP de l’outil (Streamable HTTP) — pour l’agent personnel de l’installateur","tags":["Agent","Assistance"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Un message JSON-RPC 2.0 par requête (pas de lot, pas de session, pas de flux SSE : GET répond 405). Méthodes : `initialize` (version du protocole négociée, `instructions` du serveur), `ping`, `tools/list`, `tools/call` ; les notifications (`notifications/initialized`) répondent 202 sans corps. Authentification par la clé API du compte (`Authorization: Bearer bsol_live_sk_…` ou `x-api-key`) ou une session ; sans elle, 401 avec `WWW-Authenticate`. Les outils sont les gestes de l'assistance de l'outil, le dossier nommé par le paramètre `dossier` : lectures pour tout installateur (`lister_dossiers`, `lire_a_traiter`, `lire_dossier`, `lire_paquet_de_depot`) ; gestes quand l'assistance qui fait est ouverte pour le compte (étude, devis, signature en ligne ou sur papier, pièces de la DP, dépôt et réponse de la mairie, pièces demandées au client, raccordement, CONSUEL, statut d'étape, création d'un dossier au devis signé), chacun annoté `readOnlyHint` / `destructiveHint` ; un geste répond FAIT (jeton d'annulation), PRÉPARÉ — RIEN N'EST ENCORE FAIT (jeton de confirmation : `confirmer_geste` après l'accord de la personne, `annuler_geste` le laisse) ou REFUSÉ avec la raison. Chaque geste est tracé comme fait par l'agent (clé). Limites : 120 messages par minute et par adresse, 60 par compte.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"oneOf":[{"type":"string"},{"type":"integer"}],"description":"Absent pour une notification"},"method":{"type":"string","enum":["initialize","ping","tools/list","tools/call","notifications/initialized"]},"params":{"type":"object","description":"tools/call : { name, arguments }"}}}}}},"responses":{"200":{"description":"Réponse JSON-RPC : `result` (initialize : protocolVersion, capabilities, serverInfo, instructions ; tools/list : tools[] ; tools/call : content[], structuredContent, isError) ou `error` (code, message)"},"202":{"description":"Notification reçue, sans corps"},"401":{"description":"Authentification requise (erreur JSON-RPC -32001, en-tête WWW-Authenticate)"},"429":{"description":"Trop de messages (erreur JSON-RPC -32000, Retry-After)"}}}},"/assist":{"get":{"summary":"Assistance : disponibilité pour ce compte","tags":["Assistance"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"L'assistance « Comment je fais ? » répond aux questions sur l'outil et sur la démarche solaire (mairie, Enedis, CONSUEL, EDF OA, TVA, barèmes), avec ses sources. Dit si elle est disponible pour ce compte : service branché, assistance intégrée non désactivée dans Mon compte. Chaque question est consignée dans la file Feedback (lue par l'équipe pour améliorer l'outil).","responses":{"200":{"description":"{ active, reason: null | service_not_active | integrated_agent_disabled, message, contactEmail, version, corpusVersion, deployed: [{ at, summary, question }] — les remarques de la personne mises en production par la maintenance mineure ces quatorze derniers jours (ce qui a changé, en une ligne), actions: booléen — l’assistance qui fait est ouverte pour ce compte, palier: 1 à 4 — ce que l’assistance ouvre pour ce compte, chaque palier comprenant les précédents (1 : elle répond et écoute ; 2 : elle fait les gestes dans l’outil ; 3 : elle prévient, avec le brief du matin ; 4 : elle lit la photo d’un document reçu et la range avec sa preuve) }"},"401":{"description":"Authentification requise"}}},"post":{"summary":"Poser une question à l’assistance","tags":["Assistance"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Réponse immédiate et sourcée (toute règle énoncée cite sa source et sa date : les sources uniques de l'outil ou une page d'un domaine officiel). Historique texte facultatif (12 tours au plus), page et dossier pour contextualiser (l'accès au dossier est vérifié ; un dossier inaccessible n'échoue pas la question). Une ligne Feedback est créée : statut TRIAGED quand la réponse est sourcée, ESCALATED sinon (réponse sans source, introuvable ou absent de l'outil, panne, retour sur l'outil) — l'équipe en est prévenue tout de suite ; il n'y a rien à « transmettre » : ce qui doit remonter remonte. Limite : 10 questions par minute et par compte.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["question"],"properties":{"question":{"type":"string","minLength":2,"maxLength":2000},"conversationId":{"type":"string","format":"uuid","description":"Identifiant du fil (créé par le serveur si absent)"},"history":{"type":"array","maxItems":24,"items":{"type":"object","required":["role","content"],"properties":{"role":{"type":"string","enum":["user","assistant"]},"content":{"type":"string","maxLength":4000}}}},"pathname":{"type":"string","maxLength":300,"description":"Page ouverte, ex. /dossiers/{id}/demarches"},"projectId":{"type":"string","format":"uuid"},"screen":{"type":"string","maxLength":8000,"description":"Texte de l’écran au moment de la question (titres, boutons et leur état, bandeaux) — l’espace le relève seul ; lu pour cette question, jamais conservé ; les noms, adresses, emails et téléphones des clients y sont masqués avant la lecture par le modèle"},"image":{"type":"object","description":"Une image jointe à la question : la capture d’écran demandée par l’assistance (« Joignez une capture d’écran de … »), ou la photo d’un document reçu que la personne joint elle-même (courrier de la mairie, récépissé, courrier du gestionnaire de réseau, attestation CONSUEL, devis signé) — lue pour cette question, jamais conservée par l’assistance elle-même ; quand l’assistance qui fait en note la preuve, elle rejoint l’étape comme pièce (geste fait), ou est mise de côté dans le stockage privé des pièces jusqu’au clic sur la carte : « Confirmer » la joint à l’étape, « Annuler » la supprime (PATCH, act leave)","required":["mediaType","data"],"properties":{"mediaType":{"type":"string","enum":["image/jpeg","image/png","image/webp","image/gif"]},"data":{"type":"string","description":"Base64 sans préfixe, 2,5 millions de caractères au plus (≈ 1,9 Mo)"}}}}}}}},"responses":{"200":{"description":"{ reply, sources: [{ url, title }], conversationId, turn, feedbackId, outcome (answered | unclear_in_tool | not_in_tool | bug | unknown | out_of_scope), outcomeLabel, sourced, escalated, model, actions: [{ id, tool, state (done | prepared | failed), title, lines, notice, link, confirmToken, undoToken, expiresAt }] — une carte par geste fait ou préparé par l’assistance qui fait (compte ouvert : tout le catalogue sur un dossier modifiable, la seule création d’un dossier au devis signé sans dossier), vide sinon }"},"400":{"description":"Validation"},"401":{"description":"Authentification requise"},"403":{"description":"Assistance intégrée désactivée pour ce compte"},"429":{"description":"Plus de 10 questions par minute"},"502":{"description":"Le modèle n’a pas répondu — réessayer ou écrire à l’équipe"},"503":{"description":"Service non activé sur la plateforme (SERVICE_NOT_ACTIVE)"}}},"patch":{"summary":"Confirmer, laisser ou annuler un geste de l’assistance qui fait","tags":["Assistance"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"{ act: confirm | undo | leave, token } — l'assistance qui fait : confirmer un geste préparé (rien n'est fait avant ce clic ; le jeton porte exactement le geste montré, 30 minutes), le laisser (leave, avec le jeton de confirmation : rien n'est fait, la carte du fil passe « laissée » et la photo mise de côté avec le geste est supprimée) ou défaire un geste fait (jeton d'une heure ; rien n'est défait si le dossier a bougé depuis). Le geste passe par la route de l'écran, avec les droits du compte et la trace de l'assistance intégrée. Rien d'autre ne se transmet par ici : ce qui doit remonter à l'équipe remonte par le tri de chaque question (POST).","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["act","token"],"properties":{"act":{"type":"string","enum":["confirm","undo","leave"]},"token":{"type":"string","maxLength":16000},"cardId":{"type":"string","format":"uuid","description":"La carte du fil qui porte le geste : son état rendu la remplace (GET /assist/thread)"}}}}}},"responses":{"200":{"description":"{ card } (état done, undone, left ou failed)"},"400":{"description":"Validation, ou carte expirée"},"401":{"description":"Authentification requise"},"403":{"description":"Assistance qui fait non ouverte pour ce compte"},"404":{"description":"Dossier introuvable pour ce compte"}}}},"/assist/thread":{"get":{"summary":"Le fil de l’assistance du compte, avec le brief du matin","tags":["Assistance"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Un fil par compte, côté serveur (palier 3, 2026-10-21) : les dernières entrées — questions (role user), réponses (role assistant : sources, cartes des gestes) et le brief du matin (role brief : ce qui demande l'attention de la personne, calculé depuis l'état de ses dossiers par les mêmes services que l'accueil, avec la carte du geste que l'assistance peut faire quand elle est ouverte pour le compte — préparée, jamais faite sans confirmation). Pour un installateur dont le palier 3 est ouvert (`palier` de GET /assist), le brief du jour est écrit à la première lecture s'il ne l'est pas encore ; un matin sans rien à dire n'apparaît pas. Les jetons des cartes préparées sont refrappés à chaque lecture (30 minutes) ; un geste fait ne se défait plus depuis le fil passé une heure.","responses":{"200":{"description":"{ entries: [{ id, role (user | assistant | brief), content, sources, actions: [cartes], brief: { items: [{ projectId, href, title, detail, tone, cardIds }], more, missing } | null, conversationId, turn, feedbackId, createdAt, readAt }], unread }"},"401":{"description":"Authentification requise"}}},"patch":{"summary":"Marquer le fil comme lu","tags":["Assistance"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["read"],"properties":{"read":{"type":"boolean","enum":[true]}}}}}},"responses":{"200":{"description":"{ read: nombre d’entrées passées lues }"},"400":{"description":"Corps attendu : { read: true }"},"401":{"description":"Authentification requise"}}}},"/notifications/unsubscribe":{"post":{"summary":"Ne plus recevoir le récap du lundi (lien signé du courriel)","tags":["System"],"security":[],"description":"Publique, protégée par le jeton signé que porte le pied du récap du lundi et son en-tête List-Unsubscribe (un clic, RFC 8058). JSON { token } depuis la page de confirmation, ou ?token= avec le corps `List-Unsubscribe=One-Click` depuis la messagerie. Coupe la préférence « Digest hebdomadaire » du compte (réactivable depuis Mon compte) ; idempotent. GET renvoie vers la page de confirmation sans rien couper.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"}}}}}},"responses":{"200":{"description":"{ ok: true, pref: \"weeklyReport\", already: boolean }"},"400":{"description":"Lien invalide ou expiré"},"429":{"description":"Trop de demandes"}}}},"/openapi":{"get":{"summary":"This OpenAPI specification","tags":["System"],"responses":{"200":{"description":"OpenAPI 3.1 JSON spec"}}}},"/projects/{id}/invitations":{"get":{"summary":"List project invitations (client invite installateur)","tags":["Invitations","Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Liste des invitations émises par le client propriétaire pour inviter un installateur sur son dossier.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Invitations triées par date de création décroissante"},"401":{"description":"Authentification requise"},"404":{"description":"Projet introuvable ou accès refusé"}}},"post":{"summary":"Invite an installer to the project","tags":["Invitations","Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Le client propriétaire invite un installateur par email libre ou via installerProfileId. Un email est envoyé et, si l'installateur a déjà un compte, une notification in-app est créée. Exige email vérifié.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"installerEmail":{"type":"string","format":"email","description":"Email de l’installateur (si on ne connaît pas son profil sur la plateforme)"},"installerProfileId":{"type":"string","format":"uuid","description":"Id d’un InstallerProfile existant"},"message":{"type":"string","maxLength":1000,"description":"Message personnel au destinataire"}}}}}},"responses":{"201":{"description":"Invitation créée, email envoyé"},"400":{"description":"Email invalide ou donnée manquante"},"401":{"description":"Authentification requise"},"403":{"description":"Email non vérifié"},"404":{"description":"Projet ou installateur introuvable"},"409":{"description":"Invitation PENDING déjà existante ou installateur déjà rattaché"}}}},"/projects/{id}/invitations/{invitationId}":{"delete":{"summary":"Revoke a pending invitation","tags":["Invitations","Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Le client propriétaire révoque une invitation encore en PENDING. Les invitations déjà répondues ne peuvent pas être révoquées.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"invitationId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Invitation révoquée"},"401":{"description":"Authentification requise"},"404":{"description":"Invitation ou projet introuvable"},"409":{"description":"Statut non révocable (non-PENDING)"}}}},"/projects/{id}/shares":{"get":{"summary":"List share links of a project (owner only)","tags":["Shares","Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Lien public en lecture du dossier (« shareable by default »). Retourne tous les shares (actifs, révoqués, expirés) avec viewCount et lastViewedAt.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Liste des shares triée par date de création décroissante"},"401":{"description":"Authentification requise"},"404":{"description":"Projet introuvable ou accès refusé"}}},"post":{"summary":"Create a public read-only share link","tags":["Shares","Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Génère un token public en lecture pour notaire / banque / agent immobilier / voisin. Anonymisation client par défaut. Cap doux 20 shares actifs/projet. Email vérifié exigé.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"audience":{"type":"string","enum":["NOTAIRE","BANQUE","AGENT_IMMOBILIER","VOISIN","AUTRE"],"default":"AUTRE"},"label":{"type":"string","maxLength":120},"durationDays":{"type":"integer","minimum":1,"maximum":365,"default":90},"anonymizeClient":{"type":"boolean"},"includeFinancials":{"type":"boolean"},"includeQuotes":{"type":"boolean"},"includeProductionData":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Share créé — payload { share: { id, audience, label, url, status, expiresAt, viewCount, ... } }"},"400":{"description":"Validation Zod (durée, libellé, audience)"},"401":{"description":"Authentification requise"},"403":{"description":"Email non vérifié"},"404":{"description":"Projet introuvable ou accès refusé"},"409":{"description":"Limite de 20 shares actifs/projet atteinte"}}}},"/projects/{id}/shares/{shareId}":{"patch":{"summary":"Update scope of an active share (label + include flags) — token preserved","tags":["Shares","Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Permet d'ajuster le périmètre du lien (label, anonymizeClient, includeFinancials, includeQuotes, includeProductionData) sans rotation de token. Audience et expiresAt sont immuables — recréer pour les changer. Refusé sur shares révoqués/expirés (409).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"shareId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","maxLength":120,"nullable":true},"anonymizeClient":{"type":"boolean"},"includeFinancials":{"type":"boolean"},"includeQuotes":{"type":"boolean"},"includeProductionData":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Share mis à jour"},"400":{"description":"Aucune modification fournie ou validation"},"401":{"description":"Authentification requise"},"404":{"description":"Projet ou share introuvable"},"409":{"description":"Share révoqué ou expiré"}}},"delete":{"summary":"Revoke a share link (terminal state)","tags":["Shares","Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Révoque définitivement le lien — le token devient 410 Gone immédiatement. État terminal, irréversible. Pour ré-ouvrir un partage, créer un nouveau ProjectShare.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"shareId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Share révoqué"},"401":{"description":"Authentification requise"},"404":{"description":"Projet ou share introuvable"},"409":{"description":"Share déjà révoqué"}}}},"/shares/{token}":{"get":{"summary":"Public read of a shared dossier (no auth)","tags":["Shares"],"security":[],"description":"Lookup public d'un ProjectShare par token. Payload anonymisé selon les flags posés à la création. Audience CLIENT (le lien de suivi du client, créé par le produit — voir /projects/{id}/client-updates) : rien d'anonymisé, plus `installer` (coordonnées de l'installateur), `client.mandateSignedAt`, `connectionConvention` et `clientPieces` (les pièces demandées au client, reçues ou non — envoi par POST /shares/{token}/pieces). Toute audience : `territory`, le territoire du chantier hors de métropole (`martinique`, `corse`, `mayotte`…), lu à l'adresse du dossier — null en métropole ; la page de suivi y lit les noms des étapes et des démarches (EDF SEI, EDM). Rate limit 60 req/min/IP. Cache-Control: no-store, X-Robots-Tag: noindex. Émet le webhook share.viewed une seule fois (claim atomique sur lastViewedAt: null).","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","minLength":32}}],"responses":{"200":{"description":"Vue publique du dossier — { view: { share, project, owner, client, installer, connectionConvention, territory, simulation, financial, steps, signedQuote, production } }"},"404":{"description":"Token inconnu ou format invalide"},"410":{"description":"Lien révoqué ou expiré (ou projet supprimé)"},"429":{"description":"Rate limit (60/min/IP)"}}}},"/shares/{token}/pieces":{"post":{"summary":"Le client envoie une pièce demandée, depuis son lien de suivi (sans compte)","tags":["Shares"],"security":[],"description":"Trois modes sur une route publique : (1) protocole de jeton d'upload client direct de @vercel/blob/client (corps `{ type: 'blob.generate-client-token', … }`, `clientPayload` = `{ contentType, code }`) — le navigateur téléverse ensuite droit vers le stockage privé, ce qui lève la limite serverless pour les photos de téléphone ; (2) déclaration JSON après cet upload : `{ code, fileName, fileUrl, fileKey, fileSize, contentType }` (fileUrl sur le stockage build.solar) ; (3) multipart `file` + `code`. La pièce doit avoir été demandée par l'installateur (422 sinon) ; types et tailles des règles partagées (photo JPEG, PNG, WebP, AVIF ≤ 10 Mo ; PDF ≤ 20 Mo pour les pièces « document »). La pièce devient une pièce de l'étape, l'installateur est prévenu. 30 envois par 10 minutes et par adresse. Cache-Control: no-store, X-Robots-Tag: noindex.","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","minLength":32}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["code","fileName","fileUrl","fileKey","fileSize","contentType"],"properties":{"code":{"type":"string","enum":["facture-electricite","photo-compteur","photo-maison-pres","photo-maison-loin","attestation-tva","rib"]},"fileName":{"type":"string","maxLength":255},"fileUrl":{"type":"string"},"fileKey":{"type":"string"},"fileSize":{"type":"integer"},"contentType":{"type":"string"}}}},"multipart/form-data":{"schema":{"type":"object","required":["file","code"],"properties":{"file":{"type":"string","format":"binary"},"code":{"type":"string"}}}}}},"responses":{"201":{"description":"{ piece: { code, label, hint, stepKey, stepName, accept, requestedAt, receivedAt, fileName } }"},"400":{"description":"Type, taille ou pièce inconnue"},"404":{"description":"Lien de suivi introuvable"},"410":{"description":"Lien fermé ou expiré, dossier inaccessible"},"422":{"description":"Pièce non demandée par l'installateur"},"429":{"description":"Trop d'envois"},"501":{"description":"Upload direct indisponible (stockage non Blob) — multipart"},"503":{"description":"Stockage indisponible"}}}},"/projects/{id}/client-updates":{"get":{"summary":"Nouvelles au client : état de la case, du lien de suivi, des envois et des pièces demandées","tags":["Projects","Shares"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Dossier porté par l'installateur (le client n'a pas de compte) : le produit écrit au client au nom de l'installateur à chaque étape franchie — déclaration déposée en mairie, accord de la mairie, raccordement accepté, pose terminée, conformité, mise en service, contrat d'achat — avec un lien de suivi sans compte (/suivi/{token}, ProjectShare d'audience CLIENT créé à la signature ou au premier jalon) ; l'installateur reçoit une copie de chaque nouvelle. Retourne : `installerHeld` (false si le client a un compte : rien ne part, l'espace lui parle), `clientEmail`, `automaticEmails` (la case « Prévenir mon client par email » — Project.clientUpdatesEnabled), `followUpUrl`, `share` (createdAt, expiresAt, viewCount, lastViewedAt), `sent` (jalons envoyés, le plus récent d'abord) et `pieces` (le catalogue des pièces que seul le client peut fournir — facture d'électricité, photo du compteur, photos de la maison DPC7 et DPC8, attestation de TVA signée, RIB — avec `requestedAt`, `receivedAt`, `fileName`). Parties engagées du dossier.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{ overview: { installerHeld, clientEmail, automaticEmails, followUpUrl, share, sent, pieces } }"},"401":{"description":"Authentification requise"},"404":{"description":"Projet introuvable ou accès refusé"}}},"patch":{"summary":"La case « Prévenir mon client par email » : cocher ou décocher l'envoi automatique","tags":["Projects","Shares"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Décision fondateur 2026-10-15 : l'installateur doit savoir que ces emails partent en son nom et pouvoir décocher l'envoi automatique, dossier par dossier. `automaticEmails: false` : plus aucune nouvelle ne part d'elle-même et l'email de l'exemplaire signé ne porte pas le lien de suivi ; le lien reste envoyable à la main (POST send-link). La même case se coche sur l'écran de signature (POST /projects/{id}/sign, `clientUpdates`).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["automaticEmails"],"properties":{"automaticEmails":{"type":"boolean"}}}}}},"responses":{"200":{"description":"{ overview }"},"400":{"description":"Validation"},"401":{"description":"Authentification requise"},"404":{"description":"Projet introuvable ou accès refusé"},"422":{"description":"Client à compte : rien à régler, son espace lui parle"}}},"post":{"summary":"Envoyer le lien de suivi, demander des pièces au client, retirer une demande","tags":["Projects","Shares"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"`send-link` : le lien de suivi part au client par email, au nom de l'installateur — à sa main, que l'envoi automatique soit coché ou non ; exige l'email du client sur le dossier (422 sinon). `request-pieces` + `pieces` : les pièces du catalogue (facture-electricite, photo-compteur, photo-maison-pres, photo-maison-loin, attestation-tva, rib) sont posées sur leurs étapes (metadata `clientPieces`) et une lettre part au client avec son lien — il envoie chaque pièce depuis son téléphone par POST /shares/{token}/pieces, elle devient une pièce de l'étape (StepDocument « … — envoyée par le client », les photos de la maison au code DPC7 / DPC8 que la complétude de la DP reconnaît) et l'installateur est prévenu ; une nouvelle demande d'une pièce reçue la remet à envoyer. `cancel-piece` + `piece` : retire une demande non reçue. Email vérifié exigé (gestes sortants).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["send-link"]}}},{"type":"object","required":["action","pieces"],"properties":{"action":{"type":"string","enum":["request-pieces"]},"pieces":{"type":"array","minItems":1,"items":{"type":"string","enum":["facture-electricite","photo-compteur","photo-maison-pres","photo-maison-loin","attestation-tva","rib"]}}}},{"type":"object","required":["action","piece"],"properties":{"action":{"type":"string","enum":["cancel-piece"]},"piece":{"type":"string","enum":["facture-electricite","photo-compteur","photo-maison-pres","photo-maison-loin","attestation-tva","rib"]}}}]}}}},"responses":{"200":{"description":"{ overview, pieces?, sentTo: email | null } — sentTo null sur request-pieces quand la demande est posée mais que la lettre n'est pas partie"},"400":{"description":"Action ou pièce inconnue"},"401":{"description":"Authentification requise"},"403":{"description":"Email non vérifié"},"404":{"description":"Projet introuvable ou accès refusé"},"422":{"description":"Client à compte, ou email du client absent du dossier"},"502":{"description":"L'email n'est pas parti (send-link)"}}}}}}