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.
L’enveloppe et les en-têtes
Section intitulée « L’enveloppe et les en-têtes »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) |
Vérifier la signature
Section intitulée « Vérifier la signature »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');});import hashlib, hmac, time
def verify_webhook(raw_body: bytes, signature_header: str, timestamp: str, secret: str) -> bool: if abs(time.time() - int(timestamp)) > 300: return False # périmé, rejeu possible provided = (signature_header or '').removeprefix('sha256=') signed = f'{timestamp}.'.encode() + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, provided)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.
Les retentatives
Section intitulée « Les retentatives »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.
Le journal de livraison
Section intitulée « Le journal de livraison »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.
Renouveler le secret de signature
Section intitulée « Renouveler le secret de signature »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.
D’où partent les livraisons
Section intitulée « D’où partent les livraisons »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.