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.