Invoices & payments
A subscription raises its own invoices; you can also create them by hand. Examples use a qb_test_sk_… key.
Lifecycle
| Status | Meaning |
|---|---|
draft |
A draft, not sent yet. |
open |
Issued, waiting for payment. |
paid |
Paid. |
failed |
The payment failed. |
void |
Cancelled. |
refunded |
Refunded. |
A manual invoice
curl -X POST https://api.qbill.dev/v1/invoices \
-H "x-api-key: qb_test_sk_…" \
-H "content-type: application/json" \
-d '{"customer":"<customer id>","amount":15000,"description":"Setup","due_date":"2026-11-01","status":"open"}'
amount is in XOF, before tax. status is draft or open. Three actions on an existing invoice:
POST /v1/invoices/:id/finalize: takes a draft out of draft (draftonly);POST /v1/invoices/:id/void: cancels adraft,openorfailedinvoice;POST /v1/invoices/:id/resend: sends the invoice to the customer again.
The payment link
An invoice read from the API carries a payment_url, shaped like …/pay/<token>, null for a draft. That link is sent in invoice emails and in the invoice.created event.
Collecting with Wave
curl -X POST https://api.qbill.dev/v1/payments/initiate \
-H "x-api-key: qb_live_sk_…" \
-H "content-type: application/json" \
-d '{"invoice_id":"<invoice id>","phone_number":"+2250700000000","provider":"wave"}'
Body: invoice_id, phone_number, provider: "wave", and optionally success_url and error_url. The answer is { reference, status, redirectUrl }: redirect the customer to redirectUrl. With a test key that page is simulated (see Test mode). With the Node SDK (coming to npm): qbill.payments.initiate(...).
Your Wave account or QBill’s
- On your own Wave account: 0% QBill fee, the money arrives directly with you.
- On QBill’s Wave account: 1.5% of each payment collected, with a minimum of 100 XOF (never more than the payment itself). The amount is credited to your wallet, which you withdraw to a Wave number after verification.
Refunding
curl -X POST https://api.qbill.dev/v1/refunds \
-H "x-api-key: qb_live_sk_…" \
-H "content-type: application/json" \
-d '{"invoice_id":"<invoice id>","reason":"Customer request"}'
A refund is full and only applies to a paid invoice; otherwise the error is invoice_not_refundable. Refunding the same invoice twice is safe: the answer carries already_refunded: true. A publishable key can never refund (see Authentication & keys).