Documentation / Référence API Webhooks
Référence API

Webhooks

Les webhooks préviennent ton application en temps quasi réel d'un événement côté CrowAccounts (révocation, changement d'e-mail, suppression de compte), au lieu de devoir attendre qu'un refresh_token échoue pour t'en rendre compte.

Notification "au plus tôt", pas une garantie

Un webhook accélère la détection, il ne la remplace pas. Continue de traiter un refresh_token qui échoue comme le signal de vérité ultime — c'est ton filet de sécurité si, dans de très rares cas, toutes les tentatives de livraison échouent (voir Fiabilité de livraison).

Configuration

Renseigne un webhook_url depuis Mes applications → [ton app] → Modifier. Contraintes, appliquées à la création comme à la modification :

  • Doit être en HTTPS — le secret de signature ne doit jamais transiter en clair.
  • Ne peut pas cibler une adresse locale, privée ou réservée (localhost, 127.0.0.1, plages 10.x/192.168.x/172.16-31.x, lien-local 169.254.x...) — protection contre le SSRF côté serveur CrowAccounts.
  • Champ optionnel : le laisser vide désactive simplement l'envoi de webhooks pour cette app.

Un webhook_secret est généré automatiquement à la création de l'application (même si webhook_url est vide), affiché une seule fois. Tu peux le régénérer à tout moment depuis le dashboard si besoin — pense alors à mettre à jour ton code de vérification avec le nouveau secret immédiatement, l'ancien cesse de fonctionner tout de suite.

Événements disponibles

ÉvénementDéclenché quandContenu de data
consent.revokedL'utilisateur révoque l'accès à ton application (individuellement ou via "tout révoquer") depuis son dashboard.{ "sub": "<user_id>" }
user.email_updatedUn utilisateur change son adresse e-mail — envoyé uniquement aux apps ayant le scope email accordé pour cet utilisateur.{ "sub": "<user_id>" }
user.deletedUn utilisateur supprime son compte CrowAccounts.{ "sub": "<user_id>" }

La donnée n'est jamais dans le payload

Volontairement, le webhook ne transporte que l'identifiant utilisateur (sub), jamais la nouvelle valeur (le nouvel e-mail, par exemple). Si tu as besoin de la valeur à jour, rappelle /oauth/userinfo avec un jeton valide pour cet utilisateur plutôt que de faire confiance à un contenu qui pourrait être rejoué.

Format du payload et des en-têtes

json
{
  "id": "a1b2c3d4-...",      // identifiant unique de l'événement — constant sur toutes les relances
  "event": "consent.revoked",
  "created_at": "2026-09-07T10:32:00.000Z",
  "data": { "sub": "42" }
}
En-têteDescription
X-CrowAccounts-EventLe type d'événement (identique au champ event du corps).
X-CrowAccounts-TimestampHorodatage Unix (secondes) de cette tentative précise d'envoi — change à chaque relance, contrairement à created_at.
X-CrowAccounts-SignatureFormat sha256=<hex>, HMAC-SHA256 de "{timestamp}.{corps_brut}" avec ton webhook_secret.
User-AgentCrowAccounts-Webhooks/1.0

Vérifier la signature

Recalcule le HMAC côté serveur et compare-le en temps constant. Vérifie aussi la fraîcheur du timestamp pour te protéger d'un rejeu (une requête interceptée puis renvoyée telle quelle des mois plus tard) : une tolérance de 5 minutes est un bon point de départ.

javascript
const crypto = require('crypto');

const TOLERANCE_SECONDS = 300; // 5 minutes

function verifyCrowAccountsWebhook(rawBody, headers, webhookSecret) {
    const timestamp = headers['x-crowaccounts-timestamp'];
    const signatureHeader = headers['x-crowaccounts-signature'] || '';
    const [, receivedSignature] = signatureHeader.split('=');

    if (!timestamp || !receivedSignature) return false;

    // Rejette un message trop vieux (rejeu) — comparaison en clair, pas sensible.
    const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
    if (ageSeconds > TOLERANCE_SECONDS) return false;

    const expectedSignature = crypto
        .createHmac('sha256', webhookSecret)
        .update(`${timestamp}.${rawBody}`)
        .digest('hex');

    const a = Buffer.from(receivedSignature, 'hex');
    const b = Buffer.from(expectedSignature, 'hex');

    // Longueurs différentes -> timingSafeEqual lèverait une exception
    if (a.length !== b.length) return false;

    return crypto.timingSafeEqual(a, b);
}

Utilise le corps brut, pas le JSON reparsé

La signature porte sur les octets exacts envoyés. Si ton framework parse automatiquement le JSON avant que tu n'y aies accès (Express avec express.json() par exemple), configure une route dédiée qui conserve le corps brut (express.raw()) pour ce endpoint précis — JSON.stringify(JSON.parse(body)) ne redonne pas forcément les mêmes octets.

Fiabilité de livraison

La première tentative est synchrone, avec un délai maximum de 8 secondes. Si elle échoue (site injoignable, timeout, réponse non-2xx), CrowAccounts retente automatiquement avec un délai croissant :

TentativeDélai avant cette tentative
1Immédiat
230 secondes
32 minutes
410 minutes
51 heure
6 (dernière)6 heures

Soit environ 7 heures de fenêtre totale avant abandon définitif. À chaque tentative, l'URL et le secret utilisés sont relus à jour — si tu as changé ton webhook_url ou régénéré ton webhook_secret entre-temps, la relance en tient compte (ou s'annule si le webhook a été désactivé).

Bonnes pratiques côté récepteur

  • Réponds vite. Renvoie un 200 dès que tu as validé la signature, puis traite l'événement de façon asynchrone si le traitement est long — une réponse tardive (> 8s) ou un code non-2xx est comptée comme un échec et déclenche une relance.
  • Sois idempotent. L'id de l'événement reste identique sur toutes les tentatives d'un même envoi (y compris les relances) : déduplique dessus si tu veux éviter de traiter deux fois le même événement.
  • Ne fais jamais confiance à une requête non signée ou dont la signature ne correspond pas — rejette-la avec un 401, ne la traite pas "au cas où".
  • En développement local, comme webhook_url doit être une adresse publique en HTTPS, utilise un tunnel (ngrok, Cloudflare Tunnel, etc.) pour exposer temporairement ton serveur local.