Aller au contenu
Justilio

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.

Un environnement de test, avec vos clés et vos réglages, est ouvert pendant le pilote.

Le parcours d’un dossier

  1. 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.

  2. WEBHOOKcase.analyzed

    Être prévenu

    Justilio appelle votre serveur dès que la pré-analyse est publiée. L’événement est signé.

  3. 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.

  4. 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.

En-têtes
Authorization: Bearer jst_<prefix>_<secret>
Idempotency-Key: 7b0e2c64-6f0a-4d6b-9a1e-3c5d8f2e4a10
X-Request-Id: req_20261106_0042

Cré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é.

Requête
curl "$JUSTILIO_API_URL/v1/cases" \
  -H "Authorization: Bearer $JUSTILIO_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  --data @case.json
Corps (case.json), pièce tronquée
{
  "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"
  }
}
Réponse 202
{
  "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 :

  1. lisez le corps brut, avant tout décodage JSON ;
  2. calculez le HMAC-SHA256 de l’horodatage t, d’un point et du corps, avec votre secret de webhook, puis comparez-le à v1 en temps constant ;
  3. 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.

Requête reçue
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"
  }
}
Vérifier la signature en Node.js
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"));
}
En Python
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))
Vecteur de test
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"}}

Avec ce secret, ce corps exact et cet en-tête, la vérification réussit si l’horloge est figée à l’horodatage t.

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.

Voici le dossier suspect de l’accueil, tel que le rend l’API.

Réponse 200
{
  "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

CodeLibelléSens
consistentcohérentTout concorde.
incompleteincompletIl manque une information : client_question donne la question à poser.
inconsistentincohérentLa pièce ne correspond pas au virement.
suspectsuspectSigne 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.

Corps
{
  "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é."
}
Réponse 200
{
  "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.

HTTPCodeQuand
400idempotency_key_requiredEn-tête Idempotency-Key absent ou trop long.
401unauthorizedClé d’API absente, invalide ou révoquée.
404case_not_foundAucun dossier de votre établissement ne porte cet identifiant.
409transfer_already_receivedCe virement a déjà son dossier : la réponse donne son case_id.
409stale_analysisLa décision porte sur une analyse qui n’est plus la dernière publiée.
409analysis_runningUne nouvelle analyse est en cours : décidez sur son résultat.
409request_in_progressLa même requête est encore en cours.
409case_closedLe dossier est en purge ou purgé.
413payload_too_largeCorps trop gros : 10 Mo de pièce au plus.
422invalid_requestRequête invalide : details désigne les champs en cause.
422document_rejectedPièce refusée par les contrôles de sécurité, ou mal encodée (invalid_document). Rien n’est enregistré.
422idempotency_key_reusedClé d’idempotence déjà utilisée pour une autre requête.
429too_many_receptionsTrop de pièces en réception : réessayez après Retry-After.
503reception_unavailableIndisponibilité 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.