Aller au contenu

Webhooks

CowriePay envoie une requête HTTP POST à vos endpoints enregistrés dès qu’un événement suivi se produit. Chaque livraison est signée en HMAC-SHA256, et la vérification de cette signature est la seule chose qui vous garantisse que la requête vient bien de nous.

La liste des événements auxquels vous pouvez vous abonner vit avec la référence de l’API, à côté du champ events qui les accepte : voir la référence Webhooks.

Le corps a toujours les trois mêmes clés :

{
"event": "DEPOSIT_CONFIRMED",
"created_at": "2024-05-27T10:10:00.000Z",
"data": { "...": "champs propres à l'événement" }
}

Quatre en-têtes l’accompagnent :

En-tête Valeur
X-CowriePay-Event Le nom de l’événement, la même valeur que event dans le corps
X-CowriePay-Timestamp Horodatage Unix en secondes, au moment de la signature de la livraison
X-CowriePay-Signature sha256=<hex>, HMAC-SHA256 sur {timestamp}.{corps_brut}
X-CowriePay-Delivery L’identifiant de livraison, votre clé de déduplication (voir plus bas)

Retirez le préfixe sha256=, recalculez HMAC_SHA256(secret_endpoint, X-CowriePay-Timestamp + "." + corps_brut), puis comparez en temps constant. Rejetez une livraison dont l’horodatage a plus de quelques minutes : c’est ce qui empêche qu’une requête interceptée vous soit rejouée plus tard.

Signez les octets bruts du corps, et n’analysez le JSON qu’une fois la signature validée. Re-sérialiser l’objet déjà analysé est la cause habituelle d’une intégration correcte qui signale une signature invalide : une différence d’espacement ou d’ordre des clés change les octets, donc l’empreinte.

const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, timestamp, secret) {
const provided = (signatureHeader || '').replace(/^sha256=/, '');
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(provided), b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express : montez express.raw pour que req.body soit les octets bruts, pas un objet analysé.
app.post('/webhooks/cowriepay', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-cowriepay-signature'];
const ts = req.headers['x-cowriepay-timestamp'];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.status(400).send('Stale');
if (!verifyWebhook(req.body, sig, ts, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(req.body);
// traiter payload.event ...
res.status(200).send('OK');
});

Utilisez une comparaison en temps constant (timingSafeEqual, compare_digest). L’égalité de chaînes ordinaire révèle quelle portion de la signature était correcte, caractère par caractère.

L’identifiant de livraison est stable d’une retentative à l’autre

Section intitulée « L’identifiant de livraison est stable d’une retentative à l’autre »

X-CowriePay-Delivery identifie la livraison, pas la tentative. Les cinq tentatives d’une même livraison portent le même identifiant, et c’est précisément ce qui en fait une clé de déduplication utilisable : stockez-le, et ignorez une livraison que vous avez déjà traitée.

Deux livraisons d’un même événement sous-jacent portent des identifiants différents, et c’est délibéré. Cela se produit quand vous avez plusieurs endpoints enregistrés (une livraison chacun), et quand notre tâche de réconciliation constate qu’un événement de règlement ne vous est jamais parvenu et en réenfile un nouveau. Dédupliquez donc sur l’identifiant de livraison pour un traitement au plus une fois d’une tentative, et sur l’identifiant de la ressource présent dans la charge utile (deposit_id, withdrawal_id) pour que votre propre effet de bord ne se produise qu’une seule fois.

Votre endpoint dispose de 10 secondes pour renvoyer un 2xx. Tout le reste, un dépassement de délai, une erreur de connexion, ou tout statut non-2xx, compte comme un échec et déclenche une retentative. Il y a 5 tentatives au total, espacées d’un délai exponentiel de 1, 2, 4 puis 8 minutes :

Tentative Quand
1 Immédiatement
2 1 minute après la tentative 1
3 2 minutes après la tentative 2
4 4 minutes après la tentative 3
5 8 minutes après la tentative 4

Après la cinquième tentative, la livraison passe en FAILED et n’est plus réessayée.

La fenêtre complète de retentatives est donc courte : environ 15 minutes au total. Dimensionnez la disponibilité de votre endpoint en conséquence : un déploiement qui coupe votre récepteur pendant une demi-heure perdra des livraisons, et le chemin de reprise consiste à les relire dans le journal de livraison, pas à attendre une retentative qui ne viendra pas. Les tentatives sont déclenchées par un worker qui s’exécute toutes les 10 secondes : considérez les délais ci-dessus comme le moment le plus tôt où une retentative peut avoir lieu, non comme un horaire précis.

Si un endpoint accumule 5 livraisons définitivement échouées en 7 jours, nous prévenons par courriel le contact du workspace qu’il semble hors service.

GET /v2/webhooks/{id}/deliveries renvoie ce qui s’est réellement passé : l’événement, le statut, le statut HTTP que nous avons reçu en retour, le nombre de tentatives, la dernière erreur et les horodatages. C’est là que vous regardez quand une livraison n’est jamais arrivée, et c’est ainsi que vous récupérez celles qui ont échoué pendant que votre récepteur était indisponible.

Les enregistrements livrés et échoués sont conservés 90 jours, puis purgés. Ceux qui sont encore en attente ne sont jamais purgés.

POST /v2/webhooks/{id}/rotate-secret émet un nouveau secret de signature, renvoyé une seule fois dans la réponse et jamais récupérable ensuite.

Pour ne perdre aucune livraison pendant que vous mettez à jour votre copie, l’ancien secret reste valide durant une fenêtre de recouvrement de 24 heures. Pendant cette fenêtre, chaque livraison porte deux en-têtes de signature :

En-tête Signé avec
X-CowriePay-Signature le NOUVEAU secret
X-CowriePay-Signature-Previous l’ANCIEN secret, présent uniquement pendant le recouvrement

Acceptez la requête si l’un ou l’autre de ces en-têtes correspond au secret que vous détenez actuellement. Le déroulé : renouvelez, stockez le nouveau secret, continuez d’accepter les deux en-têtes jusqu’à ce que votre déploiement soit en ligne, puis laissez la fenêtre expirer. La réponse renvoie previous_secret_expires_at, qui vous indique à partir de quand l’ancien secret cesse d’être accepté.

Détail des frais sur les événements de règlement

Section intitulée « Détail des frais sur les événements de règlement »

DEPOSIT_SWEPT et WITHDRAWAL_CONFIRMED portent un objet fee, qui vous permet de rapprocher le net crédité ou envoyé sans un second appel. gross_amount est le montant on-chain, fee_amount est les frais CowriePay, net_amount est ce qui est entré dans le solde (dépôt) ou en est sorti (retrait). Tous les montants sont des chaînes décimales lisibles.

{
"event": "DEPOSIT_SWEPT",
"created_at": "2024-05-27T10:15:00.000Z",
"data": {
"deposit_id": "c3d4e5f6-0000-0000-0000-000000000003",
"workspace_id": "11111111-0000-0000-0000-000000000000",
"wallet_id": "a1b2c3d4-0000-0000-0000-000000000001",
"chain": "TRON_MAINNET",
"asset": "USDT_TRON",
"amount": "50.000000",
"txid": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab",
"status": "SWEPT",
"confirmations": 19,
"required_confirmations": 19,
"detected_at": "2024-05-27T10:05:00.000Z",
"confirmed_at": "2024-05-27T10:10:00.000Z",
"fee": {
"gross_amount": "50.000000",
"fee_amount": "0.450000",
"net_amount": "49.550000",
"applied_rate": "0.009",
"applied_min_fee": "0.300000"
}
}
}

L’objet fee est additif : il est absent des événements antérieurs du cycle de vie (DEPOSIT_DETECTED, DEPOSIT_CONFIRMED, WITHDRAWAL_PROCESSING). Traitez son absence avec souplesse, et lisez Compatibilité ascendante pour savoir ce qui peut encore apparaître dans une charge utile au fil du temps.

Les livraisons partent aujourd’hui d’une seule adresse publique. Sa valeur, les raisons pour lesquelles elle peut changer, et pourquoi une liste d’autorisation d’IP relève du confort pare-feu et non d’un substitut à la vérification de signature, sont sur la page IP source des webhooks.