Webhooks

Webhooks tell your server when something happens in QBill. This guide covers receiving, the envelope, verifying the signature, retries, and the list of events.

Receiving

Create an endpoint in the portal, or with a secret key (qb_test_sk_… here):

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"]}'

With the Node SDK (coming to npm):

const endpoint = await qbill.webhookEndpoints.create({
  url: 'https://example.com/qbill/webhooks',
  events: ['invoice.paid', 'subscription.past_due'],
});

The answer carries the endpoint’s secret: keep it, you need it to verify every delivery. The portal’s Test button sends a webhook.test event to one endpoint.

The envelope

{
  "id": "evt_…",
  "type": "invoice.paid",
  "created": 1790000000,
  "livemode": false,
  "data": { }
}

created is in Unix seconds. livemode is false for test keys. The id is the same on every attempt of one event: store it and ignore an id you have already processed.

Verifying the signature

Each delivery carries two headers:

  • X-QBill-Signature: sha256=<hex>
  • X-QBill-Timestamp: <Unix milliseconds>

The signature is an HMAC-SHA256, keyed with the endpoint secret, of the string ${timestamp}.${rawBody}. Verify it on the raw body, exactly as received: parsing the JSON and serialising it again changes the bytes and breaks the signature.

By hand, in 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);
}

Compare in constant time, on buffers of equal length, as above. The SDK does all of it, and rejects a timestamp more than 300 seconds old:

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);
    // handle event.type
    res.sendStatus(200);
  } catch {
    res.sendStatus(400);
  }
});

express.raw hands you the raw body. Verifying needs no API key.

Retries

Answer within 30 seconds with any 2xx status: that delivery is done. Otherwise QBill tries again, up to 5 attempts in all, waiting about 1 s, 2 s, 4 s then 8 s between them. You can replay a delivery from the portal.

The 22 events

Subscribe an endpoint to any of these.

Customer

Event Meaning
customer.created A customer was created.
customer.updated A customer changed, by you or in their portal.
customer.deleted A customer was deleted.

Invoice

Event Meaning
invoice.created An invoice was raised (renewal, by hand, top-up, sales link); it carries the payment link.
invoice.paid An invoice settled.
invoice.payment_failed A payment attempt failed at the provider; the invoice becomes failed and the reminders start.
invoice.voided An invoice was voided and will not be collected.
invoice.refunded A paid invoice was refunded in full.
invoice.dunning_retry A payment failed and the customer was sent a reminder; says which one.

Subscription

Event Meaning
subscription.created A subscription was created.
subscription.updated Plan or quantity changed, or the end of the subscription was set or kept.
subscription.activated Renewed into a new period, or back from past due once paid.
subscription.paused A subscription was paused.
subscription.resumed A paused subscription resumed.
subscription.past_due The final reminder went out unpaid.
subscription.completed An instalment plan finished, or a one-time purchase was billed.
subscription.cancelled A subscription ended.
subscription.reactivated A cancelled subscription was brought back, as a new one.

Wallet

Event Meaning
wallet.topped_up A top-up was paid and credited to the prepaid wallet.
wallet.low_balance A prepaid wallet fell under its threshold.
wallet.auto_recharge A prepaid wallet started topping itself up.

Usage

Event Meaning
usage.threshold_reached A subscriber reached 80 % or 100 % of a usage limit.

Test

Event Meaning
webhook.test Sent by the Test button, to that endpoint only; you do not subscribe to it.

Errors on the API side are covered in Errors; the dunning events in Dunning; usage thresholds in Usage.