Développeurs
Branchez Justilio en quatre appels.
Une API REST en JSON : votre back-office envoie le virement et la pièce, reçoit la pré-analyse par un webhook signé, puis enregistre la décision de l’analyste. Cette page explique l’intégration ; la référence, en anglais, détaille chaque champ.
Le parcours d’un dossier
POST/v1/cases
Créer le dossier
Le virement, le client, la pièce en base 64 et, si vous les avez, les virements passés. Réponse 202 dès que la pièce est reçue, contrôlée et stockée chiffrée.
WEBHOOKcase.analyzed
Être prévenu
Justilio appelle votre serveur dès que la pré-analyse est publiée. L’événement est signé.
GET/v1/cases/{case_id}
Lire la pré-analyse
Statut, constats, valeurs lues sur la pièce et, pour un dossier incomplet, la question à poser au client.
POST/v1/cases/{case_id}/decision
Enregistrer la décision
Validé, refusé, pièce demandée ou réouvert. La décision nourrit l’historique des dossiers suivants.
Deux appels complètent le parcours : POST /v1/cases/{case_id}/documents ajoute une pièce, par exemple après la réponse du client, et POST /v1/cases/{case_id}/reanalyze relance l’analyse, par exemple après l’indisponibilité d’un registre.
Authentification et idempotence
Chaque établissement reçoit ses clés d’API. Envoyez la clé dans l’en-tête Authorization ; elle ne s’affiche qu’une fois, et Justilio n’en garde qu’une empreinte. Appelez l’API depuis vos serveurs seulement.
Chaque écriture porte un en-tête Idempotency-Key unique, par exemple un UUID. Renvoyée avec la même requête, elle rend la réponse d’origine sans rien créer ; avec une autre requête, elle est refusée. transfer_ref ajoute une seconde garantie : un seul dossier par virement.
X-Request-Id, facultatif, est gardé au journal des accès pour retrouver une requête.
Authorization: Bearer jst_<prefix>_<secret>
Idempotency-Key: 7b0e2c64-6f0a-4d6b-9a1e-3c5d8f2e4a10
X-Request-Id: req_20261106_0042Créer un dossier
Envoyez le virement, le client, la pièce encodée en base 64 et, si vous les avez, les virements passés du client : ils révèlent un fournisseur payé sur un nouveau compte, ou une facture déjà réglée. La réponse 202 arrive dès que la pièce est reçue, contrôlée et stockée chiffrée. Une pièce refusée par les contrôles de sécurité donne un 422, et rien n’est enregistré.
curl "$JUSTILIO_API_URL/v1/cases" \
-H "Authorization: Bearer $JUSTILIO_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
--data @case.json{
"transfer_ref": "trf_20261106_000481",
"transfer": {
"amount": "48000.00",
"currency": "EUR",
"execution_date": "2026-11-06",
"direction": "outgoing",
"counterparty_name": "Velmora Conseil",
"counterparty_iban": "FR7699999000010000048006191",
"reference": "FACT VC-2026-014",
"declared_purpose": "Mission de conseil stratégique",
"vop_result": "match"
},
"client": {
"ref": "cli_8f3a2",
"kind": "company",
"name": "Nordis Trading",
"siren": "093847218"
},
"prior_transfers": [
{
"amount": "1250.00",
"currency": "EUR",
"execution_date": "2026-10-02",
"direction": "outgoing",
"counterparty_name": "Brévalis Logistique",
"counterparty_iban": "FR7699999000010000027154877",
"reference": "FACT 2026-0917"
}
],
"document": {
"filename": "facture-VC-2026-014.pdf",
"content_base64": "JVBERi0xLjcK"
}
}{
"case_id": "4f6c2a0e-8b1d-4c3e-9a57-2d8e1b6f0c41",
"state": "received",
"revision": 1
}Recevoir le webhook signé
Dès qu’une pré-analyse est publiée, Justilio envoie un POST à l’adresse HTTPS que vous avez déclarée. Le corps ne porte que des identifiants et le statut : lisez ensuite le dossier avec GET /v1/cases/{case_id}.
Vérifiez la signature avant de vous fier à l’événement :
- lisez le corps brut, avant tout décodage JSON ;
- calculez le HMAC-SHA256 de l’horodatage
t, d’un point et du corps, avec votre secret de webhook, puis comparez-le àv1en temps constant ; - refusez une signature de plus de cinq minutes, et dédoublonnez sur
Justilio-Event-Id: un même événement peut arriver deux fois.
Répondez vite avec un code 2xx. Sinon (autre code, délai dépassé, ou redirection, jamais suivie), Justilio réessaie après 1 min, 5 min, 30 min, 2 h, 6 h et 12 h, pendant 24 heures. Le dossier reste lisible ensuite.
POST /your/webhook/endpoint
Content-Type: application/json
Justilio-Event-Id: 6a0d9e44-2c71-4b5f-a8d3-91e7f0b25c6a
Justilio-Signature: t=1794042861, v1=0a9deb684ee9bc9f15d34dd851b36f25812e4afb6ca0e24c3001c089a97030c9
User-Agent: Justilio-Webhooks/1
{
"id": "6a0d9e44-2c71-4b5f-a8d3-91e7f0b25c6a",
"type": "case.analyzed",
"created_at": "2026-11-06T09:14:21Z",
"data": {
"case_id": "4f6c2a0e-8b1d-4c3e-9a57-2d8e1b6f0c41",
"run_id": "b2d7e3f1-5a64-4e8b-8c19-0f3a9d2c7e58",
"status": "suspect",
"rules_version": "examplebank-2026-11-01"
}
}import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received (a Buffer), before any JSON parsing.
export function verifyJustilio(rawBody, header, secret, toleranceSeconds = 300) {
const match = /^t=(\d{1,12}), v1=([0-9a-f]{64})$/.exec(header ?? "");
if (!match) return false;
const timestamp = Number(match[1]);
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest();
return timingSafeEqual(expected, Buffer.from(match[2], "hex"));
}
import hashlib
import hmac
import re
import time
def verify_justilio(raw_body: bytes, header: str, secret: bytes, tolerance: int = 300) -> bool:
"""True if `Justilio-Signature` signs this exact body, less than 5 minutes ago."""
match = re.fullmatch(r"t=(\d{1,12}), v1=([0-9a-f]{64})", header or "")
if match is None:
return False
timestamp = int(match.group(1))
if abs(time.time() - timestamp) > tolerance:
return False
expected = hmac.new(secret, f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, match.group(2))
secret: whsec_example_not_a_real_secret
header: t=1794042861, v1=0a9deb684ee9bc9f15d34dd851b36f25812e4afb6ca0e24c3001c089a97030c9
body: {"id":"6a0d9e44-2c71-4b5f-a8d3-91e7f0b25c6a","type":"case.analyzed","created_at":"2026-11-06T09:14:21Z","data":{"case_id":"4f6c2a0e-8b1d-4c3e-9a57-2d8e1b6f0c41","run_id":"b2d7e3f1-5a64-4e8b-8c19-0f3a9d2c7e58","status":"suspect","rules_version":"examplebank-2026-11-01"}}Lire la pré-analyse
La réponse donne le statut, les constats avec la valeur lue (found) et la valeur attendue (expected), les valeurs lues sur la pièce, le verdict de falsification et, pour un dossier incomplet seulement, la question à poser au client. analysis reste null jusqu’à la première analyse publiée.
{
"case_id": "4f6c2a0e-8b1d-4c3e-9a57-2d8e1b6f0c41",
"transfer_ref": "trf_20261106_000481",
"state": "analyzed",
"revision": 1,
"created_at": "2026-11-06T09:14:02Z",
"analysis": {
"analysis_id": "b2d7e3f1-5a64-4e8b-8c19-0f3a9d2c7e58",
"status": "suspect",
"status_label": "suspect",
"forgery_check": "modified",
"forgery_label": "falsification détectée",
"findings": [
{
"code": "FORGERY_COVERED_TEXT",
"effect": "suspect",
"effect_label": "suspect",
"message": "Sous le total de 48 000,00 €, le PDF garde un ancien total de 4 800,00 €, masqué par un rectangle blanc.",
"document": "facture-VC-2026-014.pdf",
"field": "total_ttc",
"found": "4800.00",
"expected": null
},
{
"code": "COMMON_OFFICER",
"effect": "vigilance",
"effect_label": "vigilance",
"message": "Nordis Trading et Velmora Conseil ont un dirigeant en commun.",
"document": null,
"field": null,
"found": null,
"expected": null
},
{
"code": "COMPANY_YOUNG",
"effect": "vigilance",
"effect_label": "vigilance",
"message": "Velmora Conseil a été créée le 17/09/2026, sept semaines avant le virement.",
"document": "facture-VC-2026-014.pdf",
"field": "issuer_siren",
"found": "2026-09-17",
"expected": null
},
{
"code": "LARGE_AMOUNT_SMALL_SUPPLIER",
"effect": "vigilance",
"effect_label": "vigilance",
"message": "48 000 € pour une société sans salarié, créée il y a moins d'un an.",
"document": "facture-VC-2026-014.pdf",
"field": "total_ttc",
"found": "48000.00",
"expected": null
},
{
"code": "NEW_COUNTERPARTY",
"effect": "vigilance",
"effect_label": "vigilance",
"message": "Premier virement de ce client vers Velmora Conseil, d'après l'historique transmis.",
"document": null,
"field": null,
"found": null,
"expected": null
}
],
"document_kind": "invoice",
"extracted": {
"issuer_name": "Velmora Conseil",
"issuer_siren": "048273619",
"issuer_vat_number": "FR63048273619",
"recipient_name": "Nordis Trading",
"invoice_number": "VC-2026-014",
"invoice_date": "2026-11-03",
"total_ht": "40000.00",
"total_tva": "8000.00",
"total_ttc": "48000.00",
"currency": "EUR",
"iban": "FR7699999000010000048006191"
},
"client_question": null,
"rules_version": "examplebank-2026-11-01",
"analyzed_at": "2026-11-06T09:14:21Z"
}
}Statuts et effets
| Code | Libellé | Sens |
|---|---|---|
consistent | cohérent | Tout concorde. |
incomplete | incomplet | Il manque une information : client_question donne la question à poser. |
inconsistent | incohérent | La pièce ne correspond pas au virement. |
suspect | suspect | Signe de fraude ou risque grave. Jamais de question au client. |
Chaque constat porte un effet : suspect, inconsistent, incomplete, vigilance (un point à surveiller, sans effet seul sur le statut) ou positive. Le statut est l’effet le plus grave trouvé. Ni score ni points dans les réponses : les constats expliquent le statut.
Enregistrer la décision
L’analyste décide sur l’analyse qu’il a lue (analysis_id) : validated, refused, document_requested ou reopened. La décision rejoint l’historique : c’est elle qui permet, par exemple, de reconnaître une facture déjà payée. Une décision sur une analyse dépassée est refusée en 409 stale_analysis ; pendant une nouvelle analyse, en 409 analysis_running. La réponse donne alors published_analysis_id.
{
"analysis_id": "b2d7e3f1-5a64-4e8b-8c19-0f3a9d2c7e58",
"decision": "refused",
"analyst_ref": "analyst_042",
"comment": "Facture retouchée entre sociétés liées : transmis à la conformité."
}{
"decision_id": "e81a4c27-3f9b-4d06-b5e2-7c40a1d9f363"
}Les erreurs
Une erreur rend un code stable et un message en français : fiez-vous au code. Les réponses 429 et 503 portent un en-tête Retry-After.
| HTTP | Code | Quand |
|---|---|---|
| 400 | idempotency_key_required | En-tête Idempotency-Key absent ou trop long. |
| 401 | unauthorized | Clé d’API absente, invalide ou révoquée. |
| 404 | case_not_found | Aucun dossier de votre établissement ne porte cet identifiant. |
| 409 | transfer_already_received | Ce virement a déjà son dossier : la réponse donne son case_id. |
| 409 | stale_analysis | La décision porte sur une analyse qui n’est plus la dernière publiée. |
| 409 | analysis_running | Une nouvelle analyse est en cours : décidez sur son résultat. |
| 409 | request_in_progress | La même requête est encore en cours. |
| 409 | case_closed | Le dossier est en purge ou purgé. |
| 413 | payload_too_large | Corps trop gros : 10 Mo de pièce au plus. |
| 422 | invalid_request | Requête invalide : details désigne les champs en cause. |
| 422 | document_rejected | Pièce refusée par les contrôles de sécurité, ou mal encodée (invalid_document). Rien n’est enregistré. |
| 422 | idempotency_key_reused | Clé d’idempotence déjà utilisée pour une autre requête. |
| 429 | too_many_receptions | Trop de pièces en réception : réessayez après Retry-After. |
| 503 | reception_unavailable | Indisponibilité passagère (ou deposit_expired) : réessayez, et renvoyez la pièce si besoin. |
Limites, données et sécurité
- Une pièce par requête, 10 Mo au plus, en PDF, JPEG, PNG ou HEIC ; vos réglages peuvent restreindre ces formats.
- Jusqu’à 500 virements passés par dossier.
- Pièces chiffrées par établissement, hébergées en Europe, conservées le temps que vous réglez.
- Un numéro de sécurité sociale lu sur une pièce n’est jamais conservé.
- Les clés d’API sont gardées sous forme d’empreinte, et chaque établissement ne voit que ses dossiers.
Préparons l’intégration avec votre équipe technique.
Pour le pilote, nous ouvrons un environnement de test avec vos clés et vos réglages.