A3BC — Build digital trust. Together.
Français English
Documentation/Partage différé/Webhooks

Webhooks

Recevoir et vérifier les notifications delayed_sharing.update et delayed_sharing.revoke.

Qu’est-ce qu’un webhook ?

D’habitude, c’est votre serveur qui appelle une API. Avec un webhook, c’est l’inverse : A3BC appelle votre serveur quand quelque chose se passe (un credential vient d’être validé, un utilisateur a révoqué son consentement). Vous n’avez donc pas à interroger l’API en boucle : vous attendez d’être prévenu.

Pour cela, vous exposez une URL publique en HTTPS (par exemple https://app.example.com/webhooks/a3bc). Vous la communiquez à A3BC, qui configure votre « consumer ».

Cycle de vie d’une notification

  1. Un événement survient chez A3BC (par exemple une validation de credential).
  2. A3BC envoie un POST JSON à votre URL, avec l’en-tête X-Webhook-Signature.
  3. Votre serveur vérifie la signature, vérifie qu’il n’a pas déjà traité cet id, répond 204, puis traite en tâche de fond.
  4. Si vous répondez autre chose que 200, 201 ou 204, A3BC renvoie la même notification selon sa stratégie de reprise (même id, attempt augmenté). Si vous répondez 429, il ralentit.
  5. Après max_retries essais, la notification passe en échec.

A3BC envoie un POST HTTP à l’URL de votre consumer pour vous signaler les changements. Utilisez ces notifications pour savoir quand réinterroger l’API de partage différé.

Un webhook ne contient aucune donnée personnelle : seulement le contexte nécessaire pour appeler la bonne API.

Livraison

Votre endpoint doit répondre 200, 201 ou 204. Sinon, la notification est renvoyée selon une stratégie de reprise configurable. Un 429 adapte le rythme d’envoi à celui que vous pouvez tenir.

Payload

Attribut Type Description
id string Identifiant unique, identique à chaque nouvel essai de la même notification.
name string Nom du webhook.
timestamp_ms number Horodatage de création, en millisecondes.
attempt number Numéro de l’essai.
data objet Contenu propre au webhook.
{
  "id": "84a459b6-d297-11ec-8ff0-c3c3ea77040f",
  "name": "delayed_sharing.update",
  "timestamp_ms": 1651476325505,
  "attempt": 1,
  "data": {
    "delayed_sharing": "https://.../delayed-sharing/dddb6b7c-...",
    "new_claims": ["private:ssn", "private:ssn_key"]
  }
}

Événements

delayed_sharing.update

Nouveaux claims disponibles, échec de validation, etc.

Attribut Type Obligatoire Description
delayed_sharing string oui URL de l’API de partage différé.
new_claims liste non Noms des claims nouvellement disponibles. Les valeurs s’obtiennent via l’API.
unavailable_claims liste non Claims qui ne seront pas disponibles.
unavailable_claims_reason enum non user_deletion (l’utilisateur a supprimé le credential) ou validation_failed (la vérification a échoué).

delayed_sharing.revoke

L’utilisateur a révoqué son consentement : les claims ne sont plus disponibles.

Attribut Type Obligatoire Description
delayed_sharing string oui URL de l’API de partage différé.

Signature

Si la signature est activée, chaque requête porte l’en-tête X-Webhook-Signature de la forme version:HEX(hmac-sha256(payload)), par exemple :

X-Webhook-Signature: v1:006974CFD99911E1C67870A0FD57F4BAF51B4C9B02A339590D97EDB0D7D94FA7

La version identifie la clé de signature (versionnée, avec rotation possible). Le HMAC-SHA256 porte sur le corps brut de la requête.

Dans notre intégration, la clé est une chaîne base64 de 44 caractères (32 octets) : il faut la décoder avant de calculer le HMAC, puis comparer en majuscules. Le guide ne le précise pas.
import base64, hashlib, hmac

SECRETS = {"v1": "<clé base64 reçue>"}

def verify(raw_body: bytes, header: str) -> bool:
    version, digest = header.split(":", 1)
    secret = SECRETS.get(version.strip().lower())
    if not secret:
        return False
    key = base64.b64decode(secret.strip())
    expected = hmac.new(key, raw_body, hashlib.sha256).hexdigest().upper()
    return hmac.compare_digest(expected, digest.strip().upper())
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRETS = { v1: "<clé base64 reçue>" };

export function verify(rawBody, header) {
  const [version, digest] = header.split(":");
  const secret = SECRETS[version.trim().toLowerCase()];
  if (!secret) return false;
  const key = Buffer.from(secret.trim(), "base64");
  const expected = createHmac("sha256", key).update(rawBody).digest("hex").toUpperCase();
  const given = digest.trim().toUpperCase();
  return expected.length === given.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(given));
}

Calculez toujours le HMAC sur les octets reçus, avant tout parsing JSON.

Recevoir un webhook : l’essentiel du code

L’exemple de bout en bout contient un serveur complet. Voici les points d’attention :

@app.post("/webhooks/a3bc")
def webhook():
    raw = request.get_data()                       # 1. les octets bruts, AVANT tout parsing JSON
    if not signature_ok(raw, request.headers.get("X-Webhook-Signature", "")):
        return {"error": "invalid_signature"}, 401  # 2. rejeter ce qui n'est pas signé par A3BC
    event = json.loads(raw)
    if event["id"] in SEEN_EVENTS:                 # 3. une retransmission : déjà traitée
        return "", 204
    SEEN_EVENTS.add(event["id"])
    enqueue(event)                                 # 4. traiter en tâche de fond
    return "", 204                                 # 5. répondre vite

Ne parsez pas puis resérialisez le JSON pour calculer la signature : la moindre différence d’espaces ou d’ordre des clés la rend fausse.

Tester sans attendre A3BC

Simuler un webhook signé vers votre serveur local (Python) :

import base64, hashlib, hmac, json, time, uuid, requests

SECRET = "<clé base64 du webhook>"
payload = {"id": str(uuid.uuid4()), "name": "delayed_sharing.update",
           "timestamp_ms": int(time.time() * 1000), "attempt": 1,
           "data": {"delayed_sharing": "https://…/delayed-sharing/<id>", "new_claims": ["private:ssn"]}}
raw = json.dumps(payload, separators=(",", ":")).encode()          # ce que vous signez = ce que vous envoyez
sig = "v1:" + hmac.new(base64.b64decode(SECRET), raw, hashlib.sha256).hexdigest().upper()
r = requests.post("http://localhost:5000/webhooks/a3bc", data=raw,
                  headers={"Content-Type": "application/json", "X-Webhook-Signature": sig})
print(r.status_code)   # 204 attendu ; rejouez le même envoi : 204 aussi (déduplication)

Même chose en shell (OpenSSL) :

BODY='{"id":"a1","name":"delayed_sharing.revoke","data":{"delayed_sharing":"https://…"}}'
KEY_HEX=$(printf '%s' "$SECRET" | base64 -d | od -v -An -tx1 | tr -d ' \n')
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -mac HMAC -macopt hexkey:$KEY_HEX -hex | awk '{print toupper($NF)}')
curl -i -X POST http://localhost:5000/webhooks/a3bc \
  -H "Content-Type: application/json" -H "X-Webhook-Signature: v1:$SIG" --data-binary "$BODY"

Variantes à jouer à chaque fois : signature fausse (401 attendu), envoi rejoué (204 sans retraitement), corps modifié d’un caractère (401), événement inconnu (204 sans effet).

Pour les tests avec les vrais webhooks A3BC en local, exposez votre serveur avec un tunnel HTTPS (ngrok, cloudflared) et donnez cette URL à A3BC.

Dépannage

Constat Cause probable
401 côté votre serveur sur toutes les notifications Clé non décodée de base64, mauvaise version de clé, corps modifié par un proxy ou un framework avant la vérification, comparaison sensible à la casse (A3BC envoie en majuscules).
La même notification arrive plusieurs fois Normal : retransmission après un statut autre que 200/201/204, ou une réponse trop lente. Dédupliquez sur id.
Aucune notification Consumer non configuré ou inactif (active), URL non joignable depuis Internet, filtre accept_filter qui exclut l’événement.
Notifications espacées après des erreurs Stratégie de reprise exponentielle : le délai grandit jusqu’à max.

Configuration du consumer

La configuration est faite manuellement par un opérateur A3BC (pas d’auto-configuration à ce stade). Indiquez dans la demande d’accès l’URL de votre endpoint ; A3BC règle :

Attribut Description
url Destination des notifications.
accept_filter Liste des événements à recevoir.
signature, signature_key_info none ou hmac-sha256, avec version et valeur de la clé.
max_retries Nombre d’essais avant de passer l’événement en échec.
retry_strategy constant (interval) ou exponential (initial, multiplier, max).
rate_limit requests_per_second (0 = illimité) et burst.

Exemple de reprise exponentielle avec initial=1s, multiplier=5, max=30m : 1 s, 5 s, 25 s, 2 min 5 s, 10 min 25 s, puis 30 min pour chaque essai suivant.

Bonnes pratiques

  • Répondez vite, puis traitez en tâche de fond.
  • Dédupliquez sur id.
  • Sur update, récupérez un nouveau jeton client_credentials et appelez l’API ; stockez les claims non vides.
  • Sur revoke, cessez d’utiliser les données selon vos règles métier : l’API répond ensuite 403 (code 00020).