Webhooks
Définissez callbackUrl sur un paiement et Stablezact y enverra des requêtes
POST au fil de la progression du paiement. C’est le moyen fiable de savoir qu’un
paiement est réglé, car il ne dépend pas du fait que le client garde un onglet
de navigateur ouvert.
config={{ amount: 49.99, currency: 'USDC', callbackUrl: 'https://yourstore.com/webhooks/stablezact', orderId: 'ORD-1234', metadata: { basket: 'b-99' },}}orderId et metadata sont tous deux renvoyés dans chaque événement : c’est
ainsi que vous associez un événement à une commande sans tenir votre propre
table de correspondance.
Événements
Section intitulée « Événements »Lisez type. Il utilise le même vocabulaire réduit, quelle que soit la façon
dont le client a payé.
type |
Signification | Honorer |
|---|---|---|
payment.pending |
des fonds ont été détectés, le paiement n’est pas encore final | non |
payment.succeeded |
le client a payé | oui |
payment.underpaid |
le montant reçu est inférieur au montant facturé | non, relancez le client |
payment.failed |
expiré, annulé ou échoué | non |
payment.refunded |
les fonds ont été rendus au client | annulez la commande |
settlement.completed |
votre versement est arrivé sur votre wallet | déjà honorée |
settlement.delayed |
votre versement est plus lent que d’habitude, nouvelles tentatives automatiques | rien à faire |
settlement.failed |
votre versement n’a pas pu être effectué, nous vous contactons | rien à faire |
webhook.test |
une livraison de test | non |
payment.succeeded est le seul qui signifie d’honorer la commande. Tous les
autres sont là si vous en avez besoin.
Les événements payment répondent à la question « dois-je expédier cette
commande ». Les événements settlement répondent à « où est mon argent », et ils
ne changent jamais la décision d’expédier. Beaucoup d’intégrations les ignorent
et font leur rapprochement à partir de leur wallet.
type est aussi envoyé dans l’en-tête X-Webhook-Type, si vous préférez router
sur un en-tête plutôt que d’analyser le corps.
L’ancien champ event
Section intitulée « L’ancien champ event »Chaque payload contient aussi un champ event, antérieur à type. Il utilise
deux conventions de nommage, donne deux noms différents au même résultat selon
la façon dont le client a payé, et nomme nos étapes internes de règlement. Il
fonctionne toujours et continuera de fonctionner : les intégrations existantes
n’ont pas besoin d’être modifiées.
Construisez les nouvelles intégrations sur type. Si vous lisez une intégration
plus ancienne, voici la correspondance entre les deux :
event |
type |
|---|---|
payment_success, deposit.confirmed |
payment.succeeded |
payment_failure, deposit.failed, bridge.failed |
payment.failed |
deposit.detected, bridge.filled |
payment.pending |
deposit.swept, settlement.completed |
settlement.completed |
settlement.delayed |
settlement.delayed |
payment.underpaid |
payment.underpaid |
payment.refunded |
payment.refunded |
webhook_test |
webhook.test |
Le paiement est paymentId dans chaque payload. Certains anciens événements
l’appellent aussi transactionId ; les deux sont présents et identiques. La
transaction de versement est settlementTxHash, également présente sous le nom
sweepTxHash dans les anciens événements.
Si vous ne savez pas quel événement vous avez sous les yeux, récupérez le
paiement et lisez outcome. C’est la même réponse, et elle ne dépend pas des
événements qui vous sont parvenus.
Vérifier que la requête vient de nous
Section intitulée « Vérifier que la requête vient de nous »Chaque livraison comporte deux en-têtes :
X-Webhook-Signature: sha256=<hex digest>X-Webhook-Timestamp: <milliseconds since epoch>Effectuez la vérification avant d’agir sur le corps. Un endpoint qui honore des commandes sur un POST non vérifié honorera des commandes pour quiconque trouve l’URL.
Utilisez l’utilitaire du SDK si vous le pouvez
Section intitulée « Utilisez l’utilitaire du SDK si vous le pouvez »Le SDK fournit la vérification, vous n’avez donc pas à la réimplémenter :
import { verifyWebhookSignature } from 'stablezact-pay';
app.post('/webhooks/stablezact', express.json(), (req, res) => { const result = verifyWebhookSignature( req.body, req.headers['x-webhook-signature'], req.headers['x-webhook-timestamp'], process.env.STABLEZACT_WEBHOOK_SECRET );
if (!result.valid) return res.status(400).json({ error: result.error });
res.sendStatus(200); handleEvent(req.body);});Il existe aussi coinleyWebhookMiddleware si vous préférez le monter en tant
que middleware.
La vérifier vous-même
Section intitulée « La vérifier vous-même »import crypto from 'node:crypto';
function verify(payload, signatureHeader, timestampHeader, secret) { const ts = parseInt(timestampHeader, 10);
// Reject anything older than five minutes, so a captured delivery cannot be // replayed later. if (Date.now() - ts > 5 * 60 * 1000) return false;
const withTimestamp = { ...payload, timestamp: ts }; const serialized = JSON.stringify(withTimestamp, Object.keys(withTimestamp).sort());
const expected = crypto.createHmac('sha256', secret).update(serialized).digest('hex'); const received = signatureHeader.replace(/^sha256=/, '');
const a = Buffer.from(received, 'hex'); const b = Buffer.from(expected, 'hex'); return a.length === b.length && crypto.timingSafeEqual(a, b);}Trois détails qui comptent :
Le timestamp fait partie des données signées : il doit donc être ajouté au
payload avant la sérialisation, sous forme de nombre.
Les clés sont triées. JSON.stringify(value, Object.keys(value).sort()) est
exactement l’appel que fait le serveur.
Votre secret webhook doit comporter au moins 32 caractères. Les secrets plus courts sont rejetés par le vérificateur, plutôt que d’affaiblir silencieusement la signature.
Traiter les livraisons
Section intitulée « Traiter les livraisons »Répondez 200 rapidement et faites le travail ensuite. Un endpoint lent
ressemble à un endpoint en échec.
Gérez le cas où le même événement arrive deux fois. Les nouvelles tentatives et
les délais d’attente réseau impliquent une livraison au moins une fois : utiliser
comme clé paymentId plus event et ignorer les répétitions vaut bien les
quelques lignes que cela demande.
