{"openapi":"3.1.0","info":{"title":"API unbonCV","version":"1.0.0","description":"API publique pour piloter vos CV et lettres de motivation par programme —\ncréation, édition, export PDF (avec ou sans filigrane) et export PNG — sans passer par le site.\n\n**Plusieurs lettres par CV** : un CV porte jusqu'à 10 lettres (une par entreprise/poste visé), chacune avec\nses objectifs (`target`), son contenu, sa mise en page et son PDF. Collection : `/api/v1/cvs/{id}/letters`.\nLes endpoints singuliers `/letter` (dépréciés) opèrent sur la lettre la plus récemment modifiée.\n\n**Objectifs à deux niveaux** : le CV porte les objectifs GLOBAUX (`data.applicationBrief` : `applicationType`, `targetPosition`) ;\nchaque lettre porte ses objectifs SPÉCIFIQUES (`target` : entreprise, description, destinataire, poste ajustable).\nChamps retirés (juil. 2026, ignorés silencieusement en écriture) : `applicationBrief.currentSituation(Detail)`,\net `applicationBrief.targetCompany`/`targetCompanyDescription`/`recipientName` — passez-les dans le `target` de la lettre.\n\n**Authentification** : en-tête `Authorization: Bearer <clé>`. Créez et gérez vos clés depuis votre compte (`/account`). \nLe secret complet (`ubcv_live_…`) n'est affiché qu'une seule fois à la création.\n\n**Scopes** (permissions par clé) : `cv:read`, `cv:write`, `pdf:export`, `letter:read`, `letter:write`, `account:read`, `account:write`, `billing:write`.\n\n**Limites** : 600 requêtes/minute par clé ; 120 exports/minute par utilisateur (PDF + PNG confondus).\n\n**Aucune fonctionnalité IA** n'est exposée par l'API (import, analyse, traduction et génération de lettre restent réservés au site).\n\n**Enveloppe** : succès `{ \"data\": … }` ; erreur `{ \"error\": { \"code\", \"message\" } }`."},"servers":[{"url":"https://www.unboncv.com","description":"Production"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"CV","description":"Créer, lire, modifier et supprimer des CV."},{"name":"Export","description":"Exporter un CV en PDF ou PNG."},{"name":"Lettre","description":"Créer, lire, modifier, dupliquer, supprimer et exporter les lettres de motivation d'un CV — jusqu'à 10 par CV, une par entreprise/poste visé (non-IA)."},{"name":"Édition","description":"Édition chirurgicale par opérations atomiques adressées par id."},{"name":"Découverte","description":"Introspection : schéma du document, catalogue des modèles, layout."},{"name":"Compte","description":"Abonnement, plans, paiements et configuration de compte."}],"paths":{"/api/v1/account":{"get":{"tags":["Compte"],"summary":"Statut du compte","description":"Abonnement, plans, paiements récents et configuration du filigrane. Scope `account:read`.","responses":{"200":{"description":"Compte.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AccountStatus"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/billing/checkout":{"post":{"tags":["Compte"],"summary":"Souscrire (initier un paiement mobile money)","description":"Crée une commande Campay ; l'utilisateur approuve par USSD sur son téléphone. Vérifier ensuite via `/billing/orders/{orderId}/status`. Scope `billing:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["planId","phoneNumber"],"properties":{"planId":{"type":"string","description":"Voir GET /api/v1/account (plans)."},"phoneNumber":{"type":"string"}}}}}},"responses":{"201":{"description":"Paiement initié.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CheckoutResult"}}}}}},"400":{"description":"Plan ou saisie invalide.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Abonnement déjà actif ou paiement en attente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Initiation du paiement impossible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/billing/orders/{orderId}/status":{"parameters":[{"$ref":"#/components/parameters/OrderId"}],"post":{"tags":["Compte"],"summary":"Vérifier / réconcilier un paiement","description":"Interroge Campay et active l'abonnement si le paiement est confirmé. Scope `billing:write`.","responses":{"200":{"description":"Statut du paiement.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OrderStatus"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/account/pdf-protection":{"put":{"tags":["Compte"],"summary":"Définir le texte de filigrane PDF","description":"`text` = null pour effacer. Scope `account:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","nullable":true,"maxLength":140}}}}}},"responses":{"200":{"description":"Filigrane mis à jour.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"text":{"type":"string","nullable":true}}}}}}}},"400":{"description":"Texte de filigrane invalide.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/cvs/{id}/photo":{"parameters":[{"$ref":"#/components/parameters/CvId"}],"get":{"tags":["CV"],"summary":"URL de la photo","description":"URL signée (1 h) de la photo, ou `null`. Scope `cv:read`.","responses":{"200":{"description":"Photo.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"photo":{"oneOf":[{"$ref":"#/components/schemas/PhotoUrl"},{"type":"null"}]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["CV"],"summary":"Téléverser la photo","description":"Multipart : `file` (JPG/PNG/WebP/GIF ≤ 5 Mo) + `baseRevision`. Scope `cv:write`.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file","baseRevision"],"properties":{"file":{"type":"string","format":"binary"},"baseRevision":{"type":"integer"}}}}}},"responses":{"200":{"description":"Photo enregistrée.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PhotoMutation"}}}}}},"400":{"description":"Image invalide ou trop volumineuse.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflit de révision.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["CV"],"summary":"Retirer la photo","description":"Scope `cv:write`.","parameters":[{"name":"baseRevision","in":"query","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Photo retirée.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PhotoMutation"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflit de révision.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/schema":{"get":{"tags":["Découverte"],"summary":"Descripteur du document CV","description":"Champs éditables, répéteurs (items + imbriqués) et énumérations, pour construire les opérations. Toute clé valide suffit.","responses":{"200":{"description":"Descripteur du document.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DocumentSchema"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/themes":{"get":{"tags":["Découverte"],"summary":"Thèmes disponibles","description":"Thèmes (id, nom, couleurs) pour `design.themeId`.","responses":{"200":{"description":"Thèmes.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"themes":{"type":"array","items":{"$ref":"#/components/schemas/Theme"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/templates":{"get":{"tags":["Découverte"],"summary":"Catalogue des modèles","description":"Modèles OFFERTS : intégrés et importés, politique d'exposition appliquée (les modèles désactivés par l'admin sont exclus). `allowedThemes` liste les thèmes autorisés pour chaque modèle (`null` = tous les thèmes de /api/v1/themes) — seules ces combinaisons modèle×thème sont proposées aux utilisateurs.","responses":{"200":{"description":"Modèles disponibles.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"templates":{"type":"array","items":{"$ref":"#/components/schemas/TemplateSummary"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/cvs/{id}/preview.svg":{"parameters":[{"$ref":"#/components/parameters/CvId"}],"get":{"tags":["Découverte"],"summary":"Aperçu SVG du CV dans un modèle × thème (essayage)","description":"Vignette SVG (page 1) du CV RÉEL rendue dans `?template=<id>` teinté par `?theme=<themeId>` — rien n'est persisté. La photo est une URL S3 signée (~30 min) : injecter le SVG INLINE dans le DOM, pas dans une balise <img>. L'essayage n'applique PAS la politique modèle×thème (le rendu n'est jamais bloqué) : pour connaître les combinaisons offertes, lire `allowedThemes` dans /api/v1/templates. Requiert `cv:read`.","parameters":[{"name":"template","in":"query","required":false,"schema":{"type":"string"},"description":"Id de modèle à essayer ; absent = modèle courant du CV."},{"name":"theme","in":"query","required":false,"schema":{"type":"string"},"description":"Id de thème (voir /api/v1/themes) ; absent = thème courant du CV."}],"responses":{"200":{"description":"SVG de la première page.","content":{"image/svg+xml":{"schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/v1/templates/{id}/layout":{"parameters":[{"$ref":"#/components/parameters/TemplateId"}],"get":{"tags":["Découverte"],"summary":"Capacités de layout d'un modèle","description":"Sections déplaçables, colonnes disponibles et placement par défaut.","responses":{"200":{"description":"Capacités de layout.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TemplateLayoutCapabilities"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/cvs/{id}/operations":{"parameters":[{"$ref":"#/components/parameters/CvId"}],"patch":{"tags":["Édition"],"summary":"Édition chirurgicale (opérations atomiques)","description":"Applique une suite d'opérations adressées par id (champs, items 1er/2e niveau, ordre, titres, déplacement de sections), atomiquement, avec verrou optimiste `baseRevision`. Requiert `cv:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["baseRevision","operations"],"properties":{"baseRevision":{"type":"integer","description":"Révision actuelle du CV."},"operations":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/components/schemas/CvOperation"}}}}}}},"responses":{"200":{"description":"CV mis à jour.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"cv":{"$ref":"#/components/schemas/Cv"}}}}}}}},"400":{"description":"Opération ou contenu invalide (item/section/ordre introuvable ou non permis).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflit de révision ou effacement total suspect.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/cvs/{id}/layout":{"parameters":[{"$ref":"#/components/parameters/CvId"}],"get":{"tags":["Découverte"],"summary":"Arrangement courant résolu du CV","description":"Colonnes ordonnées, titres et ordre des items — pour préparer des déplacements valides. Requiert `cv:read`.","responses":{"200":{"description":"Layout courant.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ResolvedCvLayout"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/cvs":{"get":{"tags":["CV"],"summary":"Lister les CV","description":"Requiert le scope `cv:read`.","responses":{"200":{"description":"Liste des CV de l'utilisateur.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"cvs":{"type":"array","items":{"$ref":"#/components/schemas/CvSummary"}},"lastOpenedCvId":{"type":"string","nullable":true}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["CV"],"summary":"Créer un CV","description":"Crée un CV à partir de données structurées (aucune IA). Requiert le scope `cv:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","example":"Développeur Backend"},"data":{"$ref":"#/components/schemas/CvData"}}}}}},"responses":{"201":{"description":"CV créé.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"cv":{"$ref":"#/components/schemas/Cv"}}}}}}}},"400":{"description":"Titre ou données invalides.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/cvs/{id}":{"parameters":[{"$ref":"#/components/parameters/CvId"}],"get":{"tags":["CV"],"summary":"Récupérer un CV","description":"Requiert le scope `cv:read`.","responses":{"200":{"description":"Le CV.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"cv":{"$ref":"#/components/schemas/Cv"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"tags":["CV"],"summary":"Modifier un CV (fusion partielle)","description":"Fusion profonde du champ `data` fourni. Verrou optimiste via `baseRevision` (révision courante du CV). Requiert le scope `cv:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"baseRevision":{"type":"integer","description":"Révision actuelle du CV (conflit 409 si obsolète).","example":3},"title":{"type":"string"},"data":{"$ref":"#/components/schemas/CvData"}},"required":["baseRevision"]}}}},"responses":{"200":{"description":"CV mis à jour.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"cv":{"$ref":"#/components/schemas/Cv"}}}}}}}},"400":{"description":"Données invalides ou aucune modification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Conflit de révision (rechargez le CV) ou effacement total suspect.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["CV"],"summary":"Supprimer un CV","description":"Requiert le scope `cv:write`.","responses":{"200":{"description":"CV supprimé.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"success":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/cvs/{id}/pdf":{"parameters":[{"$ref":"#/components/parameters/CvId"}],"get":{"tags":["Export"],"summary":"Re-télécharger le dernier PDF exporté","description":"URL signée du dernier PDF exporté, sans re-rendre. `available: false` si aucun. Scope `cv:read`.","responses":{"200":{"description":"Dernier PDF.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"available":{"type":"boolean"},"downloadUrl":{"type":"string"},"expiresIn":{"type":"integer"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Export"],"summary":"Exporter le CV en PDF","description":"Rend le CV en PDF et renvoie une URL de téléchargement signée (1 h). Abonnement requis. Requiert le scope `pdf:export`.","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatermarkOption"}}}},"responses":{"200":{"description":"PDF exporté.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PdfExport"}}}}}},"400":{"description":"CV incomplet : `error.missingFields` liste les champs requis manquants (vérifiables à blanc via GET /cvs/{id}/readiness).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/cvs/{id}/readiness":{"parameters":[{"$ref":"#/components/parameters/CvId"}],"get":{"tags":["Export"],"summary":"Complétude d'export (dry-run)","description":"Les anomalies bloquantes que renverrait l'export en 400 `CV_INCOMPLETE`, sans consommer d'export ni exiger d'abonnement — le contrat d'export, vérifiable à blanc. Scope `cv:read`.","responses":{"200":{"description":"Complétude.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"ready":{"type":"boolean"},"issues":{"type":"array","items":{"$ref":"#/components/schemas/CompletenessIssue"}}},"required":["ready","issues"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/cvs/{id}/png":{"parameters":[{"$ref":"#/components/parameters/CvId"}],"post":{"tags":["Export"],"summary":"Exporter le CV en PNG","description":"Rend le CV en images PNG (une par page, ~190 DPI), renvoyées en base64. Abonnement requis. Requiert le scope `pdf:export`.","responses":{"200":{"description":"PNG exportés.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PngExport"}}}}}},"400":{"description":"CV incomplet : `error.missingFields` liste les champs requis manquants (vérifiables à blanc via GET /cvs/{id}/readiness).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/cvs/{id}/letters":{"parameters":[{"$ref":"#/components/parameters/CvId"}],"get":{"tags":["Lettre"],"summary":"Lister les lettres du CV","description":"Résumés de toutes les lettres du CV (label, fraîcheur, PDF, corps rempli), triées de la plus récente à la plus ancienne. Requiert le scope `letter:read`.","responses":{"200":{"description":"Lettres du CV.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LetterList"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Lettre"],"summary":"Créer une lettre","description":"Crée une lettre vierge pré-remplie depuis le CV (sans IA), ou DUPLIQUE une lettre existante via `copyFromLetterId` (contenu + mise en page copiés, nouveau `letterId`, PDF non repris). `target` = les objectifs de CETTE lettre (entreprise/poste visés) : passez-le à la création — c'est ce qui permet N lettres → N candidatures sur un même CV. Plafond : 10 lettres par CV (409 `LETTER_LIMIT_REACHED`). Requiert le scope `letter:write`.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","maxLength":120,"description":"Nom d'affichage (ex. « Orange Cameroun — Développeur »)."},"target":{"$ref":"#/components/schemas/LetterTarget"},"copyFromLetterId":{"type":"string","description":"Duplique cette lettre du même CV au lieu de partir d'une lettre vierge."}}}}}},"responses":{"201":{"description":"Lettre créée.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LetterMutationResult"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Plafond de lettres atteint (`LETTER_LIMIT_REACHED`, 10 par CV).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/cvs/{id}/letters/{letterId}":{"parameters":[{"$ref":"#/components/parameters/CvId"},{"$ref":"#/components/parameters/LetterId"}],"get":{"tags":["Lettre"],"summary":"État d'une lettre","description":"Contenu, fraîcheur (vs le CV + la cible de la lettre) et complétude. Requiert le scope `letter:read`.","responses":{"200":{"description":"État de la lettre.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LetterState"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"tags":["Lettre"],"summary":"Modifier une lettre","description":"Modifie les blocs, la mise en page, le label et/ou la cible. Verrou optimiste via `baseToken`. Requiert le scope `letter:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"baseToken":{"type":"string","description":"Jeton opaque renvoyé dans `writeToken`. Conflit 409 si obsolète."},"blocks":{"type":"array","items":{"$ref":"#/components/schemas/LetterBlock"}},"design":{"type":"object","description":"Surcharges de mise en page (marges, tailles, alignements…)."},"label":{"type":"string","maxLength":120},"target":{"$ref":"#/components/schemas/LetterTarget"}},"required":["baseToken"]}}}},"responses":{"200":{"description":"Lettre mise à jour.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LetterMutationResult"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Lettre introuvable pour ce CV.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflit de version ou lettre périmée (langue du CV changée).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Lettre"],"summary":"Supprimer une lettre","description":"Supprime la lettre et son PDF exporté. Requiert le scope `letter:write`.","responses":{"200":{"description":"Lettre supprimée.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/cvs/{id}/letters/{letterId}/pdf":{"parameters":[{"$ref":"#/components/parameters/CvId"},{"$ref":"#/components/parameters/LetterId"}],"post":{"tags":["Lettre"],"summary":"Exporter une lettre en PDF","description":"Rend CETTE lettre en PDF et renvoie une URL signée (1 h). Abonnement requis. Requiert le scope `pdf:export`.","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatermarkOption"}}}},"responses":{"200":{"description":"PDF de lettre exporté.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PdfExport"}}}}}},"400":{"description":"Lettre incomplète (corps vide).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/cvs/{id}/letter":{"parameters":[{"$ref":"#/components/parameters/CvId"}],"get":{"tags":["Lettre"],"deprecated":true,"summary":"État de la lettre (déprécié)","description":"DÉPRÉCIÉ — opère sur la lettre la plus récemment modifiée. Utilisez `/letters/{letterId}`. Requiert le scope `letter:read`.","responses":{"200":{"description":"État de la lettre.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LetterState"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Lettre"],"deprecated":true,"summary":"Créer une lettre vierge (déprécié)","description":"DÉPRÉCIÉ — 409 `LETTER_EXISTS` dès que le CV a une lettre. Utilisez `POST /letters` (multi-lettres). Requiert le scope `letter:write`.","responses":{"201":{"description":"Lettre créée.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LetterMutationResult"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Une lettre existe déjà (utilisez la collection /letters).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"tags":["Lettre"],"deprecated":true,"summary":"Modifier la lettre (déprécié)","description":"DÉPRÉCIÉ — modifie la lettre la plus récemment modifiée. Utilisez `PATCH /letters/{letterId}`. Verrou optimiste via `baseToken`. Requiert le scope `letter:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"baseToken":{"type":"string","description":"Jeton opaque renvoyé dans `writeToken`. Conflit 409 si obsolète."},"blocks":{"type":"array","items":{"$ref":"#/components/schemas/LetterBlock"}},"design":{"type":"object","description":"Surcharges de mise en page (marges, tailles, alignements…)."}},"required":["baseToken"]}}}},"responses":{"200":{"description":"Lettre mise à jour.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LetterMutationResult"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Aucune lettre pour ce CV (créez-la d'abord).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflit de version ou lettre périmée (langue du CV changée).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/cvs/{id}/letter/pdf":{"parameters":[{"$ref":"#/components/parameters/CvId"}],"post":{"tags":["Lettre"],"deprecated":true,"summary":"Exporter la lettre en PDF (déprécié)","description":"DÉPRÉCIÉ — exporte la lettre la plus récemment modifiée. Utilisez `POST /letters/{letterId}/pdf`. Abonnement requis. Requiert le scope `pdf:export`.","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatermarkOption"}}}},"responses":{"200":{"description":"PDF de lettre exporté.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PdfExport"}}}}}},"400":{"description":"Lettre incomplète.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Clé API : `Authorization: Bearer ubcv_live_…`"}},"parameters":{"CvId":{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Identifiant du CV."},"LetterId":{"name":"letterId","in":"path","required":true,"schema":{"type":"string"},"description":"Identifiant de la lettre (voir GET /cvs/{id}/letters)."},"TemplateId":{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Identifiant du modèle (ex. M1)."},"OrderId":{"name":"orderId","in":"path","required":true,"schema":{"type":"string"},"description":"Identifiant de la commande de paiement."}},"responses":{"Unauthorized":{"description":"Clé API manquante, invalide ou révoquée.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PaymentRequired":{"description":"Abonnement actif requis pour exporter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Scope insuffisant pour cette clé.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Ressource introuvable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"Conflit de version / d'état.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Trop de requêtes (limite de débit atteinte).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","example":"NOT_FOUND"},"message":{"type":"string","example":"CV non trouvé"},"missingFields":{"type":"array","description":"Présent sur les 400 `CV_INCOMPLETE` des exports : les champs requis manquants, dans la même forme que GET /cvs/{id}/readiness.","items":{"$ref":"#/components/schemas/CompletenessIssue"}}},"required":["code","message"]}},"required":["error"]},"CompletenessIssue":{"type":"object","description":"Champ requis manquant pour l'export. `path` est stable (contrat) ; `label` et `message` sont des textes français destinés à l'affichage.","properties":{"path":{"type":"string","example":"personalInfo.phone","description":"Chemin stable du champ dans les données du CV (ou de la section : `experience`, `summary`…)."},"label":{"type":"string","example":"Téléphone"},"message":{"type":"string","example":"Téléphone est requis pour exporter le PDF."},"sectionId":{"type":"string","example":"personal"},"severity":{"type":"string","enum":["blocking","recommended"],"example":"blocking"}},"required":["path","label","message","severity"]},"CvOperation":{"description":"Opération d'édition chirurgicale, discriminée par `op`. `section` = clé de données (experience, education…) ; `parentId`+`nested` ciblent un item imbriqué (descriptions, items, technologies).","oneOf":[{"type":"object","required":["op","fields"],"properties":{"op":{"const":"setPersonalInfo"},"fields":{"type":"object","additionalProperties":true,"description":"Champs personalInfo partiels."}}},{"type":"object","required":["op","value"],"properties":{"op":{"const":"setSummary"},"value":{"type":"string"}}},{"type":"object","required":["op","section"],"properties":{"op":{"const":"addItem"},"section":{"type":"string"},"parentId":{"type":"string"},"nested":{"type":"string"},"item":{"type":"object","additionalProperties":true},"position":{"oneOf":[{"type":"integer"},{"type":"string","enum":["start","end"]}]}}},{"type":"object","required":["op","section","id","fields"],"properties":{"op":{"const":"updateItem"},"section":{"type":"string"},"id":{"type":"string"},"parentId":{"type":"string"},"nested":{"type":"string"},"fields":{"type":"object","additionalProperties":true}}},{"type":"object","required":["op","section","id"],"properties":{"op":{"const":"removeItem"},"section":{"type":"string"},"id":{"type":"string"},"parentId":{"type":"string"},"nested":{"type":"string"}}},{"type":"object","required":["op","section","order"],"properties":{"op":{"const":"reorderItems"},"section":{"type":"string"},"parentId":{"type":"string"},"nested":{"type":"string"},"order":{"type":"array","items":{"type":"string"}}}},{"type":"object","required":["op","section","title"],"properties":{"op":{"const":"setSectionTitle"},"section":{"type":"string","description":"Id de section template (ex. experience-section)."},"title":{"type":"string"}}},{"type":"object","required":["op","section","direction"],"properties":{"op":{"const":"moveSection"},"section":{"type":"string","description":"Id de section template."},"direction":{"type":"string","enum":["up","down","left","right"]}}},{"type":"object","required":["op"],"description":"Réorganise les sections gauche/droite pour minimiser la longueur verticale réelle du CV, paliers respectés. Le résumé professionnel et le contact ne finissent jamais dans la même colonne (séparation appliquée si besoin, sans jamais tronquer de contenu). No-op si déjà optimal ; 400 COLUMN_NOT_AVAILABLE sur un modèle mono-colonne. Calcul serveur déterministe : l'éditeur peut différer de quelques pixels, le rendu PDF final est identique.","properties":{"op":{"const":"balanceColumns"}}},{"type":"object","required":["op"],"properties":{"op":{"const":"setDesign"},"set":{"type":"object","additionalProperties":true,"description":"Surcharges de design (fusion profonde) : themeId, fontFamily, fontSizes.{rôle}, dates, name, birthDate, skill/languageLevelDisplay."},"unset":{"type":"array","items":{"type":"string"},"description":"Chemins à réinitialiser au défaut (ex. « fontSizes.body »)."}}}]},"DocumentSchema":{"type":"object","additionalProperties":true,"description":"Champs (personalInfo, summary), répéteurs (items + imbriqués descriptions/items/technologies) et énumérations (niveaux de langue, marchés, EntryDate…). Dates : birthDate en ISO « YYYY-MM-DD » strict ; dates d'entrées en EntryDate (range/point/duration, ancres {year, month?}) — voir la description du schéma CvData."},"Theme":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"colors":{"type":"object","properties":{"ink":{"type":"string"},"accent":{"type":"string"},"support":{"type":"string"}}}}},"AccountStatus":{"type":"object","properties":{"subscription":{"type":"object","properties":{"active":{"type":"boolean"},"expiresAt":{"type":"string","format":"date-time","nullable":true}}},"plans":{"type":"array","items":{"type":"object","additionalProperties":true}},"payments":{"type":"array","items":{"type":"object","additionalProperties":true}},"pdfProtection":{"type":"object","properties":{"text":{"type":"string","nullable":true},"defaultText":{"type":"string"},"maxLength":{"type":"integer"}}}}},"PhotoUrl":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"expiresIn":{"type":"integer"}}},"PhotoMutation":{"type":"object","properties":{"photoKey":{"type":"string","nullable":true},"revision":{"type":"integer"},"pdfKey":{"type":"string","nullable":true}}},"CheckoutResult":{"type":"object","properties":{"orderId":{"type":"string"},"campayReference":{"type":"string","nullable":true},"ussdCode":{"type":"string","nullable":true,"description":"Code USSD à composer pour approuver le paiement."},"operator":{"type":"string","nullable":true},"amountXaf":{"type":"integer"},"durationHours":{"type":"integer"},"provider":{"type":"string"},"status":{"type":"string"}}},"OrderStatus":{"type":"object","properties":{"orderId":{"type":"string"},"status":{"type":"string"},"activated":{"type":"boolean"}},"additionalProperties":true},"TemplateSummary":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"photoMode":{"type":"string"},"paperFormat":{"type":"string"},"supportedCountries":{"type":"array","items":{"type":"string"}},"forbiddenFields":{"type":"array","items":{"type":"string"}},"columnMode":{"type":"string","enum":["single","dual"]},"allowedThemes":{"type":"array","items":{"type":"string"},"nullable":true,"description":"Ids des thèmes autorisés pour ce modèle (politique admin) ; null = tous les thèmes."}}},"TemplateLayoutCapabilities":{"type":"object","properties":{"templateId":{"type":"string"},"columnMode":{"type":"string","enum":["single","dual"]},"columns":{"type":"array","items":{"type":"string"}},"movableSections":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"nativeSide":{"type":"string","enum":["left","right"]},"editorSectionId":{"type":"string","nullable":true},"label":{"type":"string"}}}},"defaultScopes":{"type":"object","properties":{"left":{"type":"array","items":{"type":"string"}},"right":{"type":"array","items":{"type":"string"}}}}}},"ResolvedCvLayout":{"type":"object","properties":{"templateId":{"type":"string"},"columnMode":{"type":"string","enum":["single","dual"]},"scopes":{"type":"object","properties":{"left":{"type":"array","items":{"type":"string"}},"right":{"type":"array","items":{"type":"string"}}}},"titles":{"type":"object","additionalProperties":{"type":"string"}},"itemOrder":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}},"WatermarkOption":{"type":"object","description":"Filigrane optionnel. Absent/false = sans filigrane ; true = texte enregistré (ou défaut) ; { text } = texte personnalisé.","properties":{"watermark":{"oneOf":[{"type":"boolean"},{"type":"object","properties":{"text":{"type":"string","maxLength":140}}}]}}},"CvSummary":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"pdfKey":{"type":"string","nullable":true},"photoKey":{"type":"string","nullable":true}}},"CvData":{"type":"object","description":"Contenu structuré du CV (personalInfo, summary, experience, education, skills, languages, projects, templateId, language, layout, design…). Champ libre validé côté serveur ; consultez l'éditeur pour la forme complète. PERMIS DE CONDUIRE — personalInfo.drivingLicense : tableau de codes fermés parmi « A1, A, B, C, D, E, F, G » (ex. [\"A\", \"B\"]) ; l'ancien texte libre (« Permis B ») est encore accepté en entrée et converti en codes ; le rendu ajoute le libellé localisé (« Permis A, B » en fr, « Licence B » en en…). FORMATS DE DATES — personalInfo.birthDate : ISO « YYYY-MM-DD » STRICTEMENT (tout autre format est ignoré en silence) ; dates d'entrées (experience/education/projects/certifications .date) : objet EntryDate { kind: \"range\", start/end: {year, month?} | null, ongoing?: bool } | { kind: \"point\", at: {year, month?} } | { kind: \"duration\", amount, unit: \"month\"|\"year\", in } — les chaînes « YYYY » / « YYYY-MM » et la forme legacy {startDate, endDate} (endDate « present » = en cours) sont aussi acceptées et normalisées ; month absent = année seule affichée, jamais de mois inventé.","additionalProperties":true},"Cv":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string","nullable":true},"data":{"$ref":"#/components/schemas/CvData"},"revision":{"type":"integer","description":"À fournir comme `baseRevision` lors d'un PATCH."},"pdfKey":{"type":"string","nullable":true},"photoKey":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"PdfExport":{"type":"object","properties":{"format":{"type":"string","example":"pdf"},"watermark":{"type":"boolean"},"downloadUrl":{"type":"string","format":"uri","description":"URL S3 signée, valable `expiresIn` secondes."},"pdfKey":{"type":"string"},"expiresIn":{"type":"integer","example":3600},"subscriptionExpiresAt":{"type":"string","format":"date-time","nullable":true}}},"PngExport":{"type":"object","properties":{"format":{"type":"string","example":"png"},"mimeType":{"type":"string","example":"image/png"},"scale":{"type":"number","example":2},"pageCount":{"type":"integer"},"pages":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer"},"width":{"type":"integer"},"height":{"type":"integer"},"base64":{"type":"string","description":"Image PNG encodée en base64."}}}}}},"LetterBlock":{"type":"object","properties":{"id":{"type":"string","example":"body"},"label":{"type":"string"},"text":{"type":"string"}}},"LetterTarget":{"type":"object","description":"Objectifs SPÉCIFIQUES d'UNE lettre (entreprise/poste visés) : surchargent les objectifs globaux du CV champ par champ pour cette lettre uniquement. Alimentent la fraîcheur (`isStale`) et la génération. Un champ absent hérite du CV.","properties":{"company":{"type":"string","maxLength":120,"description":"Entreprise/structure visée."},"companyDescription":{"type":"string","maxLength":280,"description":"Ce que fait la structure."},"position":{"type":"string","maxLength":120,"description":"Poste/domaine visé."},"recipientName":{"type":"string","maxLength":80,"description":"Destinataire (recruteur), optionnel."}}},"Letter":{"type":"object","properties":{"id":{"type":"string"},"version":{"type":"integer"},"language":{"type":"string"},"label":{"type":"string","description":"Nom d'affichage de la lettre."},"target":{"$ref":"#/components/schemas/LetterTarget"},"blocks":{"type":"array","items":{"$ref":"#/components/schemas/LetterBlock"}},"design":{"type":"object","additionalProperties":true},"sourceHash":{"type":"string"},"generatedAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"pdfKey":{"type":"string"}}},"LetterState":{"type":"object","properties":{"letter":{"oneOf":[{"$ref":"#/components/schemas/Letter"},{"type":"null"}]},"writeToken":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Jeton opaque à renvoyer comme `baseToken`."},"sourceHash":{"type":"string"},"isStale":{"type":"boolean","description":"true si la lettre ne correspond plus au CV/à sa cible/à sa langue."},"readinessIssues":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"LetterMutationResult":{"allOf":[{"$ref":"#/components/schemas/LetterState"},{"type":"object","properties":{"letter":{"$ref":"#/components/schemas/Letter"},"writeToken":{"type":"string","description":"Jeton opaque courant à renvoyer comme `baseToken`."}},"required":["letter","writeToken"]}]},"LetterSummary":{"type":"object","properties":{"id":{"type":"string"},"label":{"oneOf":[{"type":"string"},{"type":"null"}]},"language":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"isStale":{"type":"boolean"},"hasPdf":{"type":"boolean"},"hasBody":{"type":"boolean","description":"false tant que le corps de la lettre est vide (export impossible)."}}},"LetterList":{"type":"object","properties":{"letters":{"type":"array","items":{"$ref":"#/components/schemas/LetterSummary"}},"sourceHash":{"type":"string"},"readinessIssues":{"type":"array","items":{"type":"object","additionalProperties":true}},"maxLetters":{"type":"integer","example":10}}}}}}