Invoice Items and Payments

7 views Markdown

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

put/api/v1/admin/invoices/{id}/items
Invoices/UpdateInvoiceItems admin the totals are recomputed

Adds, updates and removes an invoice's lines in one call.

Body 3
itemsarrayThe lines to work on. One carrying an id gets updated, one without is added as new.
deleted_idsint[]The line ids to remove.
removed_discountsarrayThe discounts to take off. Each entry carries the discount type and the line it belongs to.
Line fields items[] — 10
item_idintThe id of an existing line. Left out, a new line is opened.
descriptionstringreqWhat the line is for.
quantityintreqHow many.
amountfloatreqThe unit amount.
discountfloatA discount on this line.
discount_typestringWhether the discount is an amount or a share.
tax_ratefloatA tax rate for this line. Left empty, the invoice's rate is used.
tax_exemptboolKeeps the line free of tax.
user_pidintThe service it covers.
oduedatestringThe end of the line's period.
Response fields data
dataobjectThe invoice as it now stands. Its totals arrive recomputed.
Errors 3
not_found404No such invoice.
item_required422No line would remain when the call is done.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/invoices/{id}/items/split
Invoices/SplitInvoiceItems admin opens a new invoice

Moves the lines you pick onto a new invoice.

Body 1
item_idsint[]reqThe lines to move. All have to belong to the source, and one line has to stay behind.
Response fields data — 4
source_invoice_idintThe source invoice id.
new_invoice_idintThe invoice opened.
new_invoice_numberstring | nullThe new invoice's number. Empty until a number is given.
moved_item_idsint[]The lines that moved.
Errors 6
not_found404No such invoice.
split_required422No line was picked.
no_items422The lines do not belong to this invoice.
split_one_left422No line would stay on the source.
split_failed422The new invoice could not be opened.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/invoices/{id}/payments
Invoices/AddInvoicePayment admin clears the balance to paid

Adds a payment record to an invoice by hand.

Body 7
amountfloatreqThe amount paid. It has to be above zero.
payment_methodstringreqHow the money came in.
currency_idintThe payment currency. It gets converted when it differs from the invoice.
transaction_idstringThe transaction number. A second record with the same one is refused.
descriptionstringWhat the payment was.
paid_atstringThe date it was paid.
feesfloatThe fee taken.
Response fields data + meta — 1
dataobjectThe invoice as it now stands. Once the balance clears, the status turns to paid.
payment_idintThe id of the payment record opened. It comes back under meta.
Errors 5
not_found404No such invoice.
invalid_amount422The amount is zero or below.
method_required422No payment method was given.
payment_rejected422The invoice is already paid, the transaction number repeats, or the currency is not valid.
insufficient_scope403The key lacks the required scope.
Request
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

delete/api/v1/admin/invoices/{id}/payments/{payment_id}
Invoices/DeleteInvoicePayment admin the status does not follow

Takes a payment record off an invoice.

Response fields data
dataobjectThe invoice as it now stands. What was paid and what remains are worked out again.
Errors 3
not_found404That payment is not on this invoice.
invalid_payment422The payment id is not valid.
insufficient_scope403The key lacks the required scope.
Request
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

Leaving a line out does not remove it

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.

An invoice cannot be left with no lines

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.

Do not write the totals by hand

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.

Removing a payment does not turn the status back

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.

A split brings a new debt into being

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.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.