Invoices
The four endpoints that see, read, pay and discount invoices.
Overview
An invoice is the account's record of what is owed. An order, a renewal and a wallet top-up all raise one, and payment goes through it.
Every invoice carries two states: the raw one on the record and the one shown to the customer. The second takes the due date into account, so an unpaid invoice takes a separate value once that date passes.
Payment is kept narrow here as well: the account balance or a saved card. The roads wanting a browser step, a part payment and a transfer notice stay in the panel.
Reference
Listing the Invoices
Returns the account's invoices in order of urgency.
curl 'https://panel.example.com/api/v1/client/invoices?status=overdue' \
-H "Authorization: Bearer $CLIENT_KEY"const res = await fetch('https://panel.example.com/api/v1/client/invoices?status=overdue', {
headers: { Authorization: `Bearer ${clientKey}` },
});
const { data, meta } = await res.json();$ch = curl_init('https://panel.example.com/api/v1/client/invoices?status=overdue');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $clientKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Two state fields exist: the filter takes the CUSTOMER one, and the raw state does not separate overdue.
$rows = Kernel::internal('client:Invoices/GetInvoices',
['owner_id' => $uid, 'status' => 'overdue'])['data'];Reading an Invoice
Returns an invoice's lines, customer record, summary and payments.
curl 'https://panel.example.com/api/v1/client/invoices/1193' \
-H "Authorization: Bearer $CLIENT_KEY"const res = await fetch(`https://panel.example.com/api/v1/client/invoices/${id}`, {
headers: { Authorization: `Bearer ${clientKey}` },
});
const { data } = await res.json();
if (data.payable && ! data.subscription_locked) enablePayButton();$ch = curl_init('https://panel.example.com/api/v1/client/invoices/' . $id);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $clientKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The customer record on an invoice is a snapshot of THE MOMENT it was raised; do not reconcile it with the profile today.
$inv = Kernel::internal('client:Invoices/GetInvoice', ['owner_id' => $uid, 'id' => $id])['data'];
$thenAddress = $inv['billed_to']['address'];Paying an Invoice
Closes an open invoice with the balance or a saved card.
curl -X POST 'https://panel.example.com/api/v1/client/invoices/1193/pay' \
-H "Authorization: Bearer $CLIENT_KEY" \
-H 'Content-Type: application/json' \
-d '{"payment":{"method":"balance"}}'const res = await fetch(`https://panel.example.com/api/v1/client/invoices/${id}/pay`, {
method: 'POST',
headers: {
Authorization: `Bearer ${clientKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ payment: { method: 'card' } }),
});
const { data } = await res.json();
if (data.payment.status === 'failed') showError(data.payment.error);$ch = curl_init('https://panel.example.com/api/v1/client/invoices/' . $id . '/pay');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $clientKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['payment' => ['method' => 'balance']]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// A DECLINED CARD is not an error: the answer succeeds, the invoice stays open with its fee, so read the state.
$r = Kernel::internal('client:Invoices/PayInvoice',
['owner_id' => $uid, 'id' => $id, 'payment' => ['method' => 'card']])['data'];
if ($r['payment']['status'] === 'failed') $retryLater($id);Applying a Coupon to an Invoice
Applies a coupon to an open invoice and prices the document again.
curl -X POST 'https://panel.example.com/api/v1/client/invoices/1193/coupon' \
-H "Authorization: Bearer $CLIENT_KEY" \
-H 'Content-Type: application/json' \
-d '{"code":"WELCOME10"}'const res = await fetch(`https://panel.example.com/api/v1/client/invoices/${id}/coupon`, {
method: 'POST',
headers: {
Authorization: `Bearer ${clientKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ code }),
});
const { data } = await res.json();
showNewTotal(data.invoice.total, data.discount);$ch = curl_init('https://panel.example.com/api/v1/client/invoices/' . $id . '/coupon');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $clientKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['code' => $code]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Apply the coupon BEFORE paying: a closed document cannot be changed and there is no way back.
Kernel::internal('client:Invoices/ApplyInvoiceCoupon',
['owner_id' => $uid, 'id' => $id, 'code' => $code]);
Kernel::internal('client:Invoices/PayInvoice',
['owner_id' => $uid, 'id' => $id, 'payment' => ['method' => 'balance']]);Pitfalls
The payment endpoint answers successfully on a declined card as well: the invoice stays open and the fee of the method picked stays on the document. Read the outcome from the payment state in the answer. Take that fee into account when paying the same invoice another way.
Where a payment provider subscription collects an invoice the payment endpoint refuses. That is a gate against a double charge rather than a fault. Read the subscription field in the detail and close the pay button; paying by hand wants the subscription cancelled first.
An invoice keeps the customer record on itself: the address, tax number and title stand as they were when it was raised. The document does not move when the profile does, because it is a legal record. Do not report a mismatch by comparing it with the profile today.
One invoice takes one coupon, and only while the document is open. Applying one after payment is refused and there is no way back. Try the coupon before the payment call.
The invoice raised to put money into the wallet cannot be paid from the wallet, since that would be circular. It is paid with a saved card or from the panel. Handle that error apart when writing a general payment flow.
The raw state is the value on the record while the customer-facing one takes the due date in. Only the second moves when an unpaid invoice passes its date. The filter takes the customer state, so the overdue ones cannot be found by the raw one.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.