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
- Un événement survient chez A3BC (par exemple une validation de credential).
- A3BC envoie un
POSTJSON à votre URL, avec l’en-têteX-Webhook-Signature. - Votre serveur vérifie la signature, vérifie qu’il n’a pas déjà traité cet
id, répond204, puis traite en tâche de fond. - Si vous répondez autre chose que
200,201ou204, A3BC renvoie la même notification selon sa stratégie de reprise (mêmeid,attemptaugmenté). Si vous répondez429, il ralentit. - Après
max_retriesessais, 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.
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 jetonclient_credentialset 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 ensuite403(code00020).