{"openapi":"3.1.0","info":{"title":"build.solar API","version":"1.0.0","description":"API pour la plateforme de simulation et gestion d'installations photovoltaïques. Compatible avec les agents IA via clés API et idempotence.","contact":{"name":"build.solar","url":"https://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"}}},"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"},"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…)."},"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},"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"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["email","password","firstName","lastName"],"properties":{"email":{"type":"string"},"password":{"type":"string","minLength":8},"firstName":{"type":"string"},"lastName":{"type":"string"},"profileType":{"type":"string","enum":["PARTICULIER","INSTALLATEUR"]}}}}}},"responses":{"201":{"description":"User created + JWT token + verification URL"}}}},"/auth/me":{"get":{"summary":"Get current user","tags":["Auth"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"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"}}}}}},"responses":{"200":{"description":"Updated user"}}}},"/user/export":{"get":{"summary":"Export toutes les données personnelles (RGPD art. 15)","tags":["Auth","RGPD"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"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.","responses":{"200":{"description":"JSON téléchargeable (Content-Disposition: attachment)"},"401":{"description":"Authentification requise"},"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":[]}],"parameters":[{"$ref":"#/components/parameters/page"},{"$ref":"#/components/parameters/limit"},{"name":"status","in":"query","schema":{"type":"string"}}],"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. 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.","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}}}}}},"responses":{"200":{"description":"Project mis à jour"},"400":{"description":"Validation Zod (clé inconnue, format) ou PDL invalide (≠ 14 chiffres)"},"404":{"description":"Projet non accessible (inexistant ou caller pas owner/installateur ACCEPTED)"}}}},"/projects/{id}/simulations":{"post":{"summary":"Run simulation","tags":["Simulations"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/idempotencyKey"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["panelCount","panelModel","panelWattage","powerKwc","annualProdKwh","annualSavingsEur","systemCostEur","roiYears","co2AvoidedKg"],"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"}}}},"/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"],"properties":{"quoteRequestId":{"type":"string","format":"uuid"},"totalAmountEur":{"type":"number"},"validDays":{"type":"integer","default":30},"lineItems":{"type":"array"},"notes":{"type":"string"}}}}}},"responses":{"201":{"description":"Quote submitted. `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."},"403":{"description":"Installer role required"}}}},"/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/{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":"Installer with reviews and portfolio"}}},"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},"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)."}}}}}},"responses":{"200":{"description":"Updated profile"},"400":{"description":"SIRET Luhn-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},"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\".","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"}}}}}},"responses":{"201":{"description":"Demande de signature créée — signatureLink retourné"},"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)"}}},"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"}}}},"/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). 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 (5.5% up to 9 kWc, 20% above) is returned alongside. Optional `margin` raises the output price relative to the market anchor (0.1 = +10%). 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"],"default":"standard"},"margin":{"type":"number","minimum":0,"maximum":0.5,"description":"Price adjustment vs the market anchor (0.1 = +10%)"},"notes":{"type":"string"}}}}}},"responses":{"201":{"description":"Quote generated with line items and VAT breakdown"}}}},"/projects/offline":{"post":{"summary":"Create an offline project for an external client","tags":["Projects"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Installer creates a project for a client not on the platform. Auto-creates QuoteRequest and 10 administrative steps. Installer role required.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["clientName","name","address"],"properties":{"clientName":{"type":"string"},"clientEmail":{"type":"string","format":"email"},"clientPhone":{"type":"string"},"name":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"}}}}}},"responses":{"201":{"description":"Project created with admin steps and quote request"}}}},"/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. 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},"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. Chaque endpoint mélange une baseline autorisée avec les données réelles dès que le stock grandit — zéro gate sur le volume.","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 mixte : baseline LLM + devis ACCEPTED en DB, blend linéaire jusqu'à N=20.","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 mixte baseline + `AdministrativeStep.completedAt` réels.","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"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Steps with statuses and progress"}}},"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.","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"]}}}}}}}}},"responses":{"200":{"description":"Steps upserted"},"422":{"description":"Prerequisites not met / 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.","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"}}}}}},"responses":{"200":{"description":"Step updated"},"422":{"description":"Prerequisites not met / sensor-managed step status"}}}},"/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}/quotes/upload":{"post":{"summary":"Upload a PDF quote obtained outside the platform","tags":["Quotes"],"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}],"description":"Client uploads a PDF of a solar quote received from an external installer. The PDF is stored and Claude parses structured fields (installer, total, line items). Returns the created ExternalQuote with parseStatus = SUCCESS | PARTIAL | FAILED.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"application/pdf, max 20 MB"},"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)"},"404":{"description":"Projet introuvable ou accès refusé"}}}},"/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"],"parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Email verified"},"400":{"description":"Invalid or expired token"}}}},"/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}}}}}},"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` : 'public' (défaut) ou 'private' (lisible uniquement via les routes de téléchargement authentifiées). 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":"public"}}}}}},"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":{}}}}}},"/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. 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, 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)"}}}}}}