Invoices

6 views Markdown

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

get/api/v1/client/invoices
Invoices/GetInvoices urgency order

Returns the account's invoices in order of urgency.

Query 6
pageintWhich page.
limitintRows per page. 100 at the most.
statusstringThe state filter. It takes the customer-facing state vocabulary.
searchstringSearches the invoice number and the first line's description.
sortstringThe sort field: raised, due or amount. Left out, the urgency order stands.
dirstringThe sort direction. It works with the sort field alone.
Response fields data[] — 9 + meta — 4
invoice_idintThe invoice id.
numberstringThe invoice number shown.
statusstringThe raw state.
statestringThe state shown to the customer. It comes from the raw state and the due date, and an open invoice past its date takes a value of its own.
totalobjectThe document total. It is in its own currency.
created_atstringThe day it was raised.
paid_atstringThe day it was paid.
due_datestringThe day it falls due.
first_itemstringThe first line's description. It says quickly what the invoice is for.
total_countintHow many invoices there are. It comes back under meta as the total.
pageintThe page you are on.
limitintThe page size.
next_pageintThe next page.
Errors 2
status_invalid422The state is not one that is known.
insufficient_scope403The key lacks the required scope.
Request
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

get/api/v1/client/invoices/{id}
Invoices/GetInvoice the moment is kept

Returns an invoice's lines, customer record, summary and payments.

Response fields data — 17
invoice_idintThe invoice id.
numberstringThe invoice number shown.
statusstringThe raw state.
statestringThe state shown to the customer. It comes from the raw state and the due date, and an open invoice past its date takes a value of its own.
totalobjectThe document total. It is in its own currency.
created_atstringThe day it was raised.
paid_atstringThe day it was paid.
due_datestringThe day it falls due.
amount_dueobjectWhat is still open. It reaches zero once closed.
payableboolWhether the payment endpoint takes it.
subscription_lockedboolWhether a subscription is collecting it. Paying by hand is refused when it is.
notesstringThe operator notes on it.
billed_toobjectThe customer record kept on the invoice. It carries the details as they stood when it was raised, even after the profile moves: the kind, name, contact, e-mail, tax number, identity number and address.
custom_fieldsobject[]The extra fields the operator chose to show on the invoice.
itemsobject[]The invoice lines.
item_idintThe line id.
parent_item_idintThe parent line. It fills on an add-on line.
descriptionstringThe line description.
eventstringThe flow that wrote it. It comes empty on a line entered by hand.
service_idintThe service billed.
cyclestringThe billing cycle.
period_startstringThe start of the period covered. It can be empty on an older document.
period_endstringThe end of the period covered.
domainstringThe domain the line concerns.
quantityintHow many.
unit_priceobjectThe unit amount.
totalobjectThe line total.
summaryobjectThe money summary.
subtotalobjectThe line subtotal.
discountsobjectThe discounts. It carries the dealer discount and any coupon applied.
taxobjectThe tax line. It carries a rate and an amount.
payment_feeobjectThe fee of the method picked.
installmentobjectThe instalment plan. It carries the count and the surcharge.
totalobjectThe document total.
paidobjectWhat has been paid so far.
amount_dueobjectWhat is still open.
transactionsobject[]The money movements that closed. Each carries a kind, a method, the provider reference, an amount and a date, and failed attempts are not kept.
bank_transferobjectA transfer notice waiting for approval. It carries the bank name, the sender and a reference.
Errors 2
not_found404No such invoice, it belongs to another customer, or it is a draft.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/client/invoices/{id}/pay
Invoices/PayInvoice the balance or a saved card

Closes an open invoice with the balance or a saved card.

Body 1
paymentobjectreqThe payment source.
methodstringreqThe road: the account balance or a saved card.
card_idintThe card to charge. The account's default card is charged when left out.
Response fields data — 2
invoiceobjectThe invoice read afresh. It carries the number, state, total, what is open and the dates.
paymentobjectHow the charge went.
methodstringThe road used.
statusstringWhether it paid or failed. A failure happens on a declined card alone.
cardobjectThe id and last four digits of the card charged.
transaction_idstringThe provider's transaction reference.
errorstringWhy it was declined.
Errors 10
not_found404No such invoice, it belongs to another customer, or it is a draft.
invoice_not_payable422The invoice is not open or nothing is left to pay.
subscription_collects422A subscription is collecting this invoice.
payment_method_invalid422The payment road is neither of the two values.
balance_not_allowed422A top-up invoice cannot be paid from the wallet.
insufficient_balance422The wallet does not cover the amount with the fee. What is needed and what is there come in the answer's detail.
no_stored_card422No card was named and there is no default.
card_expired422The saved card has expired.
card_not_chargeable422The card's provider wants a browser step.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/client/invoices/{id}/coupon
Invoices/ApplyInvoiceCoupon one coupon per document

Applies a coupon to an open invoice and prices the document again.

Body 1
codestringreqThe coupon code.
Response fields data — 3
invoiceobjectThe document priced again.
couponobjectThe coupon applied.
discountobjectThe discount granted on this document.
Errors 8
not_found404No such invoice, it belongs to another customer, or it is a draft.
invoice_not_payable422The invoice is not open.
coupon_code_required422No code was sent.
coupon_invalid422No such coupon.
coupon_not_invoice422The coupon cannot be used on invoices.
coupon_used422A coupon was already applied to this document.
coupon_scope422The coupon does not apply to the lines on this invoice.
coupon_rejected422The coupon engine refused it. The reason comes in the message.
Request
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

A declined card returns no error

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.

A subscription collecting it refuses a manual payment

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.

The customer details on an invoice belong to the moment

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 coupon per document, and before paying

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.

A top-up invoice is not paid from the wallet

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 two state fields do not say the same thing

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.

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.