Endpoints — Invoices
Invoices are the bridge between subscriber transactions and the operator’s accounting. They support partial payments, FIFO quick-pay against open balances, consolidation of many pending invoices into one, headless-Chromium PDF rendering, and an unauthenticated public receipt URL that powers QR-code-scanned receipts.
Base URL
Section titled “Base URL”https://your-panel-host/api/invoicesPublic (no-auth) URLs additionally live under:
https://your-panel-host/api/invoices/public/:idhttps://your-panel-host/api/invoices/public/:id/pdfAuthentication
Section titled “Authentication”All /api/invoices/* endpoints require Authorization: Bearer <jwt>. The two public routes intentionally do not — they let an end customer scan the QR code on a printed invoice and see the bill or download the PDF without an account.
Reseller scoping: a reseller sees only invoices for subscribers in their tree. Admins see everything. Modify-verbs require ResellerOrAdmin(); delete is admin-only.
The invoice object
Section titled “The invoice object”{ "id": 4521, "invoice_number": "INV-2026-04521", "subscriber_id": 102, "reseller_id": 7, "amount": 25.00, "tax_amount": 2.50, "total": 27.50, "paid_amount": 0.00, "balance": 27.50, "status": "pending", "currency": "USD", "issue_date": "2026-05-01", "due_date": "2026-05-15", "paid_at": null, "items": [ { "description": "8M-20G plan (May 2026)", "qty": 1, "unit_price": 25.00, "amount": 25.00 } ], "notes": "Auto-generated on renewal", "commission_rate": 10.0, "commission_amount": 2.50, "created_at": "2026-05-01T08:00:01Z"}status values: pending, partial, paid, overdue, void.
GET /api/invoices
Section titled “GET /api/invoices”List invoices.
Permission: any authenticated user (results are scope-filtered).
Query parameters
| Param | Type | Description |
|---|---|---|
status | string | pending, partial, paid, overdue, void |
subscriber_id | int | One subscriber’s invoices |
reseller_id | int | Admin scope filter |
from / to | string | ISO date range on issue_date |
search | string | Match on invoice_number, subscriber username/full_name |
page / limit | int | Default 1 / 50, max 500 |
Response — 200 OK
{ "success": true, "data": [ { "id": 4521, "...": "..." } ], "pagination": { "page": 1, "limit": 50, "total": 318 }}curl "https://panel.example.com/api/invoices?status=pending&limit=20" \ -H "Authorization: Bearer ..."GET /api/invoices/:id
Section titled “GET /api/invoices/:id”Return one invoice with full item lines and payment history.
Permission: any authenticated user (scope-filtered).
Errors: 404 invoice not found, 403 not authorized.
POST /api/invoices
Section titled “POST /api/invoices”Create an invoice manually. (Renewals create invoices automatically via the renewal handler — you typically only call this for ad-hoc charges.)
Permission: admin or reseller (ResellerOrAdmin).
Body
| Field | Type | Required | Description |
|---|---|---|---|
subscriber_id | int | yes | FK |
items | array | yes | Each { description, qty, unit_price } |
tax_rate | number | no | % — defaults to system setting default_tax_rate |
due_date | string | no | ISO date — defaults to today + default_due_days |
notes | string | no | Free text rendered on PDF |
currency | string | no | 3-letter ISO; defaults to system currency |
Response — 201 Created — full invoice object including computed amount, tax_amount, total, invoice_number, commission_rate, commission_amount.
curl -X POST https://panel.example.com/api/invoices \ -H "Authorization: Bearer ..." \ -H "Content-Type: application/json" \ -d '{ "subscriber_id": 102, "items": [{ "description": "Installation fee", "qty": 1, "unit_price": 50.00 }], "tax_rate": 10, "due_date": "2026-06-01" }'PUT /api/invoices/:id
Section titled “PUT /api/invoices/:id”Edit a pending invoice. Blocked once the invoice has any payment (status != pending) — to change a paid invoice, void and re-issue.
Permission: admin or reseller.
Editable fields: items, tax_rate, due_date, notes. Recomputes amount / tax_amount / total.
DELETE /api/invoices/:id
Section titled “DELETE /api/invoices/:id”Hard-delete. Admin only. Use sparingly — voiding is preferred over delete for audit hygiene. The endpoint refuses to delete a paid invoice (409 cannot delete paid invoice).
POST /api/invoices/:id/payment (Add payment)
Section titled “POST /api/invoices/:id/payment (Add payment)”Record a payment against this invoice. Partial payments are supported — repeat the call until balance = 0 and the invoice flips to paid.
Permission: admin or reseller.
Body
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | yes | Must be ≤ remaining balance |
method | string | no | cash, card, bank, wallet, prepaid_card, other |
reference | string | no | Operator-supplied reference (cheque #, txn id) |
paid_at | string | no | ISO datetime — defaults to now |
notes | string | no | — |
Response — 200 OK
{ "success": true, "data": { "invoice": { "id": 4521, "status": "paid", "paid_amount": 27.50, "balance": 0.00, "paid_at": "2026-05-12T11:30:00Z" }, "payment": { "id": 8812, "amount": 27.50, "method": "cash" }, "transaction_id": 99231 }}A matching row is written to transactions (so the Dashboard revenue cards update) and to invoice_payments (for the audit trail and PDF receipt).
Errors: 400 amount exceeds balance, 409 invoice is void.
GET /api/invoices/:id/payments
Section titled “GET /api/invoices/:id/payments”Return all payment rows for this invoice. Used by the PDF template and the operator UI.
POST /api/invoices/quick-pay/:subscriber_id
Section titled “POST /api/invoices/quick-pay/:subscriber_id”Pro account-ledger pattern. The customer hands over one amount; the system applies it FIFO across all the subscriber’s pending invoices — oldest first — until the amount is exhausted. A partial payment is recorded on whichever invoice the amount runs out on.
Permission: admin or reseller.
Body
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | yes | Amount the customer paid |
method | string | no | As above |
reference / notes | string | no | — |
Response — 200 OK
{ "success": true, "applied": [ { "invoice_id": 4400, "amount": 25.00 }, { "invoice_id": 4521, "amount": 15.00 } ], "remaining": 0.00}If amount exceeds the total open balance, the excess is credited to the subscriber’s wallet (and remaining shows the wallet credit applied).
POST /api/invoices/consolidate/:subscriber_id
Section titled “POST /api/invoices/consolidate/:subscriber_id”Create one merged invoice that sums every pending invoice for the subscriber, then void the originals. No payment is recorded — the operator can then POST /:id/payment once on the consolidated invoice.
Permission: admin or reseller.
Body
| Field | Type | Required | Description |
|---|---|---|---|
notes | string | no | Rendered on the merged PDF |
Response — 200 OK
{ "success": true, "data": { "consolidated_invoice": { "id": 4901, "total": 75.00, "...": "..." }, "voided_invoice_ids": [4400, 4452, 4521] }}POST /api/invoices/:id/send-whatsapp · /send-combined
Section titled “POST /api/invoices/:id/send-whatsapp · /send-combined”Send the invoice PDF over WhatsApp to the subscriber’s phone. Requires WhatsApp configured in Settings → Notifications (Ultramsg or ProxRad WA account linked).
/send-combined is the consolidated variant — it merges pending invoices in memory, renders one PDF, and sends without touching the DB.
Permission: admin or reseller.
GET /api/invoices/:id/pdf (JWT-authenticated)
Section titled “GET /api/invoices/:id/pdf (JWT-authenticated)”Stream the PDF, generated on the fly by headless Chromium from the same HTML template used by the public view.
Response — 200 OK — Content-Type: application/pdf. Stream is gzip-friendly but inherently binary.
Query params: ?paper=A4 (or A5, Letter, Legal, A3, B4, B5, etc. — 17 paper sizes supported). Default is the system-wide default_paper_size.
GET /api/invoices/public/:id (Public — no auth)
Section titled “GET /api/invoices/public/:id (Public — no auth)”The customer-facing invoice view. The QR code printed on every PDF encodes this URL. The handler returns a clean HTML page styled with the operator’s branding, including a download link to /pdf and an embedded “How to pay” block.
This endpoint is rate-limited at the nginx layer to 30 req/min/IP.
curl https://panel.example.com/api/invoices/public/4521The :id is the internal id rather than the invoice_number. Sequential ids are intentional — the QR codes are too large to fit a UUID — but enumeration is mitigated by the per-IP rate limit and the fact that the page is read-only.
GET /api/invoices/public/:id/pdf (Public — no auth)
Section titled “GET /api/invoices/public/:id/pdf (Public — no auth)”The PDF variant. Useful for printing without logging in. Same paper-size query param as the JWT-authenticated version.
GET /api/invoices/commissions
Section titled “GET /api/invoices/commissions”Commission breakdown for the caller’s reseller (or for any reseller in admin scope).
Permission: any authenticated user.
Query
| Param | Type | Description |
|---|---|---|
reseller_id | int | Admin scope only |
from / to | string | ISO date range on issue_date |
status | string | Default paid — only paid invoices count toward earned commission |
Response — 200 OK
{ "success": true, "data": { "total_commission": 1245.00, "total_invoiced": 12450.00, "rate": 10.0, "rows": [ { "month": "2026-04", "invoiced": 6200.00, "commission": 620.00 }, { "month": "2026-05", "invoiced": 6250.00, "commission": 625.00 } ] }}The commission rate is taken from resellers.commission_rate. Pending invoices contribute commission_amount but are not summed in total_commission (that only counts paid invoices, configurable via commission_basis system setting).
POST /api/invoices/prorate
Section titled “POST /api/invoices/prorate”Pre-compute a prorated amount before changing a subscriber’s service. Mirrors GET /api/subscribers/:id/calculate-change-service-price from a billing-first perspective.
Permission: admin or reseller.
Body: { "subscriber_id": 102, "new_service_id": 8 }
Response — 200 OK
{ "success": true, "data": { "old_unused_credit": 12.50, "new_service_charge": 25.00, "net_amount_due": 12.50, "days_remaining": 15 }}Errors
Section titled “Errors”{ "success": false, "message": "amount exceeds balance" }| Status | Meaning |
|---|---|
| 400 | Validation — message describes |
| 401 | Missing / invalid JWT |
| 403 | Permission denied or wrong reseller scope |
| 404 | Invoice id not found |
| 409 | State conflict — paid invoice cannot be edited, void cannot be paid, etc. |
Rate limits
Section titled “Rate limits”- 300 req/min/IP on the JWT surface.
- 30 req/min/IP on
/public/*(nginx layer).
PDF generation is the slowest path — headless Chromium takes 600–1500 ms per render. The endpoint queues if more than 4 PDFs are rendering simultaneously per API container (MAX_CHROMIUM_WORKERS).
Related pages
Section titled “Related pages”- Endpoints — Subscribers — invoices belong to subscribers
- Billing & Invoices — UI for the same surface
- Settings → Notifications — WhatsApp config required for
/send-whatsapp - Reports → Revenue — aggregates of paid invoices