Invoice Items and Payments
The four endpoints that edit and split invoice lines and hold the payment records.
Overview
These four endpoints run the money side of an invoice. Two deal with its lines: editing them and moving some onto an invoice of their own. 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 one way only. Clearing the balance turns it paid, while removing a payment does not turn it back.
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',
]);Recording a Payment
Adds a payment record to an invoice by hand.
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":"Balance","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: 'Balance',
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' => 'Balance',
'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' => 'Balance',
'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);// The invoice stays PAID: the balance reopens, and turning the status back is your job.
Api::Invoices()->DeleteInvoicePayment(['id' => $id, 'payment_id' => $pid]);
Api::Invoices()->UpdateInvoiceStatus(['id' => $id, 'status' => 'unpaid']);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.
Clearing the balance turns an invoice paid by itself, yet removing the payment does not walk that back. What was paid and what remains are worked out again while the status stays paid. When undoing a payment entered by mistake, turn the status back to unpaid as a separate step.
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.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.