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.
Un endpoint doit être une URL HTTPS publique. Une adresse IP privée ou réservée, un nom d’hôte sans
point, et les suffixes .internal, .local, .localhost et .home.arpa sont refusés, à la création,
à la modification, et de nouveau à chaque tentative de livraison.
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.
Les redirections ne sont jamais suivies. Une réponse 3xx est un échec comme tout autre statut non-2xx : enregistrez l’URL finale de votre récepteur, pas une URL qui renvoie ailleurs. Seuls les 1 000 premiers caractères du corps de votre réponse sont enregistrés : un accusé de réception court suffit.
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.
La dernière erreur est un code :
last_error |
Signification |
|---|---|
CONNECTION_FAILED |
La connexion à votre endpoint n’a pas pu s’ouvrir, ou a été coupée |
TIMEOUT |
Votre endpoint n’a pas répondu à temps |
TLS_ERROR |
La négociation TLS a échoué, le plus souvent à cause du certificat |
DNS_FAILED |
Le nom d’hôte n’a pas pu être résolu |
REDIRECT_NOT_FOLLOWED |
Votre endpoint a répondu 3xx ; les redirections ne sont jamais suivies |
DESTINATION_NOT_ALLOWED |
L’URL, ou une adresse vers laquelle elle se résout, n’est pas publique |
PLATFORM_ERROR |
Un échec de notre côté, avant tout contact avec votre endpoint ; la livraison est retentée |
HTTP_<statut> |
Tout autre statut non-2xx, par exemple HTTP_500 |
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.