Managing Invoices

10 vues Markdown

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

get/api/v1/admin/invoices
Invoices/GetInvoices admin

Returns invoices, filtered and paged.

Query 15
pageintWhich page.
limitintRecords per page. A value out of range falls back to the default.
searchstringSearches the invoice number and the client details.
statusstringFilters by status: waiting, unpaid, paid, cancelled, refund. It also takes derived filters such as overdue, upcoming, formalised and not formalised.
client_idintFilters by client.
currency_idintFilters by currency.
taxedboolSeparates the formalised ones.
amountstringFilters by amount.
amount_opstringWhich way the amount comparison runs.
item_descriptionstringSearches the line descriptions.
cdatestringFilters by the date issued.
cdate_opstringWhich way that comparison runs.
duedatestringFilters by the due date.
duedate_opstringWhich way that comparison runs.
daterangestringFilters between two dates. Its direction comes from its own comparison field.
Response fields data[] — 14 + meta — 4
idintThe invoice id.
numberstring | nullThe invoice number. Empty until a number is given.
statusstringThe invoice status: waiting, unpaid, paid, cancelled, refund.
currency_idintThe invoice currency.
subtotalfloatThe subtotal.
taxfloatThe tax.
totalfloatThe grand total.
formalizedboolWhether it was turned into a formal invoice.
payment_methodstring | nullThe payment method.
created_atstring | nullWhen it was issued.
due_datestring | nullWhen it falls due.
paid_atstring | nullWhen it was paid.
refund_datestring | nullWhen it was refunded.
clientobjectThe client: id, name, company and e-mail. As they stood when the invoice was issued.
total intHow many match the filter. It comes back under meta.
pageintThe page you are on.
limitintThe page size.
next_pageintThe next page. Zero means you are on the last one.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/invoices
Invoices/CreateInvoice admin the notice is optional

Issues an invoice to a client by hand and writes its lines.

Body 8
client_idintreqThe client the invoice goes to.
itemsarrayreqThe invoice lines. At least one line is needed.
currency_idintThe invoice currency. Left out, the client's own is used.
statusstringThe status it opens with. Unpaid by default.
payment_methodstringThe payment method.
created_atstringThe date issued. Left out, the moment of the call is used.
due_datestringThe due date. Left out, the end of today is used.
send_notificationboolSends the client the notice that suits the status.
Line fields items[] — 8
descriptionstringreqWhat the line is for.
quantityintreqHow many. One at the least.
amountfloatreqThe unit amount.
discountfloatThe discount value.
discount_typestringWhether the discount is an amount or a share.
tax_ratefloatA tax rate for this line.
tax_exemptboolKeeps the line free of tax.
user_pidintThe service it belongs to. Zero says the line stands alone.
Response fields 201 — data
dataobjectThe invoice issued. Same shape as the detail endpoint.
Errors 6
invalid_client422No client was given.
not_found404No such client.
currency_required422No currency could be resolved.
items_required422No line was given.
create_failed422The invoice could not be issued.
insufficient_scope403The key lacks the required scope.
Request
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

get/api/v1/admin/invoices/stats
Invoices/GetInvoiceStats admin

Returns the total for the type and period you pick.

Query 2
typestringWhat gets added up: unpaid, paid, overdue or tax. The unpaid ones by default.
periodstringWhich period: the last seven days, the last fifteen, this month, last month, this year or last year. This month by default.
Response fields data — 5
typestringThe type applied.
periodstringThe period applied.
amountfloatThe total. A raw number, with other currencies converted into the local one before adding.
countintHow many invoices went into it.
currency_idintThe currency the total is in.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
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

get/api/v1/admin/invoices/{id}
Invoices/GetInvoice admin

Returns an invoice with its lines, payments and the client snapshot.

Response fields data — 33
idintThe invoice id.
numberstring | nullThe invoice number.
statusstringThe invoice status: waiting, unpaid, paid, cancelled, refund.
currency_idintThe invoice currency.
created_atstring | nullWhen it was issued.
due_datestring | nullWhen it falls due.
paid_atstring | nullWhen it was paid.
refund_datestring | nullWhen it was refunded.
subtotalfloatThe subtotal.
taxfloatThe tax. Commission and surcharge taxes do not enter here.
additional_taxfloatThe additional tax.
totalfloatThe grand total.
total_paidfloatWhat was paid, worked out from the payment records.
balancefloatWhat remains owing.
tax_ratefloatThe tax rate.
taxation_typestringWhether tax sits inside the amount or outside it.
formalizedboolWhether it was turned into a formal invoice.
taxfreeboolWhether it is free of tax.
localboolWhether it is a local invoice.
recurringboolWhether it repeats.
recurring_timeintHow often it repeats.
recurring_periodstringThe period it repeats over.
payment_methodstring | nullThe payment method.
payment_method_statusstring | nullThe state of the payment method.
payment_method_dataobjectWhat the payment method carries with it.
payment_method_commissionfloatThe payment commission.
payment_method_commission_ratefloatThe commission rate.
payment_method_commission_taxfloatThe tax on the commission. It enters the grand total and not the tax field.
installment_surchargefloatThe instalment surcharge.
installment_surcharge_taxfloatThe tax on the surcharge. This too enters the grand total and not the tax field.
installment_countintHow many instalments.
send_bill_to_addressfloatThe charge for posting the bill.
notesstringThe note on the invoice.
unreadboolWhether the client has yet to read it.
exchange_ratefloatThe rate at the time of issue.
discountsobjectThe discount lines and their total.
user_dataobjectThe client and their address as they stood at issue.
clientobjectThe client: id, name, company and e-mail.
itemsarrayThe invoice lines.
paymentsarrayThe payment records.
Line fields items[] — 17
idintThe line id.
parent_idintThe parent line. Sub-lines hang off a line above.
owner_idintThe invoice it belongs to.
client_idintThe client.
service_idintThe service it covers. Zero says the line stands alone.
descriptionstringWhat the line is for.
quantityintHow many.
tax_exemptboolWhether it is free of tax.
tax_ratefloatThe line's tax rate. Minus one says the rate comes from the invoice.
additional_taxesarrayThe extra taxes on the line.
additional_tax_totalfloatWhat those add up to.
amountfloatThe unit amount.
total_amountfloatThe line total. Quantity times amount, less the discount.
currency_idintThe line currency.
due_datestring | nullThe end of the line's period.
rankintWhere it sits on the invoice.
optionsarrayExtra details on the line.
Payment fields payments[] — 14
idintThe payment id.
amount_infloatWhat came in.
amount_outfloatWhat went out.
feesfloatThe fee taken.
currency_idintThe payment currency.
ratefloatThe rate at the time of payment.
payment_methodstring | nullThe payment method.
transaction_idstring | nullThe transaction number.
descriptionstringWhat the payment was.
paid_atstring | nullWhen it was paid.
created_atstring | nullWhen the record was made.
created_byintThe staff member who made it.
recorded_bystring | nullTheir name.
ipstring | nullThe address it came from.
Errors 2
not_found404No such invoice.
insufficient_scope403The key lacks the required scope.
Request
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

patch/api/v1/admin/invoices/{id}
Invoices/UpdateInvoice admin

Changes an invoice's header fields, recomputing the totals when needed.

Body 11
numberstringThe invoice number.
payment_methodstringThe payment method.
taxation_typestringWhether tax sits inside the amount. Changing it recomputes the totals.
currency_idintThe invoice currency. Changing it recomputes the totals.
tax_ratefloatThe tax rate.
formalizedboolTurns it into a formal invoice. The first time it does, the client is told.
taxfreeboolMakes the invoice free of tax. It takes precedence over formalising.
created_atstringThe date issued.
due_datestringThe due date.
paid_atstringThe date paid.
refund_datestringThe date refunded.
Response fields data
dataobjectThe invoice as it now stands. Same shape as the detail endpoint.
Errors 2
not_found404No such invoice.
insufficient_scope403The key lacks the required scope.
Request
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

delete/api/v1/admin/invoices/{id}
Invoices/DeleteInvoice admin unwinds a chain of records

Removes an invoice and every record hanging off it.

Response fields data — 2
deletedboolWhether the delete ran.
idintThe id of the invoice removed.
Errors 3
not_found404No such invoice.
blocked_by_gate422A hook refused the delete.
insufficient_scope403The key lacks the required scope.
Request
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 tax field does not hold every tax

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.

Draft invoices are not in the list

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.

Formal and tax-free share one field

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.

A delete unwinds the chain

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.

Empty dates come back empty

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.

Cet article vous a-t-il été utile ?

Merci pour votre retour !

Besoin d'aide supplémentaire ?

Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.