Webhooks
Les webhooks préviennent votre serveur quand quelque chose arrive dans QBill. Ce guide couvre la réception, l’enveloppe, la vérification de la signature, les réessais et la liste des événements.
Recevoir
Créez un endpoint dans le portail, ou avec une clé secrète (ici qb_test_sk_…) :
curl -X POST https://api.qbill.dev/v1/webhooks \
-H "x-api-key: qb_test_sk_…" \
-H "content-type: application/json" \
-d '{"url":"https://example.com/qbill/webhooks","events":["invoice.paid","subscription.past_due"]}'
Avec le SDK Node (bientôt sur npm) :
const endpoint = await qbill.webhookEndpoints.create({
url: 'https://example.com/qbill/webhooks',
events: ['invoice.paid', 'subscription.past_due'],
});
La réponse porte le secret de l’endpoint : gardez-le, il sert à vérifier chaque livraison. Le bouton Test du portail envoie un événement webhook.test à un endpoint.
L’enveloppe
{
"id": "evt_…",
"type": "invoice.paid",
"created": 1790000000,
"livemode": false,
"data": { }
}
created est en secondes Unix. livemode vaut false avec une clé de test. L’id est le même à chaque tentative d’un même événement : stockez-le et ignorez un id déjà traité.
Vérifier la signature
Chaque livraison porte deux en-têtes :
X-QBill-Signature: sha256=<hex>X-QBill-Timestamp: <millisecondes Unix>
La signature est un HMAC-SHA256, avec le secret de l’endpoint pour clé, de la chaîne ${timestamp}.${rawBody}. Vérifiez-la sur le corps brut, tel qu’il est reçu : lire le JSON puis le resérialiser change les octets et casse la signature.
À la main, en Node :
import crypto from 'node:crypto';
function verify(rawBody, headers, secret) {
const timestamp = headers['x-qbill-timestamp'];
const received = String(headers['x-qbill-signature'] ?? '').replace(/^sha256=/, '');
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(received);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Comparez en temps constant, sur des tampons de même longueur, comme ci-dessus. Le SDK fait tout cela, et refuse un horodatage vieux de plus de 300 secondes :
import { QBill } from '@qbill/node';
app.post('/qbill/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = QBill.webhooks.constructEvent(req.body, req.headers, process.env.QBILL_WEBHOOK_SECRET);
// traiter event.type
res.sendStatus(200);
} catch {
res.sendStatus(400);
}
});
express.raw vous donne le corps brut. La vérification n’a pas besoin de clé d’API.
Réessais
Répondez en moins de 30 secondes avec n’importe quel statut 2xx : la livraison est terminée. Sinon QBill réessaie, jusqu’à 5 tentatives en tout, en attendant environ 1 s, 2 s, 4 s puis 8 s entre elles. Vous pouvez rejouer une livraison depuis le portail.
Les 22 événements
Abonnez un endpoint à n’importe lesquels de ces événements.
Client
| Événement | Sens |
|---|---|
customer.created |
Un client a été créé. |
customer.updated |
Un client a changé, par vous ou dans son portail. |
customer.deleted |
Un client a été supprimé. |
Facture
| Événement | Sens |
|---|---|
invoice.created |
Une facture a été émise (renouvellement, à la main, recharge, lien de vente) ; elle porte le lien de paiement. |
invoice.paid |
Une facture a été réglée. |
invoice.payment_failed |
Une tentative de paiement a échoué chez le prestataire ; la facture passe à failed et les relances démarrent. |
invoice.voided |
Une facture a été annulée et ne sera pas encaissée. |
invoice.refunded |
Une facture payée a été remboursée en totalité. |
invoice.dunning_retry |
Un paiement a échoué et le client a reçu une relance ; indique laquelle. |
Abonnement
| Événement | Sens |
|---|---|
subscription.created |
Un abonnement a été créé. |
subscription.updated |
Le plan ou la quantité a changé, ou la fin de l’abonnement a été fixée ou maintenue. |
subscription.activated |
Renouvelé dans une nouvelle période, ou sorti de past_due une fois payé. |
subscription.paused |
Un abonnement a été mis en pause. |
subscription.resumed |
Un abonnement en pause a repris. |
subscription.past_due |
La dernière relance est partie sans paiement. |
subscription.completed |
Un plan en plusieurs fois est terminé, ou un achat unique a été facturé. |
subscription.cancelled |
Un abonnement s’est terminé. |
subscription.reactivated |
Un abonnement annulé a été remis en place, comme un nouvel abonnement. |
Portefeuille prépayé
| Événement | Sens |
|---|---|
wallet.topped_up |
Une recharge a été payée et créditée sur le portefeuille prépayé. |
wallet.low_balance |
Un portefeuille prépayé est passé sous son seuil. |
wallet.auto_recharge |
Un portefeuille prépayé a lancé sa recharge. |
Usage
| Événement | Sens |
|---|---|
usage.threshold_reached |
Un abonné a atteint 80 % ou 100 % d’une limite d’usage. |
Test
| Événement | Sens |
|---|---|
webhook.test |
Envoyé par le bouton Test, à cet endpoint seulement ; on ne s’y abonne pas. |
Les erreurs côté API sont dans Erreurs ; les événements de relance dans Relances ; les seuils d’usage dans Usage.