Une API qui se lit avant de se signer.
La spécification est publique. Les flux de décisions (webhook, rejeu, lecture par dossier) sont neutres : ni adresse d'article, ni motif rédigé. La signature des webhooks se vérifie en dix lignes chez vous. Cette page documente ce que l'API expose aujourd'hui — rien de plus, et c'est volontaire : ce qui n'est pas encore livré n'est pas documenté.
- Spécification OpenAPI lisible sans clé ni compte
- Jamais de déclaration de soupçon, jamais de verbatim client
- Webhooks signés, vérifiables hors de Vigilae
Servie par l'API elle-même : le contrat que vous lisez est celui qui tourne.
Le parcours entier, et deux exclusions qui ne bougeront pas.
Votre logiciel peut mener un dossier de bout en bout : créer le client, ouvrir le dossier, déposer les pièces, lancer le criblage, déclarer les bénéficiaires effectifs, lire la cote et retirer le dossier de preuve. Deux choses ne passeront jamais par là, et ce n'est pas une limitation d'abonnement : aucune déclaration de soupçon, et aucun verbatim de votre client. La première est un acte que la loi réserve à la personne qui déclare ; le second n'a rien à faire dans un tuyau d'intégration.
Le dossier, de bout en bout
Clients, dossiers, pièces justificatives, criblage, bénéficiaires effectifs, cote de risque, dossier de preuve, contrôle d'intégrité. Chaque ressource a sa portée : votre clé n'ouvre que ce que vous lui avez donné.
Journal et agrégats
Les événements pKYC du cabinet — pièce expirée, changement de bénéficiaire effectif, re-criblage à traiter — et des compteurs de portefeuille pour votre tableau de bord. Réponse toujours bornée, curseur stable.
Ce que vous écrivez porte un nom
Déposer, cribler, qualifier : chaque écriture exige sa portée et trace l'opérateur. Une qualification anonyme est refusée — l'intégration enregistre une décision humaine, elle n'en produit aucune.
L'API est conçue server-to-server : une clé dans du code livré au navigateur est une clé publiée. Faites porter la clé par un proxy serveur, jamais par la page.
Des clés scopées, montrées une fois, révocables tout de suite.
- Format. Chaque clé commence par
vgk_et s'envoie enAuthorization: Bearer vgk_…. Le secret n'est montré qu'à la création : seul son empreinte SHA-256 est conservée chez nous. - Scopes. Une clé ne porte que les portées cochées à sa création : les générales
lecture(défaut) etecriture, ou des portées fines, ressource par ressource —clients:,dossiers:,pieces:,criblage:(enlectureouecriture),origine-fonds:lecture,journal:lecture.
L'héritage est suffixal, et il ne va que dans un sens :lecturecouvre tout*:lecture,ecrituretout*:ecriture; une portée fine, elle, n'ouvre rien au-delà d'elle-même.
Six portées sortent de ce couple et ne s'héritent donc d'aucune clé générale :alertes:qualifier,embed:emettre,adversemedia:consulter,conformite:consulter,dispositif:consulter,integrite:consulter. Elles se délèguent une par une, à la création de la clé.
Une clé sans le scope requis reçoit un 403 — voir #scopes. - Expiration. Un an après la création (visible dans la console du cabinet). Une clé échue reçoit un 401 explicite — voir #cle-expiree.
Une clé peut aussi cesser d'être acceptée avant cette date, quand l'API ne fait plus partie de l'abonnement du cabinet. L'arrêt tombe alors 30 jours au moins après un premier courriel au cabinet, à la date que donne un second, envoyé 7 jours au moins avant. Dès qu'elle est fixée, cette date se lit dans la console du cabinet (champcoupure_ledeGET /connect/cles), et chaque réponse à un appel accepté l'annonce dans l'en-têteSunsetjusqu'à ce qu'elle tombe. Passé cette date, l'appel reçoit le 401 de #authentification, avec"motif": "fin_option_api"; si l'API revient dans l'abonnement, la clé est de nouveau acceptée, jusqu'à sa propre échéance.
Une clé révoquée ne revient pas avec l'API. Et quand l'abonnement entier est suspendu ou prend fin, les clés cessent d'être acceptées sur-le-champ, sans ce délai. - Débit. 60 requêtes/minute par clé (surchargeable par clé). Un 429 porte
Retry-Afteret les en-têtesX-RateLimit-Limit / -Remaining / -Reset. - Révocation. Immédiate, depuis la console. La résolution se fait par empreinte : aucune énumération de clés n'est possible.
Trois appels curl, et vous avez fait le tour.
# Santé + authentification de la clé
curl -s https://vigilae.org/api/v1/sante \
-H "Authorization: Bearer vgk_votre_cle"
# Journal de surveillance, depuis le début, par pages de 100
curl -s "https://vigilae.org/api/v1/evenements?depuisSeq=0&limite=100" \
-H "Authorization: Bearer vgk_votre_cle"
# Qualifier l'événement 42 (scope écriture)
curl -s -X POST https://vigilae.org/api/v1/evenements/42/qualifier \
-H "Authorization: Bearer vgk_votre_cle" \
-H "Content-Type: application/json" \
-d '{"statut":"qualifie","disposition":"traite"}'La pagination, en une règle
Passez depuisSeq (0 au premier appel) et limite (écrêtée à 500) : la réponse est triée par seq croissant et rend prochainSeq, à repasser tel quel à l'appel suivant. Une page plus courte que limite signifie la fin du journal — prochainSeq reste alors stable et sert de curseur de polling. Sans depuisSeq, vous obtenez la vue « plus récents d'abord », bornée à 500.
Le portefeuille s'interroge sur /api/v1/portefeuille/agregats — des compteurs, une réponse, pas de pagination.
Signés, horodatés, livrés au moins une fois.
Plutôt que d'interroger le journal, recevez-le : Vigilae livre les événements à votre URL, dans l'ordre, par lots de 100 au maximum. La livraison est at-least-once — le curseur n'avance que sur votre 2xx, lot par lot — et l'idempotence se fait par seq : traitez chaque séquence une seule fois (#idempotence).
Vérifier la signature
Chaque livraison porte l'en-tête x-vigilae-signature: t=<unix>,v1=HMAC-SHA256(secret, t + "." + corps). Recalculez v1 depuis le corps brut reçu, et rejetez tout horodatage t au-delà de 5 minutes : c'est l'anti-rejeu. Pendant une rotation de secret, l'en-tête porte un v1 par secret encore valide (chevauchement de 24 h) : acceptez si l'un correspond.
// Node — vérification de signature (aucune dépendance)
import { createHmac, timingSafeEqual } from 'node:crypto';
export function signatureValide(entete, corpsBrut, secrets, toleranceMs = 5 * 60 * 1000) {
const t = Number((/(?:^|,)t=(\d+)/.exec(entete) || [])[1]);
if (!t || Math.abs(Date.now() - t * 1000) > toleranceMs) return false; // anti-rejeu
const recus = [...entete.matchAll(/v1=([0-9a-f]{64})/g)].map((m) => m[1]);
return [].concat(secrets).some((secret) => {
const attendu = createHmac('sha256', secret).update(t + '.' + corpsBrut).digest('hex');
return recus.some((r) => r.length === attendu.length
&& timingSafeEqual(Buffer.from(r), Buffer.from(attendu)));
});
}# Python — même vérification
import hmac, hashlib, re, time
def signature_valide(entete: str, corps_brut: bytes, secrets, tolerance_s: int = 300) -> bool:
m = re.search(r"(?:^|,)t=(\d+)", entete)
if not m or abs(time.time() - int(m.group(1))) > tolerance_s:
return False # anti-rejeu
recus = re.findall(r"v1=([0-9a-f]{64})", entete)
for secret in secrets:
attendu = hmac.new(secret.encode(), m.group(1).encode() + b"." + corps_brut,
hashlib.sha256).hexdigest()
if any(hmac.compare_digest(attendu, r) for r in recus):
return True
return FalseTransitoire : l'ancien format sha256=HMAC(corps) est encore émis sur x-vigilae-signature-legacy pour les récepteurs déjà en place ; son retrait est prévu avec les webhooks v2. Les nouveaux récepteurs vérifient le schéma horodaté ci-dessus, pas l'ancien.
La rotation se déclenche depuis la console (POST /connect/webhook/rotation côté application) : pendant 24 h, l'ancien secret signe encore — le temps de déployer le nouveau chez vous sans fenêtre d'échec.
L'état de vigilance, dans vos écrans.
Votre logiciel crée déjà les dossiers par l'API ; les widgets lui donnent la surface qui manquait : montrer l'état d'un dossier dans votre fiche client, sans reconstruire une interface de conformité. Le flux tient en trois temps — votre serveur forge un jeton court, l'URL rendue se pose dans une iframe, le widget affiche. Il ne fait qu'afficher : aucune décision, aucune qualification, aucune écriture ne passe par un widget. Le clic ouvre l'application Vigilae, où un humain identifié agit.
1. Forger le jeton — côté serveur, jamais dans la page
La clé vgk_ ne touche jamais le navigateur. Votre serveur appelle le mint avec le scope dédié embed:emettre (coché à la création de la clé, jamais hérité) et seul le jeton court descend vers la page : quinze minutes par défaut, borné 5-60, lié à un dossier, un composant, une origine déclarée. Il voyage dans le fragment de l'URL — il n'atteint ni nos logs, ni les vôtres.
# Un jeton pour le Compliance Button, libellé choisi dans le catalogue
curl -s -X POST https://vigilae.org/api/v1/embed/jetons \
-H "Authorization: Bearer vgk_votre_cle" \
-H "Content-Type: application/json" \
-d '{"dossier_id":"D123","composant":"bouton",
"audience":"https://votre-logiciel.exemple","libelle":"controler_client"}'
# 201 -> { "jeton": "…", "expire_le": "…",
# "url": "https://vigilae.org/embarque/bouton?c=…#…" }2. Poser l'iframe — le lanceur, en dix lignes
Le lanceur vigilae-embed.js crée l'iframe sandboxée, fait le handshake et suit la taille du widget (bornée — l'iframe ne dicte jamais sa hauteur à votre page). Côté CSP, votre page n'ouvre qu'une directive : frame-src https://vigilae.org. Aucun script inline exigé, aucune dépendance. Une iframe écrite à la main fonctionne tout aussi bien.
<script src="https://vigilae.org/embarque/vigilae-embed.js" defer></script>
<div id="vigilance"></div>// Dans votre script : l'url vient de VOTRE serveur, qui a appelé le mint
var widget = VigilaeEmbed.monter({
conteneur: 'vigilance',
url: urlRendueParLeMint,
renouvelerJeton: function () { // à l'expiration : re-forger côté serveur
return fetch('/mon-serveur/jeton-vigilae').then(function (r) { return r.text(); });
},
});
// widget.rafraichir(jeton) pousse un jeton frais ; widget.demonter() retire tout.3. Le Compliance Button : libellé au choix, état non négociable
Le libellé du bouton se choisit dans un catalogue fermé — jamais de texte libre dans une surface qui parle de vigilance :
controler_client— « Contrôler ce client » (défaut)lancer_vigilance— « Lancer la vigilance »verifier_be— « Vérifier les bénéficiaires effectifs »preparer_dossier— « Préparer le dossier LCB-FT »
La pastille, elle, rend un état calculé par Vigilae : a_jour (vert — criblage et cotation à jour), a_revoir (ambre — criblage ou cotation périmés, ou source en échec au dernier passage), alerte (rouge — au moins une alerte à traiter), inconnu (gris — dossier jamais criblé). Le libellé se configure, jamais la sémantique : la couleur est liée à l'état dans le shell même, qui ignore toute tentative de la surcharger par postMessage. Un dossier en alerte s'affiche en alerte, quel que soit l'habillage.
Le contrat postMessage, en entier
Le widget émet vigilae:pret, vigilae:taille (auto-ajustement), vigilae:expire (re-forgez côté serveur, puis postez vigilae:jeton) et vigilae:ouvrir. Jamais un nom, un risque ou une alerte dans un message : votre logiciel a déjà tout par l'API server-to-server, et ce canal n'en sera pas un second.
frame-ancestors du shell. L'administrateur la déclare lui-même dans Vigilae, écran « Intégrations & sécurité » : dix origines https:// au plus, hôte explicite, jamais de joker ; les composants qu'il active ; et une révocation en masse qui tue d'un geste tous les jetons déjà émis. http://localhost n'est admis que sur un cabinet bac à sable — écrivez à contact@vigilae.org (objet « Widgets embarqués ») pour en ouvrir un le temps de votre intégration. Aucune origine déclarée : le mint refuse en 400. Instance dont le stockage des origines n'est pas branché : 501 explicite — voir #embed-indisponible.Sandbox : sur demande.
Les erreurs de l'API ont chacune une adresse stable sur la référence des erreurs : #authentification, #scopes, #cloisonnement, #debit, #signature… Les réponses d'erreur de l'API renverront vers ces ancres.
L'API n'expose ni déclaration de soupçon ni verbatim client, par construction (art. L.561-18 CMF). Aucune décision de vigilance n'est prise par l'API : elle expose et qualifie des événements, le professionnel décide.