Skip to content

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.

https://your-panel-host/api/invoices

Public (no-auth) URLs additionally live under:

https://your-panel-host/api/invoices/public/:id
https://your-panel-host/api/invoices/public/:id/pdf

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.

{
"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.

List invoices.

Permission: any authenticated user (results are scope-filtered).

Query parameters

ParamTypeDescription
statusstringpending, partial, paid, overdue, void
subscriber_idintOne subscriber’s invoices
reseller_idintAdmin scope filter
from / tostringISO date range on issue_date
searchstringMatch on invoice_number, subscriber username/full_name
page / limitintDefault 1 / 50, max 500

Response — 200 OK

{
"success": true,
"data": [ { "id": 4521, "...": "..." } ],
"pagination": { "page": 1, "limit": 50, "total": 318 }
}
Terminal window
curl "https://panel.example.com/api/invoices?status=pending&limit=20" \
-H "Authorization: Bearer ..."

Return one invoice with full item lines and payment history.

Permission: any authenticated user (scope-filtered).

Errors: 404 invoice not found, 403 not authorized.

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

FieldTypeRequiredDescription
subscriber_idintyesFK
itemsarrayyesEach { description, qty, unit_price }
tax_ratenumberno% — defaults to system setting default_tax_rate
due_datestringnoISO date — defaults to today + default_due_days
notesstringnoFree text rendered on PDF
currencystringno3-letter ISO; defaults to system currency

Response — 201 Created — full invoice object including computed amount, tax_amount, total, invoice_number, commission_rate, commission_amount.

Terminal window
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"
}'

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.

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

FieldTypeRequiredDescription
amountnumberyesMust be ≤ remaining balance
methodstringnocash, card, bank, wallet, prepaid_card, other
referencestringnoOperator-supplied reference (cheque #, txn id)
paid_atstringnoISO datetime — defaults to now
notesstringno

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.

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

FieldTypeRequiredDescription
amountnumberyesAmount the customer paid
methodstringnoAs above
reference / notesstringno

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

FieldTypeRequiredDescription
notesstringnoRendered 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 OKContent-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.

Terminal window
curl https://panel.example.com/api/invoices/public/4521

The :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.

Commission breakdown for the caller’s reseller (or for any reseller in admin scope).

Permission: any authenticated user.

Query

ParamTypeDescription
reseller_idintAdmin scope only
from / tostringISO date range on issue_date
statusstringDefault 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).

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
}
}
{ "success": false, "message": "amount exceeds balance" }
StatusMeaning
400Validation — message describes
401Missing / invalid JWT
403Permission denied or wrong reseller scope
404Invoice id not found
409State conflict — paid invoice cannot be edited, void cannot be paid, etc.
  • 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).