Invoice Items and Payments
The five endpoints that edit, split and merge invoice lines and hold the payment records.
Overview
These five endpoints run the money side of an invoice. Three deal with its lines: editing them, moving some onto an invoice of their own, and folding other invoices into it. The other two deal with payment records.
The line endpoint works in bulk: one call adds, updates and removes. When it is done the totals are worked out again. The subtotal, the tax and the grand total are never written by hand.
A payment record moves the invoice status both ways. Clearing the balance turns it paid. Removing a payment the remaining ones no longer cover turns it back to unpaid.
Reference
Writing the Lines
Adds, updates and removes an invoice's lines in one call.
curl -X PUT 'https://panel.example.com/api/v1/admin/invoices/1212/items' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"items":[{"item_id":2811,"description":"Hosting","quantity":1,"amount":12.5}],"deleted_ids":[2812]}'const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/${id}/items`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: [
{ item_id: 2811, description: 'Hosting Plan', quantity: 1, amount: 12.5 },
{ description: 'Setup Fee', quantity: 1, amount: 5 },
],
deleted_ids: [2812],
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/' . $id . '/items');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'items' => [
['item_id' => 2811, 'description' => 'Hosting Plan', 'quantity' => 1, 'amount' => 12.5],
['description' => 'Setup Fee', 'quantity' => 1, 'amount' => 5],
],
'deleted_ids' => [2812],
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// A line you leave out is NOT removed; to remove one, put its id in the deleted list.
Api::Invoices()->UpdateInvoiceItems([
'id' => $id,
'items' => [['item_id' => 2811, 'description' => 'Hosting', 'quantity' => 1, 'amount' => 12.5]],
'deleted_ids' => [2812],
]);Splitting the Lines
Moves the lines you pick onto a new invoice.
curl -X POST 'https://panel.example.com/api/v1/admin/invoices/1228/items/split' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"item_ids":[2835]}'const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/${id}/items/split`, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ item_ids: [2835] }),
});
const { data } = await res.json();
console.log(data.new_invoice_id);$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/' . $id . '/items/split');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['item_ids' => [2835]]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The new invoice opens UNPAID and the client is NOT told about it by itself.
$r = Api::Invoices()->SplitInvoiceItems(['id' => $id, 'item_ids' => $picked])['data'];
Api::Invoices()->SendInvoiceNotification([
'id' => $r['new_invoice_id'], 'template' => 'invoice-created',
]);Merging Invoices
Folds other unpaid invoices of the same client into the invoice in the path.
Each merge_* refusal names the invoices that blocked it in details.invoice_ids. Every check runs first, so a refusal leaves all invoices as they were.
curl -X POST 'https://panel.example.com/api/v1/admin/invoices/1212/merge' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"invoice_ids":[1229,1235]}'const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/${id}/merge`, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ invoice_ids: [1229, 1235] }),
});
const body = await res.json();
if (!res.ok) console.log(body.error.code, body.error.details?.invoice_ids);$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/' . $id . '/merge');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['invoice_ids' => [1229, 1235]]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// A refusal comes back as an error envelope and leaves every invoice as it was.
$r = Api::Invoices()->MergeInvoices(['id' => $mainId, 'invoice_ids' => $others]);
$gone = $r['meta']['merged_invoice_ids'] ?? [];Recording a Payment
Adds a payment record to an invoice by hand.
Balance takes the amount from the client's account balance; a balance that does not cover it refuses the payment. On an open invoice with no payment yet, the invoice is first repriced with this method's current commission rate and takes this method, part payments included; what is still owed follows the new total.Balance and the client's account balance does not cover the amount. The details carry what is required and what is available, in the balance currency.curl -X POST 'https://panel.example.com/api/v1/admin/invoices/1212/payments' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"amount":12.5,"payment_method":"BankTransfer","transaction_id":"TXN-1042"}'const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/${id}/payments`, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 12.5,
payment_method: 'BankTransfer',
currency_id: 840,
transaction_id: 'TXN-1042',
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/' . $id . '/payments');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 12.5,
'payment_method' => 'BankTransfer',
'transaction_id' => 'TXN-1042',
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The transaction number guards against DOUBLES; retrying after a network error is safe.
Api::Invoices()->AddInvoicePayment([
'id' => $id,
'amount' => 12.5,
'payment_method' => 'BankTransfer',
'transaction_id' => $txn,
]);Removing a Payment
Takes a payment record off an invoice.
curl -X DELETE 'https://panel.example.com/api/v1/admin/invoices/1212/payments/31' \
-H "Authorization: Bearer $API_KEY"const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/${id}/payments/${paymentId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/' . $id . '/payments/' . $paymentId);
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);// A paid invoice the remaining payments no longer cover turns unpaid by itself.
Api::Invoices()->DeleteInvoicePayment(['id' => $id, 'payment_id' => $pid]);Pitfalls
A line you do not send on the write call stays put. Removing it means putting its id in the deleted list. A script that sends the line set as it stands will not remove what was taken out. Compare the two lists and write the difference into the deleted list.
Both the write and the split call want at least one line left on the invoice when they finish. Trying to delete every line, or to split them all away, gives an error. To be rid of the invoice, remove or cancel the invoice itself rather than its lines.
When the write call finishes, the subtotal, the tax and the grand total are worked out again. Trying to write a total through the invoice edit endpoint at the same time leaves a record pulled by two sources. Totals come from the lines, and changing them means changing the lines.
When the remaining payments no longer cover a paid invoice, removing a payment turns it back to unpaid by itself. An amount taken from the account balance goes back to it. A top-up invoice takes its loaded credit back too. Nothing else is walked back: extended service periods and activated orders stay as they are.
The split call opens a new unpaid invoice and tells the client nothing by itself. The client is left with another debt they have not heard of. After a split, take the new invoice id and send a notice. The source total drops as well, so both documents have changed.
The merge call keeps only the invoice in the path. The others are deleted once their lines have moved, so their ids stop working. Orders and services that pointed to them now point to the main invoice. Keep the ids from the response if your own records refer to them.
Related Articles
Vielen Dank für Ihre Rückmeldung!
Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.