Invoice Status and Notices

7 views Markdown

The five endpoints for an invoice's status, its client details and the notices sent.

Overview

These five endpoints run the life of an invoice once issued. They change its status, correct the client details, send a notice, formalise it and remind.

The status change is the heaviest thing here. Marking an invoice paid opens an income entry, gives it a paid-invoice number and sets the linked services moving; refunding winds that back and can order a refund through the payment module.

Three of the endpoints mail the client: the notice, the reminder and, when its file is there, formalising. Calling them from a script reaches real people.

Reference

Changing the Status

put/api/v1/admin/invoices/{id}/status
Invoices/UpdateInvoiceStatus admin it has side effects

Changes an invoice's status and carries out what that status brings.

Body 5
statusstringreqThe new status: paid, unpaid, refund or cancelled.
payment_methodstringThe payment method. On a paid invoice, sending this alone changes the method.
refund_methodstringHow the refund happens. It can go back through the payment module.
cancel_servicesboolCancels the services on the invoice as well. It works on refund and cancel.
notifyboolTells the client.
Response fields data
dataobjectThe invoice as it now stands. Same shape as the detail endpoint.
Errors 5
not_found404No such invoice.
invalid_status422The status is not recognised.
same_status422The invoice already stands there.
blocked_by_gate422A hook refused the change.
insufficient_scope403The key lacks the required scope.
Request
curl -X PUT 'https://panel.example.com/api/v1/admin/invoices/1212/status' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"status":"paid","payment_method":"Balance","notify":true}'
const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/${id}/status`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    status: 'paid',
    payment_method: 'Balance',
    notify: true,
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/' . $id . '/status');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'status'         => 'paid',
        'payment_method' => 'Balance',
        'notify'         => true,
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Marking it paid OPENS AN INCOME ENTRY, gives a number and sets the services moving.
Api::Invoices()->UpdateInvoiceStatus([
    'id'     => $id,
    'status' => 'paid',
    'notify' => true,
]);

Correcting the Client Details

patch/api/v1/admin/invoices/{id}/client-details
Invoices/UpdateInvoiceClientDetails admin this invoice only

Corrects the client and address details frozen onto an invoice.

Body 10
kindstringWhether the client is a person or a company. On a person the company name is cleared.
first_namestringThe first name. The full name is built from it.
last_namestringThe last name.
emailstringThe e-mail address.
phonestringThe phone.
identitystringThe identity or tax number.
company_namestringThe company name. It applies to a company.
tax_numberstringThe tax number.
tax_officestringThe tax office.
addressobjectThe address: street, country, state, city and postcode. State and city are resolved from an id when given as a number and stored as written when given as text.
Response fields data
dataobjectThe invoice as it now stands. Same shape as the detail endpoint.
Errors 2
not_found404No such invoice.
insufficient_scope403The key lacks the required scope.
Request
curl -X PATCH 'https://panel.example.com/api/v1/admin/invoices/1212/client-details' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"kind":"corporate","company_name":"Ornek A.S.","tax_number":"0000000000"}'
const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/${id}/client-details`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    kind: 'corporate',
    company_name: 'Example Inc.',
    tax_number: '000000000',
    address: { detail: '123 Market Street', country_id: 840, zipcode: '94105' },
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/' . $id . '/client-details');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'kind'         => 'corporate',
        'company_name' => 'Example Inc.',
        'tax_number'   => '000000000',
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// This endpoint DOES NOT TOUCH the live client; it corrects the copy on this invoice.
Api::Invoices()->UpdateInvoiceClientDetails([
    'id'           => $id,
    'company_name' => 'Example Inc.',
]);

Sending a Notice

post/api/v1/admin/invoices/{id}/notifications
Invoices/SendInvoiceNotification admin a real e-mail goes out

Sends the client the invoice notice template you pick.

Body 1
templatestringreqThe key of the template to send. It has to be one of the invoice notices and switched on.
Response fields data — 3
sentboolWhether the send ran.
templatestringThe template sent.
invoice_idintThe invoice id.
Errors 4
not_found404No such invoice.
template_required422No template was given.
invalid_template422The template is not recognised, or it is switched off.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/invoices/1231/notifications' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"template":"invoice-created"}'
const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/${id}/notifications`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ template: 'invoice-created' }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/' . $id . '/notifications');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['template' => 'invoice-created']),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A switched-off template gives a 422; the list on screen does not mark which are on.
Api::Invoices()->SendInvoiceNotification([
    'id'       => $id,
    'template' => 'invoice-created',
]);

Formalising an Invoice

post/api/v1/admin/invoices/{id}/formalize
Invoices/FormalizeInvoice admin once only

Turns an invoice into a formal one.

Body 1
notifyboolTells the client. It goes out when a formalisation file is there.
Response fields data
dataobjectThe invoice as it now stands. Same shape as the detail endpoint.
Errors 4
not_found404No such invoice.
already_formalized422The invoice is already formal.
blocked_by_gate422A hook refused it.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/invoices/1231/formalize' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"notify":false}'
const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/${id}/formalize`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ notify: false }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/' . $id . '/formalize');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['notify' => false]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A second call gives a 422; in a batch script read the invoice's state first.
$inv = Api::Invoices()->GetInvoice(['id' => $id])['data'];
if (! $inv['formalized']) Api::Invoices()->FormalizeInvoice(['id' => $id]);

Sending a Reminder

post/api/v1/admin/invoices/{id}/remind
Invoices/RemindInvoice admin

Sends the client a reminder for an unpaid invoice.

Body
No body is needed, send an empty one. The invoice comes from the path, and the notification sent is invoice-reminder; there is no field to change it with.
Response fields data — 2
remindedboolWhether the send ran.
invoice_idintThe invoice id.
Errors 2
not_found404No such invoice.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/invoices/1231/remind' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/${id}/remind`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/' . $id . '/remind');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The call counts NO ATTEMPTS; it will remind the same invoice again and again.
Api::Invoices()->RemindInvoice(['id' => $id]);

Pitfalls

A status change never travels alone

Marking an invoice paid opens an income entry. It also gives the invoice a paid number and sets the linked services moving. Refunding winds all of that back, unwinds the metered rows and can order a refund through the payment module. Do not try this on a real client's invoice.

Moving to the same status is an error

When the invoice already stands there, the call is refused. The one exception is changing the payment method on a paid invoice, where the status holds and only the method is written. A script that syncs statuses should read the current one first.

The client correction belongs to the invoice

This endpoint corrects the frozen copy on the invoice and leaves the live client record alone. A wrong address may need fixing in both places. Correct it here for the document already issued, and on the client for the ones to come. A correction here does not reach other invoices either.

Formalising happens once

Formalising an invoice that is already formal gives an error. In a batch script that stops the loop at the first one, so read each invoice's state first. A hook can refuse it as well, because on some installations the act has a counterpart in the books.

Reminders are not counted

The reminder endpoint keeps no count of how often it was called, and calling it twice sends the same client two mails. A script that walks the overdue invoices should keep on its own side which invoice was reminded and when. Otherwise every run warns everyone again.

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.