Managing Invoices
The six endpoints that issue, read and edit invoices and give the revenue summary.
Overview
These six endpoints deal with the invoice itself: listing, issuing, reading, editing its header fields and deleting it. A revenue summary giving a period total sits alongside them.
An invoice carries a snapshot: the client's name, address and tax details are held inside the record as they stood at issue. A client changing their address later leaves the invoice untouched, because a document once issued tells the story of a moment.
The money has three layers: the lines give the subtotal, the tax what sits on top of it, and the commission and surcharge what the payment method brought. Together the three make the grand total.
Reference
Listing the Invoices
Returns invoices, filtered and paged.
waiting, unpaid, paid, cancelled, refund. It also takes derived filters such as overdue, upcoming, formalised and not formalised.waiting, unpaid, paid, cancelled, refund.curl 'https://panel.example.com/api/v1/admin/invoices?status=unpaid&limit=25' \
-H "Authorization: Bearer $API_KEY"const url = new URL('https://panel.example.com/api/v1/admin/invoices');
url.searchParams.set('status', 'unpaid');
const res = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/invoices?' . http_build_query(['status' => 'unpaid', 'limit' => 25]));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Draft and system invoices NEVER enter this list; the count will not match the table.
$rows = Api::Invoices()->GetInvoices([], ['status' => 'unpaid'])['data'];Issuing an Invoice
Issues an invoice to a client by hand and writes its lines.
curl -X POST 'https://panel.example.com/api/v1/admin/invoices' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"client_id":87,"currency_id":840,"items":[{"description":"Hosting","quantity":1,"amount":12.5}]}'const res = await fetch('https://panel.example.com/api/v1/admin/invoices', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
client_id: 87,
currency_id: 840,
due_date: '2026-07-01',
items: [
{ description: 'Hosting Plan', quantity: 1, amount: 12.5 },
],
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/invoices');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'client_id' => 87,
'currency_id' => 840,
'items' => [
['description' => 'Hosting Plan', 'quantity' => 1, 'amount' => 12.5],
],
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The notice is OPTIONAL: leave it out and the client never hears about the invoice.
Api::Invoices()->CreateInvoice([
'client_id' => $uid,
'items' => $lines,
'send_notification' => true,
]);Reading the Revenue Summary
Returns the total for the type and period you pick.
curl 'https://panel.example.com/api/v1/admin/invoices/stats?type=unpaid&period=this-month' \
-H "Authorization: Bearer $API_KEY"const url = new URL('https://panel.example.com/api/v1/admin/invoices/stats');
url.searchParams.set('type', 'unpaid');
url.searchParams.set('period', 'this-month');
const res = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/stats?' . http_build_query(['type' => 'unpaid', 'period' => 'this-month']));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The amount is a CONVERTED total, worked out at the day's rate, and you format it yourself.
$s = Api::Invoices()->GetInvoiceStats([], ['type' => 'unpaid'])['data'];
$sum = Money::formatter_symbol($s['amount'], $s['currency_id']);Reading One Invoice
Returns an invoice with its lines, payments and the client snapshot.
waiting, unpaid, paid, cancelled, refund.curl 'https://panel.example.com/api/v1/admin/invoices/1212' \
-H "Authorization: Bearer $API_KEY"const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/${id}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/' . $id);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// When REBUILDING the total, add the two taxes yourself; the tax field leaves them out.
$inv = Api::Invoices()->GetInvoice(['id' => $id])['data'];
$sum = $inv['subtotal'] + $inv['tax']
+ $inv['payment_method_commission'] + $inv['payment_method_commission_tax']
+ $inv['installment_surcharge'] + $inv['installment_surcharge_tax'];Editing an Invoice
Changes an invoice's header fields, recomputing the totals when needed.
curl -X PATCH 'https://panel.example.com/api/v1/admin/invoices/1212' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"number":"INV-2026-0042","tax_rate":20}'const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/${id}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
number: 'INV-2026-0042',
tax_rate: 20,
due_date: '2026-07-15 23:59:00',
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/' . $id);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'number' => 'INV-2026-0042',
'tax_rate' => 20,
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Making it tax-free OVERRIDES formalising; sending both together makes no sense.
Api::Invoices()->UpdateInvoice(['id' => $id, 'formalized' => true]);Deleting an Invoice
Removes an invoice and every record hanging off it.
curl -X DELETE 'https://panel.example.com/api/v1/admin/invoices/1226' \
-H "Authorization: Bearer $API_KEY"const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/' . $id);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Cancel it rather than delete: the accounting trail stays and it leaves what is owed.
Api::Invoices()->SetInvoiceStatus(['id' => $id, 'status' => 'cancelled']);Pitfalls
The taxes on the payment commission and the instalment surcharge enter the grand total and not the tax field. Add the subtotal to the tax expecting the grand total and the difference has no obvious source. Rebuilding the total means adding those two taxes from their own fields.
The listing endpoint hides draft and system invoices. That matches what the panel shows, yet the count from the list will not match the rows in the database. Weigh that when writing an accounting report; adding a filter does not bring the hidden ones back.
An invoice's tax state is one field taking one of three values: normal, formal or tax-free. Making it tax-free overrides formalising, and sending both together means nothing. The first time formalising is applied, the client is told as well.
Deleting an invoice does more than remove the record: it also unwinds the lines, the income and expense entries, the links to services and any pending metered rows. A hook can refuse the delete, because some invoices leaving would break the books. To back out, cancel rather than delete: the trail stays and the debt goes.
Fields for things that have not happened, such as the paid and refund dates, come back empty. The database keeps placeholder dates there and the response clears them. Check a date exists before reading it, or you end up with a screen treating a placeholder as real.
Related Articles
Merci pour votre retour !
Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.