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 (draft only);
  • POST /v1/invoices/:id/void: cancels a draft, open or failed invoice;
  • 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).