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.