Recurring Expenses

7 views Markdown

The six endpoints that define the fixed outgoings repeating each month.

Overview

Recurring expenses define the payments that come round every month: server rent, a licence fee, a subscription. What lives here is not an expense but a rule that brings expenses into being.

Each rule carries a day and a time. When the scheduled job reaches that moment the rule runs and writes that month's expense into the cash book. So the number of records here is not the number in the book.

The summary looks forward: it says how much fixed outgoing an installation has each month. To see what was actually paid, look at the cash book.

Reference

Listing the Expenses

get/api/v1/admin/invoices/periodic
Invoices/GetPeriodicExpenses admin

Returns the expenses that repeat every month, filtered.

Query 8
pageintWhich page.
limitintRecords per page. A value out of range falls back to the default.
searchstringSearches the description.
currency_idintFilters by currency.
amountstringFilters by amount.
amount_opstringWhich way the amount comparison runs.
descriptionstringSearches the description.
daterangestringFilters between two dates.
Response fields data[] — 9 + meta — 4
idintThe record id.
amountfloatThe amount charged each month.
currency_idintThe currency it is in.
descriptionstringWhat the expense is.
dayintWhich day of the month it runs.
hourintThe hour it runs.
minuteintThe minute it runs.
timestringThe hour and minute joined together.
created_atstring | nullWhen the record was opened.
totalintHow many match the filter. It comes back under meta.
pageintThe page you are on.
limitintThe page size.
next_pageintThe next page. Zero means you are on the last one.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/invoices/periodic' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/invoices/periodic', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/periodic');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// These are RULES rather than records: each one brings a cash expense into being monthly.
$rules = Api::Invoices()->GetPeriodicExpenses()['data'];

Adding an Expense

post/api/v1/admin/invoices/periodic
Invoices/CreatePeriodicExpense admin a day and a time are needed

Defines an expense to be charged every month by itself.

Body 5
currency_idintreqThe currency of the amount.
amountfloatreqThe amount charged each month. It has to be above zero.
dayintreqWhich day of the month it runs. Between one and thirty-one.
timestringreqThe time it runs. Given as an hour and a minute, then split into both.
descriptionstringWhat the expense is.
Response fields 201 — data — 9
dataobjectThe expense opened. Same shape as a list item.
Errors 6
currency_required422No currency was given.
invalid_amount422The amount is zero or below.
invalid_day422The day is out of range.
invalid_time422The time is not in the expected form.
create_failed422The record could not be opened.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/invoices/periodic' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"currency_id":840,"amount":49.9,"day":1,"time":"09:00","description":"Aylik sunucu gideri"}'
const res = await fetch('https://panel.example.com/api/v1/admin/invoices/periodic', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    currency_id: 840,
    amount: 49.9,
    day: 1,
    time: '09:00',
    description: 'Monthly server cost',
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/periodic');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'currency_id' => 840,
        'amount'      => 49.9,
        'day'         => 1,
        'time'        => '09:00',
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Picking the thirty-first can leave the record unprocessed in the SHORT months.
Api::Invoices()->CreatePeriodicExpense([
    'currency_id' => 840,
    'amount'      => 49.9,
    'day'         => 1,
    'time'        => '09:00',
]);

Reading the Summary

get/api/v1/admin/invoices/periodic/summary
Invoices/GetPeriodicSummary admin

Returns the total of the fixed monthly outgoings.

Response fields data — 3
currency_idintThe currency the total is in.
totalfloatThe monthly total. Other currencies are converted before adding.
countintHow many records went into it.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/invoices/periodic/summary' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch('https://panel.example.com/api/v1/admin/invoices/periodic/summary', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/periodic/summary');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// This total looks FORWARD: it says what will be paid each month, not what was.
$plan = Api::Invoices()->GetPeriodicSummary()['data'];

Reading One Expense

get/api/v1/admin/invoices/periodic/{periodic_id}
Invoices/GetPeriodicExpense admin

Returns a single recurring expense.

Response fields data — 9
idintThe record id.
amountfloatThe amount charged each month.
currency_idintThe currency it is in.
descriptionstringWhat the expense is.
dayintWhich day of the month it runs.
hourintThe hour it runs.
minuteintThe minute it runs.
timestringThe hour and minute joined together.
created_atstring | nullWhen the record was opened.
Errors 2
not_found404No such recurring expense.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/invoices/periodic/8' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch(`https://panel.example.com/api/v1/admin/invoices/periodic/${periodicId}`, {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/periodic/' . $periodicId);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The record does NOT carry when it last ran; read that from the cash book instead.
$rule = Api::Invoices()->GetPeriodicExpense(['periodic_id' => $id])['data'];

Updating an Expense

patch/api/v1/admin/invoices/periodic/{periodic_id}
Invoices/UpdatePeriodicExpense admin

Changes a recurring expense's amount or its timing.

Body 5
currency_idintThe currency of the amount.
amountfloatThe amount charged each month. It has to be above zero.
dayintWhich day of the month it runs. Between one and thirty-one.
timestringThe time it runs. An hour and a minute on the twenty-four hour clock.
descriptionstringWhat the expense is.
Response fields data — 9
dataobjectThe expense as it now stands. Same shape as a list item.
Errors 6
not_found404No such recurring expense.
currency_required422No currency was given.
invalid_amount422The amount is zero or below.
invalid_day422The day is out of range.
invalid_time422The time is not in the expected form.
insufficient_scope403The key lacks the required scope.
Request
curl -X PATCH 'https://panel.example.com/api/v1/admin/invoices/periodic/8' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"amount":59.9,"day":5,"time":"10:30"}'
const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/periodic/${periodicId}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ amount: 59.9, day: 5, time: '10:30' }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/periodic/' . $periodicId);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['amount' => 59.9, 'day' => 5]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The change does NOT reach back: what already ran this month keeps the old amount.
Api::Invoices()->UpdatePeriodicExpense(['periodic_id' => $id, 'amount' => 59.9]);

Removing an Expense

delete/api/v1/admin/invoices/periodic/{periodic_id}
Invoices/DeletePeriodicExpense admin

Removes a recurring expense.

Response fields data — 2
deletedboolWhether the delete ran.
idintThe id of the record removed.
Errors 3
not_found404No such recurring expense.
delete_failed422The record could not be removed.
insufficient_scope403The key lacks the required scope.
Request
curl -X DELETE 'https://panel.example.com/api/v1/admin/invoices/periodic/8' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch(`https://panel.example.com/api/v1/admin/invoices/periodic/${periodicId}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/invoices/periodic/' . $periodicId);
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);
// A delete leaves PAST cash records alone; it only stops the rule running again.
Api::Invoices()->DeletePeriodicExpense(['periodic_id' => $id]);

Pitfalls

A late day can be skipped in the short months

The thirty-first is not in every month, and neither are the thirtieth and the twenty-ninth in some. A rule set to such a day can go unprocessed in those months and the expense never reaches the book. For an outgoing that has to run monthly, pick an early day.

Changing a rule does not reach back

Changing the amount or the day touches only what comes after. An expense already processed this month stays in the book at the old amount, and the new one shows next month. To correct the past, update the record in the cash book as a separate step.

A delete does not remove past expenses

Deleting a rule only stops it running again; the expense records it made up to that day stay in the book. That is the right behaviour, because those payments really happened. Clearing the past means removing those cash records as a separate step.

The record does not carry when it last ran

A recurring expense record does not say when it last ran. Telling whether a rule ran this month means looking in the cash book for a record matching its description. Weigh that when writing a check: the list here gives no run history on its own.

The summary looks forward and the book looks back

The summary here says what will be paid each month, while the cash summary says what was paid this month. The two do not match and are not meant to: a rule running on the twentieth is not in the book on the tenth. Keep them on separate lines in a report.

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.