Webhooks sortants

Événements poussés par l'agrégateur vers l'URL webhook du partenaire : succès et échec d'émission.

Vue d'ensemble

Après un paiement confirmé, l'agrégateur déclenche l'émission du contrat ASKIA puis notifie le partenaire via un POST HTTP signé vers l'URL webhook configurée (configuration).

Le webhook est le mecanisme principal de notification asynchrone. La relecture de reference via GET /partner/contracts/requests/{reference} sert surtout de fallback ou de reconciliation.

Webhook entrant paiement (contexte)

Avant d'émettre le webhook sortant, l'agrégateur reçoit lui-même un webhook entrant de la passerelle de paiement :

method : string (default: POST)

POST

path : string

/api/v1/webhooks/dexpay

HeaderDescription
x-webhook-signatureHMAC SHA256 du body, clé interne de signature paiement.
Content-Typeapplication/json.

Cet endpoint est appelé uniquement par la passerelle de paiement. Il n'a pas à être appelé par les partenaires. Il est documenté ici pour clarifier l'origine du déclenchement de l'émission.

Événements sortants

Deux événements sont émis vers le partenaire :

ÉvénementQuand ?
insurance.contract.issuedPaiement OK et contrat ASKIA émis avec succès.
insurance.contract.issuance_failedPaiement OK mais émission ASKIA en échec.

Aucun événement n'est émis pour un paiement qui échoue avant l'étape d'émission (annulation, échec, etc.). Utilisez GET /api/v1/partner/contracts/requests/:reference pour suivre ces cas.

Headers envoyés au partenaire

HeaderDescription
Content-Typeapplication/json.
x-insurance-eventNom de l'événement (voir table ci-dessus).
x-insurance-referencereference corrélant le flow (identique à la création de session).
x-insurance-signatureHMAC SHA256 hex du body brut avec le webhookSecret (si secret configuré).

Réponse attendue du partenaire

  • Retourner 2xx rapidement si le message est bien recu.
  • Rejeter avec 401 si la signature est invalide.
  • Eviter tout traitement long avant la reponse HTTP; deferer le reste dans une file.

Payload — insurance.contract.issued

{
  "event": "insurance.contract.issued",
  "reference": "INS-AGG-2026-000123",
  "paymentStatus": "COMPLETED",
  "issuedAt": "2026-04-24T19:00:00.000Z",
  "data": {
    "contractId": "680a67c6d46c0b8dd2a02f2f",
    "numeroPolice": "8400550AS000033",
    "numeroFacture": "8400/2026/000006",
    "status": "ACTIVE",
    "liens": {
      "linkAttestation": "https://aas.diotali.com/#/attestation/SN003KC3TKA",
      "linkCarteBrune": "https://aas.diotali.com/#/carte-brune/SN003KC3TKA"
    }
  }
}

Payload — insurance.contract.issuance_failed

{
  "event": "insurance.contract.issuance_failed",
  "reference": "INS-AGG-2026-000123",
  "paymentStatus": "COMPLETED",
  "issuedAt": "2026-04-24T19:01:00.000Z",
  "data": {
    "issuanceStatus": "FAILED",
    "error": "Erreur émission contrat après paiement"
  }
}

issuance_failed signifie que l'utilisateur a payé mais n'a pas de contrat. C'est un cas à remonter immédiatement côté partenaire (alerting, support). Voir Erreurs et troubleshooting.

Vérification de signature

Le header x-insurance-signature est construit ainsi :

x-insurance-signature: <hex(HMAC_SHA256(webhookSecret, raw_body))>

Quelques règles impératives :

  • La signature est calculée sur le body brut (bytes exacts reçus), pas sur un JSON re-sérialisé.
  • Comparez avec une fonction à temps constant (crypto.timingSafeEqual, hmac.compare_digest, etc.) pour éviter les timing attacks.
  • Si webhookSecretConfigured = false, aucun header n'est envoyé — vous devez alors vous reposer sur un mécanisme alternatif (IP allowlist, mTLS). Non recommandé en production.

Exemple Node.js

import crypto from 'node:crypto';

export function isValidSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader) return false;
  const received = Buffer.from(signatureHeader, 'hex');
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  return received.length === expected.length
    && crypto.timingSafeEqual(received, expected);
}

Exemple Python

import hmac, hashlib

def is_valid_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header:
        return False
    received = bytes.fromhex(signature_header)
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
    return hmac.compare_digest(received, expected)

Retry

La livraison webhook est effectuée en best-effort. Une politique de retry automatique est prévue côté plateforme, mais n'est pas contractualisée dans cette version.

Transition d etat recommandee cote partenaire

Signal recuEtat interne conseille
insurance.contract.issuedcontrat actif / dossier termine
insurance.contract.issuance_faileddossier en incident support
Pas de webhook mais ACTIVE via relecturecontrat actif, relecture de reconciliation
Pas de webhook et PAYMENT_FAILEDpaiement echoue ou annule

Recommandations côté partenaire

Idempotence

Utilisez x-insurance-reference comme clé unique côté base de données. Si vous recevez deux fois la même référence :

  • soit le handler a déjà tourné → ignorer et répondre 200 OK,
  • soit il s'agit d'une mise à jour (ex: passage de issuance_failed à issued suite à un rejeu) → mettre à jour l'état interne.

Anti-replay

Stocker la paire (reference, event, received_at) avec un TTL raisonnable (30 jours). Rejeter tout événement identique déjà traité.

Timeouts et performance

  • Répondre 2xx en moins de 5 secondes.
  • Déporter le traitement lourd (envoi email, génération PDF, etc.) dans une file de jobs.
  • Ne pas vérifier la signature après avoir parsé le JSON : parser en premier peut ouvrir une surface d'attaque (JSON bomb). Vérifiez la signature sur le body brut, puis parsez.

Recette intégration

TestAttendu
Webhook avec signature valideHandler exécuté, 200 retourné.
Webhook avec signature invalide401 retourné, aucun traitement.
Webhook sans header signature (et secret configuré côté agg)401 retourné.
Même reference reçue 3 foisTraitement une seule fois côté partenaire.
Webhook issuance_failed puis issued (rejeu admin)État passe correctement à issued sans duplication.

On this page