Connect API — reference

Every error, explained at a stable address.

This page is an operations reference, not a pitch. Every error class of the Connect API has a permanent anchor here; the API's error responses will point to these addresses. You get the probable cause and the fix — in that order. Back to the developer documentation.

#authentification 401

Key missing, malformed or unknown

The API authenticates exclusively through the Authorization: Bearer vgk_… header — never through a session cookie. A revoked key becomes unknown the instant it is revoked.

The same 401 carries "motif": "fin_option_api" when the key has been stopped because the API is no longer part of the firm's subscription. The date of that stop was given in an email to the firm's administrators or, failing that, to its billing address, then announced in the Sunset header of every response to an accepted call (timeline under #cle-expiree). Without a motif, it can also come from a suspended subscription: suspension cuts the keys off immediately.

The fix. Check that the header actually leaves (proxies sometimes strip it), that the key starts with vgk_ and that it has not been revoked from the firm's console. Since the secret is shown only at creation, a lost key is replaced, not recovered. With "motif": "fin_option_api", neither the header nor the key is at fault: the firm's administrator writes to us at contact@vigilae.org to have the API added back to the subscription, and the same key is accepted again as soon as that is done, until its own expiry date.

#cle-expiree 401

Expired key

Every key expires one year after creation — the expire_le date is visible in the firm's console from day one. The expiry 401 is explicit: it says the key existed and is no longer valid.

An API 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, on the date given by a second email, sent at least 7 days ahead; both go to the firm's administrators or, failing that, to its billing address. Once set, that date can be read in the firm's console (the coupure_le field of GET /connect/cles), and every response to an accepted call announces it in the Sunset header 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.

The fix. Create a new key, switch your calls over, revoke the old one. For an API key, the API still has to be part of the firm's subscription: without it, the console refuses to create one. Plan this replacement in your operations rather than discovering it.

#scopes 403

Insufficient scope

Keys carry the scopes ticked when they are created: the general lecture (read, the default) and ecriture (write), or fine-grained ones per resource (clients:lecture, pieces:ecriture, origine-fonds:lecture, journal:lecture…).

Inheritance is suffix-based, one way only: lecture covers every *:lecture, ecriture every *:ecriture — never the reverse; a fine-grained scope opens only itself.

Six scopes fall outside that pair and are inherited from no general key: alertes:qualifier, embed:emettre, adversemedia:consulter, conformite:consulter, dispositif:consulter, integrite:consulter. Qualifying an event, issuing an embed token, reading a file's adverse media or a conduct score never follows from a general read right: each is ticked on its own.

The fix. Create a key carrying the needed scope — and only that one. Least privilege is deliberate: an integration that only reads has no reason to hold a write key, and a broad key will never make up for a scope that was not delegated explicitly.

#cloisonnement 404

Not found — or another firm's

A 404 means the resource does not exist or belongs to another firm. The API never distinguishes the two cases: distinguishing would reveal what exists elsewhere. Isolation is enforced at the database level, not just in application code.

The fix. Check the seq against the journal of the firm the key belongs to (GET /api/v1/evenements). If you integrate several firms, each firm has its own keys: a seq does not travel between firms.

#unicite 409

This account already exists

A POST /scim/v2/Users on a userName already provisioned for this firm returns 409 with scimType: uniqueness. Uniqueness is held by a database constraint, not by a cache: two simultaneous creations cannot produce two accounts.

The fix. Treat the 409 as an idempotent success, not as an error: read the account back with ?filter=userName eq "…" and carry on with the identifier returned. A connector that retries in a loop on a 409 will never create anything more.

#compte-hors-portee 403

This account is out of the directory's reach

A directory can neither demote nor deactivate an account carrying firm administration powers: the request is refused with scimType: mutability and logged. Without that limit, a connector adopting the compliance officer's address would lock the firm out to the very person answerable for it.

The fix. Remove that person from the synchronised scope, or change their role from within Vigilae. The refusal covers this account only: the rest of the batch is applied normally.

#debit 429

Rate exceeded

60 requests per minute per key, by default (overridable per key). The response carries Retry-After and the X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset headers.

The fix. Honour Retry-After — no immediate retry. Hitting the limit while polling is almost always the sign of a full re-scan: the depuisSeq cursor never re-reads what has already been read.

#signature webhook

The signature does not verify on your side

Your receiver must recompute v1 = HMAC-SHA256(secret, t + "." + body) from the raw body received, and reject any t older than 5 min. The classic failures: the body re-serialised before verification (reformatted JSON no longer has the same bytes), a clock drifting beyond the tolerance, and an ignored rotation — for 24 h the header carries one v1 per still-valid secret, and you must accept if any matches.

The fix. Verify against the raw bytes, sync your clock (NTP), test your verification during a rotation. The Node and Python snippets in the documentation do exactly this — copy them rather than rewriting them.

#idempotence webhook

An event arrives twice

Delivery is at-least-once, in batches of at most 100, in journal order; the cursor only advances on your 2xx, batch by batch. A failure after reception but before the cursor advances causes a redelivery — that is the contract, not a defect.

The fix. The seq is your idempotency key: process each sequence exactly once, and only answer 2xx after persisting. An early 2xx followed by a crash is the only way to lose an event.

#validation 400

Parameter outside the schema

An unknown status, a disposition outside the list (planifie, traite, ecarte), a non-integer limite: the response names the offending field, and nothing else — error messages never carry file content.

The fix. The OpenAPI specification is the source: the enumerations and bounds it declares are the ones the server enforces, since it is served by it.

#nationalite-trop-longue 400

Field value too long: Nationality

The response names the offending field, and nothing else — error messages never carry file content.

The fix. The OpenAPI specification is the source: the enumerations and bounds it declares are the ones the server enforces, since it is served by it.

#scope-manquant 403

Insufficient scope (resource API)

The resource endpoints (/clients, /dossiers, documents, screening, alerts) require a fine-grained scope: clients:*, dossiers:*, pieces:*, criblage:* or the dedicated alertes:qualifier scope. A legacy lecture key grants every *:lecture scope and ecriture every *:ecriture — but alertes:qualifier is never inherited: qualifying an alert is a responsible officer's act, delegated by ticking that scope when the key is created.

The fix. Create a key carrying exactly the scopes your integration needs — and only those. The response's detail names the missing scope.

#ressource-introuvable 404

Resource not found — or another firm's

Same rule as #cloisonnement, applied to resources: a client, a file, a document or an alert that does not exist or that belongs to another firm gets the same 404. The API never tells the two cases apart.

The fix. Check the identifier against the listings of the firm the key belongs to. An identifier never travels from one firm to another.

#idempotency-key-requis 400

Idempotency-Key header missing

Every creation or triggering POST (client, file, document, screening, beneficial owners) requires the Idempotency-Key header: a stable string of at most 200 characters, one per operation. Replaying the same key with the same body returns the same response, with no effect — your protection against duplicates on a network incident.

The fix. Generate the key before the first attempt (one UUID per business operation) and reuse it unchanged for every retry of that same operation.

#idempotency-key-reutilisee 422

Idempotency key reused for another request

This Idempotency-Key has already served another request within the last 24 hours: a different body, or the same body on another route or another file. The key is tied to ONE request — method, path, resource and body — not to the body alone: that is what stops a key derived from a file's sha256, posted to two files, from replaying the first one's response. The key replays a response, it never overwrites one: reusing it for a new operation is almost always the sign of a key derived from a counter or a truncated date.

The fix. A new key for every new operation. Never derive the key from data that repeats across operations.

#idempotency-key-en-cours 409

Another attempt under this key is still running

Two requests carrying the same Idempotency-Key arrived together: the first one runs, the second is refused before any effect. That refusal is what prevents the duplicate. The common case: a proxy returns 502 or 504 while the server is still working, and your client replays. The Retry-After header says how many seconds to wait before replaying.

The fix. Replay the same request, unchanged, with the same key, once the announced delay has passed: you will get the first attempt's response. Never mint a fresh key to "unblock" — that would create the very duplicate the key prevents.

#quota-essai-atteint 409

Test environment: quota reached

A test environment keeps fixed limits: 200 client records, 50 due-diligence files and 40 documents per file. A creation or upload that would exceed any of them is refused before anything is written, and the refusal says which: the ressource, quota and courant members carry the resource, the ceiling and the count at the time of the refusal. Two simultaneous creations competing for the last slot let only one through. Editing an existing record or file consumes nothing.

The fix. These quotas are not negotiable, and replaying the request returns the same refusal. Reset the test environment from your account — it starts again from the demo data set — or subscribe: these quotas apply to test environments only.

#dossier-scelle 409

Sealed file: the register is frozen

Sealing freezes the file's integrity chain — that is its evidential value. A sealed file can no longer be edited, no longer receives documents and is no longer re-screened through this channel. Only alert qualification remains possible: it is written off-chain, the sealed head never moves.

The fix. Nothing to "correct": this is deliberate immutability. If data must change, that is a new due-diligence operation in the application, not a mutation of the sealed file.

#dossier-clos 409

File closed: the business relationship has ended

The relationship was closed in the application: the file no longer accepts writes through the API — no edit, no document, no screening, no beneficial owners. Reads stay open: a closed file can be consulted and exported for the whole retention period.

The fix. Reopening the relationship is a human, logged act in the application; the API does not do it for you. If the operation is new, open a new file instead of reviving the old one.

#dossier-purge 409

File purged: the data has been erased

The retention deadline has passed and the file's data has been erased. Only the audit trace remains, attesting that a file existed and was purged on the stated date; it accepts no further write.

The fix. Nothing to correct and nothing to replay: the erasure is final and deliberate. A purged file identifier kept in your systems is history, never a write target.

#format-piece 415

Document format not accepted

Supporting documents accept PDF, JPG, JPEG, PNG and WEBP — the same formats as the application. The check is on the extension of the mandatory filename: a document without a valid file name is refused, whatever its content.

The fix. Convert before sending (a TIFF or HEIC scan converts to PDF or JPEG) and send the content as standard base64 in contenu, with a type among the stable a_doc_* codes.

#piece-trop-volumineuse 413

Document over 6 MB

The limit applies to the decoded file (6 MB), the same as in the application and the collection portal. Since base64 adds a third of encoding overhead, the request body accepts up to 9 MB.

The fix. Compress the document (a colour 300 dpi scanned PDF almost always drops under the limit in greyscale) rather than splitting it.

#source-criblage 502

Screening source unavailable

None of the file's entities could be checked against the lists: the source (official-list index or provider) did not answer. The API refuses to conclude — a "clean" report on top of a dead source would be the worst possible false negative. The failure is recorded on the file (erreurSource). A partial outage does not produce this 502: the 200 response keeps the matches of the entities that did answer and names the failed ones in erreurSourceEntites.

The fix. Replay the same call later, same Idempotency-Key: only success responses are memorised by idempotency — replaying after an error runs a real screening.

#embed-indisponible 501

Embedded widgets not enabled on this instance

The mint POST /api/v1/embed/jetons relies on the origins declared for your firm — that list is what drives the embedded shell's frame-ancestors CSP. This 501 says the instance you called does not carry that store: the embed surface is inert there by construction, and says so rather than minting a token no origin could ever display. Not to be confused with the 400 returned to a firm whose origins are not declared yet.

The fix. This 501 is about the instance, not your request. On vigilae.org, your firm's administrator declares the origins themselves, on the "Integrations & security" screen (the box in the widgets section says where); then replay the same call. Nothing to change in your code: the request was fine, the instance was not ready.

#stockage-indisponible 501

Surface unavailable on this deployment

Two baseline reads — GET /api/v1/dossiers/{id}/origine-fonds and GET /api/v1/dossiers/{id}/exigences — rely on stores that not every instance carries. When one is missing, the API says so instead of serving an empty list: "nothing to report" and "nothing was read" are opposite facts, and confusing them would make a file nobody looked at pass for a file with no reservations. This 501 is about the instance you called, never about your request or the file.

The fix. Do not replay in a loop: nothing will change until the instance carries that store. Treat this code as "data unavailable here", and above all do not count it as a zero in your dashboards — that is how a missing measurement turns into a bad figure. On vigilae.org both reads are served; on a dedicated deployment, upgrading the store is your operator's call.

This page will evolve with the API; the existing anchors, however, never change address — you can store them in your operations logs.