An API you can read before you sign.
The specification is public, the webhook signature can be verified in ten lines on your side. This page documents what the API exposes today — nothing more, and that is deliberate: what isn't shipped isn't documented.
- OpenAPI specification readable without a key or an account
- Never a suspicious activity report, never client verbatim
- Signed webhooks, verifiable outside Vigilae
Served by the API itself: the contract you read is the one that runs.
The whole journey, and two exclusions that will not budge.
Your software can run a file end to end: create the client, open the file, upload the documents, launch the screening, declare the beneficial owners, read the risk rating and retrieve the evidence file. Two things will never pass through it, and it is not a plan limitation: no suspicious transaction report, and no verbatim from your client. The first is an act the law reserves for the person who files it; the second has no business in an integration pipeline.
The file, end to end
Clients, files, supporting documents, screening, beneficial owners, risk rating, evidence file, integrity check. Every resource has its scope: your key opens only what you gave it.
Journal and aggregates
The firm's pKYC events — expired document, change of beneficial owner, re-screening to review — and portfolio counters for your dashboard. Always a bounded response, a stable cursor.
What you write carries a name
Upload, screen, qualify: every write demands its scope and records the operator. An anonymous qualification is refused — the integration records a human decision, it never makes one.
The API is designed server-to-server: a key in code shipped to the browser is a published key. Have a server-side proxy hold the key, never the page.
Scoped keys, shown once, revocable immediately.
- Format. Every key starts with
vgk_and is sent asAuthorization: Bearer vgk_…. The secret is shown only at creation: we keep nothing but its SHA-256 digest. - Scopes. A key carries only the scopes ticked when it is created: the general
lecture(read, the default) andecriture(write), or fine-grained ones, resource by resource —clients:,dossiers:,pieces:,criblage:(inlectureorecriture),origine-fonds:lecture,journal:lecture.
Inheritance is suffix-based, and it runs one way only:lecturecovers every*:lecture,ecritureevery*:ecriture; a fine-grained scope opens nothing beyond itself.
Six scopes fall outside that pair and are therefore inherited from no general key:alertes:qualifier,embed:emettre,adversemedia:consulter,conformite:consulter,dispositif:consulter,integrite:consulter. Each is delegated on its own, when the key is created.
A key without the required scope receives a 403 — see #scopes. - Expiry. One year after creation (visible in the firm's console). An expired key receives an explicit 401 — see #cle-expiree.
A key can also stop being accepted before that date, when the API is no longer part of the firm's subscription. The stop then falls at least 30 days after a first email to the firm, on the date given by a second email, sent at least 7 days ahead. Once set, that date can be read in the firm's console (thecoupure_lefield ofGET /connect/cles), and every response to an accepted call announces it in theSunsetheader until it falls. After that date, calls get the 401 described under #authentification, with"motif": "fin_option_api"; if the API comes back into the subscription, the key is accepted again, until its own expiry date.
A revoked key does not come back with the API. And when the whole subscription is suspended or ends, keys stop being accepted immediately, without this delay. - Rate. 60 requests/minute per key (overridable per key). A 429 carries
Retry-Afterand theX-RateLimit-Limit / -Remaining / -Resetheaders. - Revocation. Immediate, from the console. Resolution is by digest: keys cannot be enumerated.
Three curl calls and you have seen it all.
# Health + key authentication
curl -s https://vigilae.org/api/v1/sante \
-H "Authorization: Bearer vgk_your_key"
# Monitoring journal, from the beginning, in pages of 100
curl -s "https://vigilae.org/api/v1/evenements?depuisSeq=0&limite=100" \
-H "Authorization: Bearer vgk_your_key"
# Qualify event 42 (write scope)
curl -s -X POST https://vigilae.org/api/v1/evenements/42/qualifier \
-H "Authorization: Bearer vgk_your_key" \
-H "Content-Type: application/json" \
-d '{"statut":"qualifie","disposition":"traite"}'Pagination, in one rule
Pass depuisSeq (0 on the first call) and limite (capped at 500): the response is sorted by ascending seq and returns prochainSeq, to pass back as is on the next call. A page shorter than limite means the end of the journal — prochainSeq then stays stable and serves as a polling cursor. Without depuisSeq, you get the "most recent first" view, bounded to 500.
The portfolio is queried on /api/v1/portefeuille/agregats — counters, one response, no pagination.
Signed, timestamped, delivered at least once.
Rather than polling the journal, receive it: Vigilae delivers events to your URL, in order, in batches of at most 100. Delivery is at-least-once — the cursor only advances on your 2xx, batch by batch — and idempotency is by seq: process each sequence exactly once (#idempotence).
Verifying the signature
Each delivery carries the header x-vigilae-signature: t=<unix>,v1=HMAC-SHA256(secret, t + "." + body). Recompute v1 from the raw body received, and reject any timestamp t beyond 5 min: that is the anti-replay. During a secret rotation the header carries one v1 per still-valid secret (24 h overlap): accept if any of them matches.
// Node — signature verification (no dependency)
import { createHmac, timingSafeEqual } from 'node:crypto';
export function signatureValid(header, rawBody, secrets, toleranceMs = 5 * 60 * 1000) {
const t = Number((/(?:^|,)t=(\d+)/.exec(header) || [])[1]);
if (!t || Math.abs(Date.now() - t * 1000) > toleranceMs) return false; // anti-replay
const received = [...header.matchAll(/v1=([0-9a-f]{64})/g)].map((m) => m[1]);
return [].concat(secrets).some((secret) => {
const expected = createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
return received.some((r) => r.length === expected.length
&& timingSafeEqual(Buffer.from(r), Buffer.from(expected)));
});
}# Python — same verification
import hmac, hashlib, re, time
def signature_valid(header: str, raw_body: bytes, secrets, tolerance_s: int = 300) -> bool:
m = re.search(r"(?:^|,)t=(\d+)", header)
if not m or abs(time.time() - int(m.group(1))) > tolerance_s:
return False # anti-replay
received = re.findall(r"v1=([0-9a-f]{64})", header)
for secret in secrets:
expected = hmac.new(secret.encode(), m.group(1).encode() + b"." + raw_body,
hashlib.sha256).hexdigest()
if any(hmac.compare_digest(expected, r) for r in received):
return True
return FalseTransitional: the old sha256=HMAC(body) format is still emitted on x-vigilae-signature-legacy for receivers already in place; its removal is planned with webhooks v2. New receivers verify the timestamped scheme above, not the old one.
Rotation is triggered from the console (POST /connect/webhook/rotation on the application side): for 24 h the old secret still signs — time to deploy the new one on your side without a failure window.
Vigilance status, inside your screens.
Your software already creates the files through the API; widgets give it the surface that was missing: showing a file's vigilance status inside your client record, without rebuilding a compliance UI. The flow takes three steps — your server mints a short-lived token, the returned URL goes into an iframe, the widget displays. Displaying is all it does: no decision, no qualification, no write ever goes through a widget. Clicking opens the Vigilae application, where an identified human acts.
1. Mint the token — server-side, never in the page
The vgk_ key never touches the browser. Your server calls the mint with the dedicated embed:emettre scope (ticked at key creation, never inherited) and only the short token goes down to the page: fifteen minutes by default, bounded 5-60, tied to one file, one component, one declared origin. It travels in the URL fragment — it reaches neither our logs nor yours.
# A token for the Compliance Button, label picked from the catalogue
curl -s -X POST https://vigilae.org/api/v1/embed/jetons \
-H "Authorization: Bearer vgk_your_key" \
-H "Content-Type: application/json" \
-d '{"dossier_id":"D123","composant":"bouton",
"audience":"https://your-software.example","libelle":"controler_client"}'
# 201 -> { "jeton": "…", "expire_le": "…",
# "url": "https://vigilae.org/embarque/bouton?c=…#…" }2. Place the iframe — the launcher, in ten lines
The vigilae-embed.js launcher creates the sandboxed iframe, performs the handshake and follows the widget's size (bounded — the iframe never dictates its height to your page). CSP-wise, your page opens a single directive: frame-src https://vigilae.org. No inline script required, no dependency. A hand-written iframe works just as well.
<script src="https://vigilae.org/embarque/vigilae-embed.js" defer></script>
<div id="vigilance"></div>// In your script: the url comes from YOUR server, which called the mint
var widget = VigilaeEmbed.monter({
conteneur: 'vigilance',
url: urlReturnedByTheMint,
renouvelerJeton: function () { // on expiry: re-mint server-side
return fetch('/my-server/vigilae-token').then(function (r) { return r.text(); });
},
});
// widget.rafraichir(token) pushes a fresh token; widget.demonter() removes everything.3. The Compliance Button: your label, our state
The button label is picked from a closed catalogue — never free text on a surface that speaks about vigilance:
controler_client— "Check this client" (default)lancer_vigilance— "Start due diligence"verifier_be— "Check beneficial owners"preparer_dossier— "Prepare the AML file"
The status dot, however, renders a state computed by Vigilae: a_jour (green — screening and rating up to date), a_revoir (amber — screening or rating outdated, or the source failed on the last run), alerte (red — at least one alert to review), inconnu (grey — file never screened). The label is configurable, the semantics never are: the colour is bound to the state inside the shell itself, which ignores any postMessage attempt to override it. A file in alert shows an alert, whatever the styling.
The postMessage contract, in full
The widget emits vigilae:pret, vigilae:taille (auto-resize), vigilae:expire (re-mint server-side, then post vigilae:jeton) and vigilae:ouvrir. Never a name, a risk or an alert inside a message: your software already has everything through the server-to-server API, and this channel will not become a second one.
frame-ancestors CSP. The administrator declares it directly in Vigilae, on the "Integrations & security" screen: ten https:// origins at most, explicit host, never a wildcard; the components they enable; and a mass revocation that kills every token already issued in one gesture. http://localhost is only allowed on a sandbox firm — write to contact@vigilae.org (subject "Embedded widgets") to have one opened for the duration of your integration. No origin declared: the mint refuses with a 400. An instance whose origin store is not wired: an explicit 501 — see #embed-indisponible.Sandbox: on request.
Each API error class has a stable address on the error reference: #authentification, #scopes, #cloisonnement, #debit, #signature… The API's error responses will point to these anchors.
The API exposes neither suspicious activity reports nor client verbatim, by construction (art. L.561-18 of the French CMF). No vigilance decision is taken by the API: it exposes and qualifies events; the professional decides.