{"openapi":"3.0.3","info":{"title":"Vigilae Connect — API","version":"1.5.0","description":"API du cabinet : journal de surveillance pKYC, agrégats de portefeuille, les RESSOURCES clients et dossiers (v1.2 — création, lecture, édition), depuis la v1.3 le PARCOURS DE VIGILANCE complet : pièces justificatives, criblage, bénéficiaires effectifs, qualification d'alertes (spec Connect v2, lot 2), et depuis la v1.4 le BUNDLE DE PREUVE par dossier (GET /api/v1/dossiers/{id}/preuve — le même bundle que l'application, vérifiable hors ligne par le vérificateur autonome) et le catalogue de SUJETS de webhooks (lot 3). Authentification par clé API (Authorization: Bearer vgk_…). Aucune déclaration de soupçon, aucun verbatim client ne transite par cette API — l'API prépare, l'humain déclare (doctrine constante : aucun endpoint de dépôt DS n'existe ni n'existera).\n\nSCOPES / SCOPES (bilingue FR/EN) : historiques `lecture` / `ecriture` (journal pKYC + agrégats) et FINS par ressource — `clients:lecture`, `clients:ecriture`, `dossiers:lecture`, `dossiers:ecriture`, `pieces:lecture`, `pieces:ecriture`, `criblage:lecture`, `criblage:ecriture`, `journal:lecture` (journal d'audit, v1.5), et le scope DÉDIÉ `alertes:qualifier`. RÉTRO-COMPATIBILITÉ : une clé historique `lecture` vaut *:lecture et `ecriture` vaut *:ecriture ; l'inverse est faux — un scope fin n'ouvre ni les autres ressources ni les surfaces historiques (moindre privilège). EXCEPTION VOULUE : `alertes:qualifier` ne s'hérite JAMAIS d'une clé historique `ecriture` — qualifier une alerte est un acte de responsable, délégué explicitement en cochant ce scope à la création de la clé (par l'administrateur du cabinet). Scope manquant : 403 application/problem+json. / Legacy `lecture` grants every *:lecture scope, `ecriture` grants every *:ecriture; fine-grained scopes never widen, and the dedicated `alertes:qualifier` scope is never inherited from a legacy key.\n\nIA / AI : l'analyse documentaire reste gatée par les verrous IA de la plateforme. Une pièce déposée par l'API sans verrou est stockée et classée, JAMAIS analysée — le champ `analyse` le dit honnêtement (`inerte` | `possible` | `faite`). Aucune analyse implicite au dépôt, dans tous les cas. / Documents uploaded through the API are stored and classified, never analyzed unless the platform AI locks are engaged; the `analyse` field states it (`inerte` = inert).\n\nERREURS (nouveaux endpoints) : RFC 9457 `application/problem+json` — champs `type` (URI documentaire stable sous /developpeurs/erreurs#…), `title` (français), `status`, `detail`. Les endpoints historiques conservent leur champ `error` (additif).\n\nIDEMPOTENCE : tout POST de création exige l'en-tête `Idempotency-Key` (chaîne stable ≤ 200 caractères, une par opération). Rejeu même clé + même corps : la MÊME réponse est resservie, aucun doublon ; même clé + corps différent : 422 problem+json. Mémoire de rejeu : 24 h. Sans l'en-tête : 400 explicite.\n\nPAGINATION (ressources) : curseur opaque — `?limite=` (écrêté à 500, la réponse n'est JAMAIS non bornée) et `?curseur=` ; réponse `{ donnees, curseur_suivant }` ; `curseur_suivant` null = fin de liste, sinon le repasser tel quel à l'appel suivant.\n\nCLOISONNEMENT : toute ressource d'un autre cabinet répond 404 (jamais 403) — l'existence d'une ressource étrangère n'est jamais révélée.\n\nCLÉS : chaque clé expire (expire_le, 1 an à la création — visible dans GET /connect/cles) ; une clé échue reçoit un 401 explicite. Débit : 60 req/min par clé par défaut (surchargeable par clé) ; un 429 porte Retry-After et X-RateLimit-Limit/-Remaining/-Reset.\n\nUSAGE NAVIGATEUR : le préflight CORS accepte l’en-tête Authorization sur /api/v1 (préparation SDK), mais appeler l’API depuis un navigateur EXPOSE la clé à quiconque inspecte la page — cette API est conçue server-to-server : passez par un proxy serveur qui détient la clé, jamais par du code livré au navigateur.\n\nWIDGETS EMBARQUÉS (v1.5, lot 1 — Screening Widget) : pour AFFICHER l’état d’un dossier dans votre logiciel, votre SERVEUR forge un jeton d’embed court via POST /api/v1/embed/jetons (scope DÉDIÉ `embed:emettre`, jamais hérité d’une clé historique — même règle que `alertes:qualifier`) et pose l’URL rendue dans une iframe : le jeton voyage dans le FRAGMENT d’URL (jamais en query, jamais dans un log), la config d’embed dans `?c=` (référence opaque publique). Le widget MONTRE (statut, risque, dernière vérification, compteurs d’alertes) et son bouton OUVRE l’application — aucune décision, qualification ni déclaration ne passe par un widget. / Embedded widgets: your server mints a short-lived embed token (dedicated `embed:emettre` scope, never inherited); the token travels in the URL fragment; widgets display state and open the app — no decision ever flows through a widget.\n\nWEBHOOKS SORTANTS : chaque livraison porte x-vigilae-signature = t=<unix>,v1=HMAC-SHA256(secret, t + \".\" + corps). Le récepteur recalcule v1 et DOIT rejeter tout horodatage t au-delà de la tolérance de 5 minutes (anti-rejeu) ; l’idempotence se fait par seq (livraison at-least-once, lots de 100 événements max). Pendant une rotation de secret (POST /connect/webhook/rotation), l’en-tête porte un v1 par secret encore valide (chevauchement 24 h) : accepter si L’UN correspond. TRANSITOIRE : l’ancien format sha256=HMAC-SHA256(secret, corps) est encore émis sur x-vigilae-signature-legacy — retrait prévu avec les webhooks v2.\n\nSUJETS DE WEBHOOKS (catalogue v1.4, ABONNEMENT configurable depuis la v1.5) : chaque livraison porte x-vigilae-evenement = le SUJET du lot. Catalogue : pkyc.evenements (flux par défaut — TOUS les événements du journal pKYC, l’en-tête historique inchangé) et un sujet fin par type RÉEL du journal : pkyc.revue_due, pkyc.piece_expiree, pkyc.score_bascule, pkyc.be_change, pkyc.sanction_nouvelle, pkyc.bodacc_mutation. Seuls des sujets dont l’événement source EXISTE sont annoncés — les sujets prévus par la spec sans flux émetteur (criblage.alerte, dossier.scelle, piece.recue…) naîtront avec leurs émetteurs. ABONNEMENT : POST /connect/webhook accepte `sujets` (tableau de sujets du catalogue, sans doublon — 400 avec rappel du catalogue sinon ; omis ou pkyc.evenements = tous) ; PATCH /connect/webhook corrige l’abonnement SANS régénérer le secret (sujets absent = conservé) ; la rotation le conserve. Les lots sont HOMOGÈNES par sujet, l’ordre du journal est préservé, et les événements hors abonnement ne sont jamais relivrés (le curseur les passe). La sélection PAR ENDPOINT (plusieurs webhooks par cabinet) arrive avec les webhooks v2 multi-endpoints (M37). / Webhook topics: only subjects with a real source event are announced; per-topic subscription is configurable on the single v1 webhook (`sujets` on POST/PATCH, validated against the catalogue); multi-endpoint selection ships with M37.\n\nJOURNAL D’AUDIT (v1.5) : GET /api/v1/journal-audit exporte le journal de SÉCURITÉ du cabinet (qui a gouverné quoi sur les accès : second facteur posé/retiré/réinitialisé, politique du cabinet, SSO configuré, fiche ou brouillon supprimé, criblage de masse, export de contrôle, qualification par API…). Scope DÉDIÉ `journal:lecture` (une clé SIEM/auditeur peut ne porter QUE lui) ; une clé historique `lecture` le couvre — cohérence stricte du modèle « lecture vaut *:lecture ». CONTENU DS-SAFE : codes d’action bornés, identifiants d’opérateur et cibles techniques (id de dossier/fiche, ip, issuer) — JAMAIS un nom de client, jamais un verbatim, jamais une déclaration de soupçon. Pagination par curseur STABLE sur l’id : ?depuisId= (strict, défaut 0) et ?limite= (écrêtée à 500), tri id croissant, réponse { entrees, prochainId } — page vide : prochainId fait écho (polling sans dérive). / Audit journal: the firm’s security journal (bounded action codes, operator identifiers — never client names), dedicated `journal:lecture` scope (covered by a legacy `lecture` key), stable id-based cursor pagination."},"components":{"securitySchemes":{"cleApi":{"type":"http","scheme":"bearer","description":"Clé API (Authorization: Bearer vgk_…). Scopes : lecture (défaut), ecriture, clients:lecture, clients:ecriture, dossiers:lecture, dossiers:ecriture. Expire 1 an après création (401 explicite au-delà)."}},"parameters":{"idempotencyKey":{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":200},"description":"Clé d'idempotence, OBLIGATOIRE sur tout POST de création : rejeu même clé + même corps = même réponse sans effet ; même clé + corps différent = 422. Absente : 400."},"limite":{"name":"limite","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":500},"description":"Taille de page, écrêtée à 500 (réponse toujours bornée)."},"curseur":{"name":"curseur","in":"query","schema":{"type":"string"},"description":"Curseur opaque : repasser le curseur_suivant de la page précédente. Absent = première page."}},"schemas":{"Probleme":{"type":"object","description":"Erreur RFC 9457 (application/problem+json).","properties":{"type":{"type":"string","description":"URI documentaire stable (/developpeurs/erreurs#…)"},"title":{"type":"string","description":"Titre français"},"status":{"type":"integer"},"detail":{"type":"string"}}},"PageClients":{"type":"object","properties":{"donnees":{"type":"array","items":{"type":"object"}},"curseur_suivant":{"type":["string","null"],"description":"null = fin de liste ; sinon à repasser en ?curseur="}}},"PageDossiers":{"type":"object","properties":{"donnees":{"type":"array","items":{"type":"object"}},"curseur_suivant":{"type":["string","null"],"description":"null = fin de liste ; sinon à repasser en ?curseur="}}}}},"security":[{"cleApi":[]}],"paths":{"/api/v1/openapi.json":{"get":{"summary":"Cette spécification (PUBLIQUE : lisible sans clé)","security":[],"responses":{"200":{"description":"document OpenAPI"}}}},"/api/v1/sante":{"get":{"summary":"Vivacité + authentification de la clé","responses":{"200":{"description":"ok"}}}},"/api/v1/evenements":{"get":{"summary":"Journal de surveillance pKYC (événements neutres) — réponse TOUJOURS bornée (500 max)","description":"Pagination par curseur : passer depuisSeq (0 au premier appel) et limite ; la réponse est triée par seq croissant et rend prochainSeq, à repasser en depuisSeq à l’appel suivant. Une page plus courte que limite = fin du journal ; prochainSeq reste alors stable et sert de curseur de polling. Sans depuisSeq : vue « plus récents d’abord », bornée à 500, prochainSeq = plus grand seq servi.","parameters":[{"name":"statut","in":"query","schema":{"type":"string","enum":["ouvert","qualifie","clos"]}},{"name":"type","in":"query","schema":{"type":"string"}},{"name":"depuisSeq","in":"query","schema":{"type":"integer","minimum":0},"description":"Curseur : seuls les événements de seq STRICTEMENT supérieur sont rendus, par seq croissant."},{"name":"limite","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":500},"description":"Taille de page, écrêtée à 500."}],"responses":{"200":{"description":"evenements[] + prochainSeq (curseur)"}}}},"/api/v1/portefeuille/agregats":{"get":{"summary":"Agrégats de portefeuille (compteurs, jamais un dossier)","responses":{"200":{"description":"agregats"}}}},"/api/v1/evenements/{seq}/qualifier":{"post":{"summary":"Qualifier un événement pKYC (scope ECRITURE) : statut qualifie|clos, disposition planifie|traite|ecarte","parameters":[{"name":"seq","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"evenement"},"403":{"description":"scope ecriture requis"},"404":{"description":"introuvable (ou autre cabinet)"}}}},"/api/v1/clients":{"post":{"summary":"Créer une fiche client / Create a client (scope clients:ecriture, Idempotency-Key requis)","description":"Personne physique ou morale. Discriminants de criblage complets — ddn (AAAA-MM-JJ), nationalite, numeroPiece, siren — saisis UNE fois sur la fiche puis repris par POST /api/v1/dossiers (zéro double saisie). Champs : nom (requis), type (physique|morale), siren, email, telephone, adresse, contactNom, contactFonction, notes, langue, ddn, nationalite, numeroPiece. Une clé fournie mais vidée (\"\") efface volontairement le champ.","parameters":[{"$ref":"#/components/parameters/idempotencyKey"}],"responses":{"201":{"description":"{ client }"},"400":{"description":"validation ou Idempotency-Key absent (application/problem+json)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Probleme"}}}},"403":{"description":"scope clients:ecriture requis (application/problem+json)"},"422":{"description":"Idempotency-Key déjà utilisée avec un autre corps (application/problem+json)"}}},"get":{"summary":"Lister les fiches clients / List clients (scope clients:lecture, pagination curseur)","parameters":[{"$ref":"#/components/parameters/limite"},{"$ref":"#/components/parameters/curseur"}],"responses":{"200":{"description":"{ donnees, curseur_suivant }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageClients"}}}},"403":{"description":"scope clients:lecture requis (application/problem+json)"}}}},"/api/v1/clients/{id}":{"get":{"summary":"Fiche client détaillée + dossiers rattachés / Client detail (scope clients:lecture)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ client }"},"404":{"description":"introuvable — y compris toute fiche d'un autre cabinet (application/problem+json)"}}},"patch":{"summary":"Mettre à jour une fiche / Update a client (scope clients:ecriture) — fusion, mêmes validations que l'app","description":"PATCH partiel : champ absent = conservé ; champ fourni vidé (\"\") = effacé volontairement ; nom fourni vide = 400 (le nom ne se vide jamais).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ client }"},"400":{"description":"validation (application/problem+json)"},"404":{"description":"introuvable (ou autre cabinet)"}}}},"/api/v1/dossiers":{"post":{"summary":"Ouvrir un dossier de vigilance depuis une fiche client / Open a file (scope dossiers:ecriture, Idempotency-Key requis)","description":"client_id REQUIS : l'identité complète (nom, ddn, nationalité, n° de pièce, SIREN) est reprise de la fiche — jamais ressaisie (même cœur métier que la création de dossier de l'app : registre d'intégrité chaîné dès la naissance, AUCUNE pré-cotation, le moteur calcule et l'humain décide). Champs opératoires optionnels : montant, pays (ISO-2), paiement, montantEspeces, residenceFiscale (fr_resident|non_resident), ppeType|ppeFonction|ppeCessationLe, origineFondsDocumentee, destinationFondsDocumentee, objetRelation, contrepartie, statutExercice, modele, brouillon, note.","parameters":[{"$ref":"#/components/parameters/idempotencyKey"}],"responses":{"201":{"description":"{ id, statut }"},"400":{"description":"validation ou Idempotency-Key absent (application/problem+json)"},"403":{"description":"scope dossiers:ecriture requis"},"404":{"description":"client_id inconnu de ce cabinet"},"422":{"description":"Idempotency-Key déjà utilisée avec un autre corps"}}},"get":{"summary":"Lister les dossiers / List files (scope dossiers:lecture, pagination curseur, filtre statut)","parameters":[{"name":"statut","in":"query","schema":{"type":"string","enum":["ouvert","extraction","scelle"]}},{"$ref":"#/components/parameters/limite"},{"$ref":"#/components/parameters/curseur"}],"responses":{"200":{"description":"{ donnees, curseur_suivant } — projection légère (id, client, statut, risque, alertes_ouvertes)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PageDossiers"}}}},"400":{"description":"filtre statut invalide (application/problem+json)"},"403":{"description":"scope dossiers:lecture requis"}}}},"/api/v1/dossiers/{id}":{"get":{"summary":"Détail d'un dossier / File detail (scope dossiers:lecture) : état, risque décomposé, complétude, alertes","description":"risque = cotation VERSIONNÉE avec sa décomposition par facteur et regles_version — jamais un score nu ; risque.perimee et cotationPerimee signalent un calcul dont les entrées ont changé (à relancer par l'humain). pieces.controle = contrôle documentaire indicatif (expirées/bientôt/manquantes) ; pieces.checklist = pièces attendues du modèle ; alertes.ouvertes = correspondances de criblage restant à qualifier par un humain.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"détail (identite, risque{niveau,score,decomposition,version,regles_version,perimee}, pieces, alertes, decision)"},"404":{"description":"introuvable — y compris tout dossier d'un autre cabinet (application/problem+json)"}}},"patch":{"summary":"Éditer un dossier / Edit a file (scope dossiers:ecriture) — liste blanche partagée avec l'app","description":"Seuls les champs d'identification/opération de la liste blanche de l'app sont éditables (jamais decision, scellement, cotation ni criblage : ils se gagnent par les gestes du parcours). Si une entrée d'un calcul déjà fait change, le calcul est marqué PÉRIMÉ (cotationPerimee/criblagePerime dans la réponse) — aucune re-cotation implicite. Dossier scellé : 409.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ ok, modifies, cotationPerimee, criblagePerime }"},"400":{"description":"validation (application/problem+json)"},"404":{"description":"introuvable (ou autre cabinet)"},"409":{"description":"dossier scellé : registre figé (application/problem+json)"}}}},"/api/v1/dossiers/{id}/pieces":{"post":{"summary":"Déposer une pièce justificative / Upload a supporting document (scope pieces:ecriture, Idempotency-Key requis)","description":"Le MÊME format d'upload que l'application : JSON avec `type` (code STABLE parmi a_doc_identite, a_doc_domicile, a_doc_fonds, a_doc_kbis, a_doc_statuts, a_doc_rib), `filename` (extension pdf|jpg|jpeg|png|webp — 415 sinon, ancre /developpeurs/erreurs#format-piece) et `contenu` (fichier en base64 standard). Borne : 6 Mo décodés (413, ancre #piece-trop-volumineuse). Le contenu est CHIFFRÉ au repos (coffre AES-256-GCM, mêmes règles que le portail de collecte) et son empreinte SHA-256 est calculée serveur. `analyse` dans la réponse dit ce que la plateforme fera de la pièce : `inerte` (verrous IA non posés : stockée et classée, JAMAIS analysée), `possible` (verrous posés, analyse à déclencher par un humain), `faite` (une extraction a eu lieu). Aucune analyse implicite au dépôt. Dossier scellé : 409 (#dossier-scelle).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/idempotencyKey"}],"responses":{"201":{"description":"{ piece (méta : id, type, nom, sha256, statut, created_at), analyse }"},"400":{"description":"validation ou Idempotency-Key absent (application/problem+json)"},"403":{"description":"scope pieces:ecriture requis"},"404":{"description":"dossier introuvable (ou autre cabinet)"},"409":{"description":"dossier scellé : pièces figées, manifeste lié à la chaîne (application/problem+json #dossier-scelle)"},"413":{"description":"fichier au-delà de 6 Mo (#piece-trop-volumineuse)"},"415":{"description":"format non accepté (#format-piece)"},"422":{"description":"Idempotency-Key déjà utilisée avec un autre corps"}}},"get":{"summary":"Lister les pièces + checklist de complétude / List documents & completeness (scope pieces:lecture)","description":"Méta des pièces (JAMAIS le contenu des fichiers), contrôle documentaire indicatif (expirées / bientôt / vieillissantes / manquantes — les moteurs de l'application), checklist du modèle de dossier (pièce attendue -> collectée), et l'état `analyse` honnête (inerte | possible | faite selon les verrous IA).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ pieces[], controle, checklist, analyse }"},"403":{"description":"scope pieces:lecture requis"},"404":{"description":"dossier introuvable (ou autre cabinet)"}}}},"/api/v1/dossiers/{id}/criblage":{"post":{"summary":"Déclencher le criblage / Run screening (scope criblage:ecriture, Idempotency-Key requis)","description":"Le MÊME moteur que l'application : client + contrepartie + bénéficiaires effectifs (>= 25 %) + personnes morales interposées, réseau (<25 %) en signal informatif séparé. Réponse 200 : résultats par dimension ET par ENTITÉ (listes en correspondance, rapprochements, erreurSource PAR ENTITÉ — une panne partielle est toujours DITE : les hits des entités qui ont répondu sont conservés, les entités en échec sont nommées dans erreurSourceEntites, jamais un « propre » silencieux). Chaque correspondance ouvre une alerte a_qualifier — la qualification est HUMAINE, jamais automatique. Panne TOTALE de la source : 502 application/problem+json (#source-criblage) — le dossier n'est PAS criblé et l'échec est tracé au dossier. Dossier scellé : 409 (chaîne figée). La fraîcheur des listes est FIGÉE au moment du criblage (fraicheur).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/idempotencyKey"}],"responses":{"200":{"description":"{ effectue, dernierCriblageLe, moteur, erreurSource, erreurSourceEntites[], resultats[], entites[] (nom, role, match, listes, matches, erreurSource), fraicheur, alertes { ouvertes, liste[] } }"},"400":{"description":"Idempotency-Key absent (application/problem+json)"},"403":{"description":"scope criblage:ecriture requis"},"404":{"description":"dossier introuvable (ou autre cabinet)"},"409":{"description":"dossier scellé (#dossier-scelle)"},"422":{"description":"Idempotency-Key déjà utilisée avec un autre corps"},"429":{"description":"criblages trop rapprochés (Retry-After)"},"502":{"description":"source de criblage indisponible : AUCUNE entité criblée (#source-criblage)"}}},"get":{"summary":"Dernier état de criblage / Latest screening state (scope criblage:lecture)","description":"Dernier état PAR ENTITÉ (listes en correspondance, rapprochements, erreur source), alertes OUVERTES (y compris celles nées de la surveillance post-scellement, hors chaîne), drapeau perime (une entrée d'identité a changé depuis), et fraîcheur des listes figée au criblage. effectue=false tant qu'aucun criblage n'a eu lieu — jamais un état inventé.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"état (effectue, resultats, entites, erreurSource(Entites), fraicheur, alertes)"},"403":{"description":"scope criblage:lecture requis"},"404":{"description":"dossier introuvable (ou autre cabinet)"}}}},"/api/v1/dossiers/{id}/beneficiaires-effectifs":{"get":{"summary":"Bénéficiaires effectifs évalués / Beneficial owners (scope dossiers:lecture)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ evalues, seuil: 25, beneficiaires (nbBeneficiaires, beneficiaires[], tousProprietaires[], interposeeMorale, alerte, coherenceCapital), complexite }"},"403":{"description":"scope dossiers:lecture requis"},"404":{"description":"dossier introuvable (ou autre cabinet)"}}},"post":{"summary":"Déclarer la chaîne de détention, évaluée par le moteur / Declare ownership, engine-assessed (scope dossiers:ecriture, Idempotency-Key requis)","description":"DÉCLARATIF : detentions[] = { nom (requis), pct (%), mode (physique|morale|controle_fait), ddn, nationalite, numeroPiece, pays, niveau }. Le MOTEUR évalue (seuil légal 25 % — AMLR art. 62 / L.561-2-2 CMF, d'ordre public, jamais configurable) : personne morale >= 25 % = interposée signalée (remonter la chaîne), contrôle de fait déclaré = BE quel que soit le capital, cohérence du capital contrôlée. MÊME geste que l'évaluation de l'application : la cotation de risque est (r)établie dans la foulée — décomposée, versionnée, l'ajustement humain antérieur PRÉSERVÉ. Les variables AMLR déclaratives (aDistance, structureComplexe, operationInhabituelle, relationOccasionnelle, secteurRisque — booléens) sont acceptées : des faits déclarés, jamais inférés. Dossier scellé : 409.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/idempotencyKey"}],"responses":{"200":{"description":"{ seuil, beneficiaires, complexite, risque (niveau, score, decomposition, regles_version, perimee, ajustee) }"},"400":{"description":"validation ou Idempotency-Key absent (application/problem+json)"},"403":{"description":"scope dossiers:ecriture requis"},"404":{"description":"dossier introuvable (ou autre cabinet)"},"409":{"description":"dossier scellé (#dossier-scelle)"},"422":{"description":"Idempotency-Key déjà utilisée avec un autre corps"}}}},"/api/v1/dossiers/{id}/alertes/{aid}/qualifier":{"post":{"summary":"Qualifier une alerte de criblage / Qualify a screening alert (scope DÉDIÉ alertes:qualifier)","description":"Acte HUMAIN du professionnel assujetti, porté par l'intégration : `statut` parmi levee | classee | escaladee (les MÊMES statuts que l'application — aucune autre disposition n'existe) et `operateur` OBLIGATOIRE (identité de la personne qui qualifie, chaîne non vide <= 120 caractères). L'identité est portée par la qualification (qualifieePar) et tracée au journal du dossier et au journal de sécurité du cabinet. Dossier SCELLÉ : la décision vit HORS chaîne (la tête scellée ne bouge pas — y compris pour une alerte née du re-criblage post-scellement) ; la réponse porte horsChaine=true. L'escalade ne déclenche RIEN : décision et déclaration restent des gestes humains dans l'application (aucun endpoint DS, jamais). Le scope alertes:qualifier ne s'hérite d'aucune clé historique : il se coche à la création de la clé (délégation explicite de la direction du cabinet). La responsabilité réglementaire du professionnel assujetti demeure entière.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"aid","in":"path","required":true,"schema":{"type":"string"},"description":"identifiant de l'alerte (GET /criblage, alertes.liste[].id)"}],"responses":{"200":{"description":"{ alerte (statut, qualifieePar, qualifieeLe), horsChaine }"},"400":{"description":"statut hors liste ou operateur absent (application/problem+json #validation)"},"403":{"description":"scope alertes:qualifier requis — jamais hérité d'une clé historique"},"404":{"description":"dossier ou alerte introuvable (ou autre cabinet)"}}}},"/api/v1/dossiers/{id}/preuve":{"get":{"summary":"Bundle de preuve du dossier / Evidence bundle (scope dossiers:lecture)","description":"LE MÊME bundle que l'application (« Télécharger le registre », schema vigilae-registre-1), à l'octet près (constitution partagée, jamais une seconde implémentation) : chaîne d'intégrité SHA-256 complète (events), manifeste des pièces (noms + empreintes sha256 — JAMAIS le contenu des fichiers), méthodologie scellée (version des règles, seuils), et ancre Merkle + horodatage RFC 3161 si le sceau du dossier a été ancré. VÉRIFICATION : le vérificateur autonome existant reste LE canal de contrôle — node bin/verifier-preuve.mjs <bundle.json>, zéro dépendance, hors de Vigilae : « vérifiable par recalcul », jamais une affirmation d'immuabilité. Un dossier NON scellé est servi AUSSI (scelle=false, liste vivante des pièces — même comportement que l'application) ; scellé, `pieces` = le manifeste FIGÉ dans l'événement de scellement. L'en-tête de réponse x-vigilae-empreinte-bundle porte sha256=<empreinte des octets exacts du corps> (archivage : le récepteur peut prouver ce qu'il a reçu). DS-safe : aucune déclaration de soupçon ne figure jamais dans un bundle (objet confidentiel séparé, anti-tipping-off). / Same bundle as the application, byte-for-byte; verify offline with the standalone verifier (recalculation, no dependency).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"bundle vigilae-registre-1 (id, client, scelle, methodologie, algorithme, verification, events[], pieces[], ancre?) + en-tête x-vigilae-empreinte-bundle"},"403":{"description":"scope dossiers:lecture requis (application/problem+json #scope-manquant)"},"404":{"description":"dossier introuvable — y compris tout dossier d'un autre cabinet (#ressource-introuvable)"}}}},"/api/v1/journal-audit":{"get":{"summary":"Journal d'audit de sécurité du cabinet / Firm security audit journal (scope DÉDIÉ journal:lecture — couvert par une clé historique lecture)","description":"Qui a gouverné quoi sur les accès et les gestes sensibles du cabinet : mfa_active, mfa_desactive, mfa_reinitialise, politique_mfa_exigee/levee, politique_quatre_yeux_exigee/levee, bascule_entite, fonction_lcbft_attribuee/retiree, sso_configure, fiche_client_supprimee, dossier_brouillon_supprime, criblage_masse, extrait_controle, alerte_qualifiee_api… CONTENU DS-SAFE : codes d'action bornés, identifiants d'opérateur (e-mail de compte) et cibles techniques (id de dossier/fiche, ip:…, tenant:…, issuer) — JAMAIS un nom de client, jamais un verbatim, jamais une déclaration de soupçon. Pagination par curseur STABLE sur l'id (croissant = ordre d'écriture) : passer depuisId (0 au premier appel) puis repasser prochainId ; page vide = prochainId en écho (polling sans dérive). Réponse TOUJOURS bornée (500 max). / Bounded action codes and operator identifiers only — never a client name; stable id-based cursor, response always bounded.","parameters":[{"name":"depuisId","in":"query","schema":{"type":"integer","minimum":0},"description":"Curseur : seules les entrées d'id STRICTEMENT supérieur sont rendues, par id croissant. Absent = 0 (début du journal)."},{"name":"limite","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":500},"description":"Taille de page, écrêtée à 500."}],"responses":{"200":{"description":"{ entrees: [{ id, action, acteur, cible, le }], prochainId } — projection fermée, rien d'autre ne sort"},"403":{"description":"scope journal:lecture requis (application/problem+json #scope-manquant)"}}}},"/api/v1/embed/jetons":{"post":{"summary":"Forger un jeton d'embed / Mint an embed token (scope DÉDIÉ embed:emettre, jamais hérité)","description":"Appelé par VOTRE SERVEUR (jamais par du code navigateur : la clé vgk_ n'atteint jamais la page). Corps { dossier_id, composant: \"criblage\", audience, ttl? } — `audience` est UNE origine https déclarée par l'administrateur du cabinet dans sa configuration d'embed (validation stricte : hôte explicite, jamais de joker ; 400 hors liste), `ttl` en minutes, borné 5-60 (défaut 15). Réponse 201 { jeton, expire_le, url } : `url` est prête à iframer — la config voyage en query (`?c=` référence opaque publique, elle pilote la CSP frame-ancestors et le thème du shell), le jeton voyage dans le FRAGMENT (il n'atteint jamais les logs serveur). Un jeton = UN dossier, UN composant, UNE origine, un TTL court — jamais de jeton de portefeuille ; sans état (aucune table de jetons) ; révocation par dossier (re-vérifié à chaque appel) et par tenant (bump du nonce d'embed : tous les jetons meurent). Idempotency-Key NON requise : l'émission est répétable par nature. Le widget est en LECTURE SEULE : statut, risque, dernière vérification, compteurs d'alertes — jamais le détail des correspondances, jamais une décision. 501 tant que la configuration d'embed du cabinet n'est pas disponible sur l'instance. / Minted by YOUR server; the token travels in the URL fragment; widgets are display-only.","responses":{"201":{"description":"{ jeton, expire_le, url } — url = /embarque/{composant}?c={ref}#{jeton}"},"400":{"description":"audience non déclarée, composant non activé ou corps invalide (#validation)"},"403":{"description":"scope embed:emettre requis — jamais hérité d'une clé historique (#scope-manquant)"},"404":{"description":"dossier introuvable — y compris tout dossier d'un autre cabinet (#ressource-introuvable)"},"501":{"description":"stockage des origines d'embed non disponible sur cette instance (#embed-indisponible)"}}}}}}