Month Close #
Closing a month locks the books for it: nothing dated in a closed month can be posted to the Ledger, and no Balance dated in it can change, until the month is reopened. It is how a Business marks its monthly (or quarterly, or half-yearly) books as reviewed, without waiting for the Reporting Period to end.
Posted Ledger entries are already immutable and hash chained, so a close copies nothing. It records three things for each month it closes:
- the month’s status, which is the lock;
- an anchor: the position and hash of the last entry in the Entity’s ledger chain at the time of the close, so that anything added to the month later can be found;
- a balance checkpoint for every Account with a balance at the month’s end, in the Account’s Currency and in the reporting Currency. The figures are the same ones the reports give.
Drafts are not locked.
A draft Transaction dated in a closed month can still be created, changed and deleted. Only posting it is refused, until the month is reopened.
Properties #
Direct Properties #
| Property | Type | Definition |
|---|---|---|
| Month | Date | The first day of the month |
| Status | String | closed while the month is closed, open once it has been reopened |
| Closed By | Integer | The id of the user who last closed the month |
| Closed At | Datetime | When the month was last closed |
| Anchor Sequence | Integer | The position of the last entry in the Entity’s ledger chain when the month was last closed (0 for an empty ledger) |
| Anchor Hash | String | The hash of that entry |
Indirect Properties #
| Property | Type | Definition |
|---|---|---|
| Events | List | Every close and reopen of the month: who, with which token, when and why. Events are never changed or removed |
| Balance Checkpoints | List | The balance of every Account with a balance at the end of the month, while the month is closed |
| Close Frequency | String | The Entity’s close_frequency: which month-ends a close can be made through |
| Reconciliation Before Close | String | The Entity’s reconciliation_before_close: whether bank reconciliation is checked before a close |
Basics #
The rules #
- Months close in order, from the first month with anything posted in it (a posted Transaction or a Balance). A close through a month closes every open month up to and including it, in one go.
- Only a month that has ended can be closed.
- The last month of a financial year is never month-closed. It stays open for adjusting entries until the Reporting Period itself is
CLOSED, at which point every month of that year counts as closed. The months after it can be closed in the meantime: a close passes over it. - What a close can target depends on the Entity’s
close_frequency, counted from the Entity’syear_startrather than from January:
| Close Frequency | Can close through |
|---|---|
monthly (default) | the end of any month but the year’s last |
quarterly | the end of the 3rd, 6th and 9th month of the financial year |
half_yearly | the end of the 6th month of the financial year |
For a financial year starting in April, quarterly closes through the end of June, September and December; March, the year’s last month, closes with the year.
- Only the owner of the Entity can close or reopen months, with a token that carries the
books:closescope.books:writeis not enough. Every user of the books can read the months and their history withbooks:read. The same goes for changing a Reporting Period’s status.
Close through a month #
through is the month to close through, as YYYY-MM or as its last day (2026-03-31), and reason (up to 1000 characters) is kept with the month’s history.
Request #
curl --location --request POST 'https://api.microbooks.io/books/v1/month-close/close' \
--header "Authorization: Bearer <bearer_token>" \
--header "Content-Type: application/json" \
--data-raw '{
"through": "2026-03",
"reason": "Q1 reviewed"
}'
import requests
url = "https://api.microbooks.io/books/v1/month-close/close"
body = {
"through": "2026-03",
"reason": "Q1 reviewed"
}
headers = {"Authorization": "Bearer <bearer_token>"}
response = requests.request("POST", url, headers=headers, json=body)
print(response.text)
var request = require('request');
var options = {
'method': 'POST',
'url': 'https://api.microbooks.io/books/v1/month-close/close',
'headers': {'Authorization': 'Bearer <bearer_token>'},
'json': {
"through": "2026-03",
"reason": "Q1 reviewed"
}
};
request(options, function (error, response) {
if (error) throw new Error(error);
console.log(response.body);
});
$client = new Client();
$body = '{
"through": "2026-03",
"reason": "Q1 reviewed"
}';
$request = new Request('POST', 'https://api.microbooks.io/books/v1/month-close/close', ['Authorization' => 'Bearer <bearer_token>', 'Content-Type' => 'application/json'], $body);
$res = $client->sendAsync($request)->wait();
echo $res->getBody();
Response #
{
"status": "success",
"message": "Closed 3 month(s) through March 2026",
"closed": [
{
"id": 1,
"month": "2026-01-01",
"status": "closed",
"closed_by": 7,
"closed_at": "2026-04-03T09:12:44.000000Z",
"anchor_sequence": 412,
"anchor_hash": "9c1e0f6b4f0d2e8a..."
},
{
"id": 2,
"month": "2026-02-01",
"status": "closed",
"closed_by": 7,
"closed_at": "2026-04-03T09:12:44.000000Z",
"anchor_sequence": 412,
"anchor_hash": "9c1e0f6b4f0d2e8a..."
},
{
"id": 3,
"month": "2026-03-01",
"status": "closed",
"closed_by": 7,
"closed_at": "2026-04-03T09:12:44.000000Z",
"anchor_sequence": 412,
"anchor_hash": "9c1e0f6b4f0d2e8a..."
}
],
"closed_through": "2026-03-31",
"warnings": []
}
Like every write, a close can be sent as a dry run with a rollback header, to see what it would close without closing anything, and with an Idempotency-Key so that a retry replays the first response rather than closing again.
Variations #
Bank reconciliation before a close #
The Entity’s reconciliation_before_close decides whether a close checks that the bank (and credit card) Accounts are reconciled through the month first:
| Setting | Effect |
|---|---|
off (default) | No check |
warn | The close goes ahead, and lists the outstanding reconciliations under warnings |
enforce | The close is refused (detail code 108), and the outstanding reconciliations are listed under blockers |
enforce also requires every month of a financial year but the last to be closed before its Reporting Period can leave OPEN for ADJUSTING or CLOSED. With off or warn the Reporting Period update goes ahead and lists the open months under warnings.
Bank reconciliation is being built. Until it is available no Account is reported as unreconciled, sowarnandenforcechange nothing at close time.
Operations #
Review #
Lists every month that is closed or has been closed, latest first, with the next month to close (status open, id null), the books’ closed_through date, and the month-ends a close can be made through now.
Request #
curl --location --request GET 'https://api.microbooks.io/books/v1/month-close' \
--header "Authorization: Bearer <bearer_token>"
import requests
url = "https://api.microbooks.io/books/v1/month-close"
headers = {"Authorization": "Bearer <bearer_token>"}
response = requests.request("GET", url, headers=headers)
print(response.text)
Response #
{
"status": "success",
"months": [
{
"id": null,
"month": "2026-04-01",
"status": "open",
"closed_by": null,
"closed_at": null,
"anchor_sequence": null,
"anchor_hash": null
},
{
"id": 3,
"month": "2026-03-01",
"status": "closed",
"closed_by": 7,
"closed_at": "2026-04-03T09:12:44.000000Z",
"anchor_sequence": 412,
"anchor_hash": "9c1e0f6b4f0d2e8a..."
}
],
"closed_through": "2026-03-31",
"next_to_close": "2026-04-01",
"closable": true,
"close_targets": ["2026-04-30", "2026-05-31", "2026-06-30"],
"settings": {
"year_start": 1,
"close_frequency": "monthly",
"reconciliation_before_close": "off"
}
}
Reopen a month #
Reopening a month reopens it and every closed month after it, so the closed months are always the earliest ones. Each month reopened gets an event; the later ones record Reopened with <month>: <reason>. Their balance checkpoints are removed, and are taken again when they are next closed. A month of a CLOSED Reporting Period cannot be reopened: reopen the Reporting Period first.
Request #
curl --location --request POST 'https://api.microbooks.io/books/v1/month-close/reopen' \
--header "Authorization: Bearer <bearer_token>" \
--header "Content-Type: application/json" \
--data-raw '{
"month": "2026-02",
"reason": "Supplier bill received late"
}'
import requests
url = "https://api.microbooks.io/books/v1/month-close/reopen"
body = {
"month": "2026-02",
"reason": "Supplier bill received late"
}
headers = {"Authorization": "Bearer <bearer_token>"}
response = requests.request("POST", url, headers=headers, json=body)
print(response.text)
Response #
{
"status": "success",
"message": "Reopened February 2026 and the 1 closed month(s) after it",
"reopened": [
{
"id": 2,
"month": "2026-02-01",
"status": "open",
"closed_by": 7,
"closed_at": "2026-04-03T09:12:44.000000Z",
"anchor_sequence": 412,
"anchor_hash": "9c1e0f6b4f0d2e8a..."
},
{
"id": 3,
"month": "2026-03-01",
"status": "open",
"closed_by": 7,
"closed_at": "2026-04-03T09:12:44.000000Z",
"anchor_sequence": 412,
"anchor_hash": "9c1e0f6b4f0d2e8a..."
}
],
"closed_through": "2026-01-31"
}
A reopened month keeps the anchor and closer of its last close until it is closed again.
History #
Lists every close and reopen, latest first, paginated like any other list (perPage, page). Pass month (YYYY-MM) for one month’s history. A close event carries the anchor it was taken at; token_id and client identify the token and OAuth client that made the change.
GET /books/v1/month-close/events?month=2026-02
{
"status": "success",
"data": [
{
"id": 5,
"month_close_id": 2,
"user_id": 7,
"token_id": "3f0c...",
"client": "9b1e...",
"action": "reopen",
"reason": "Supplier bill received late",
"anchor_sequence": null,
"anchor_hash": null,
"created_at": "2026-04-20T08:01:10.000000Z",
"month": "2026-02-01"
},
{
"id": 2,
"month_close_id": 2,
"user_id": 7,
"token_id": "a81d...",
"client": "9b1e...",
"action": "close",
"reason": "Q1 reviewed",
"anchor_sequence": 412,
"anchor_hash": "9c1e0f6b4f0d2e8a...",
"created_at": "2026-04-03T09:12:44.000000Z",
"month": "2026-02-01"
}
],
"meta": {"current_page": 1, "per_page": 5, "total": 2, "last_page": 1}
}
Verify #
Checks the Ledger against the closes. chain_intact reports whether the Entity’s ledger hash chain is unbroken (chain gives the first break, if any). posted_after_close lists every Ledger entry dated in a closed month that joined the chain after the month was first closed, that is, after its first anchor. Corrections made while a month was reopened belong there, and each must fall inside a reopen window (from a reopen to the next close) in the month’s history: in_reopen_window. ok is false if the chain is broken or any entry is outside a window, which means the entry got into the closed month some other way.
GET /books/v1/month-close/verify
{
"status": "success",
"ok": true,
"chain_intact": true,
"chain": {"rows": 431, "break": null},
"closed_through": "2026-03-31",
"posted_after_close": [
{
"ledger_id": 9051,
"transaction_id": 1220,
"entity_sequence": 425,
"posting_date": "2026-02-26",
"posted_at": "2026-04-20T08:03:51+00:00",
"month": "2026-02-01",
"anchor_sequence": 412,
"in_reopen_window": true
}
]
}
What the lock refuses #
| Request | Dated in a closed month |
|---|---|
POST /transaction with post_transaction: true, POST /transaction/{id}/post | Refused, Transactions detail code 120 |
| Creating, updating or deleting a draft Transaction | Allowed |
| Creating, updating or deleting a Balance, or restoring one from the Recycle Bin | Refused, Balances detail code 109 |
Removing allocations (POST /assignment/deallocate) when the Transaction, an Assignment or a Transaction it cleared is dated in a closed month | Refused, Assignments detail code 122 |
| An exchange difference posted by an Assignment | Dated on the day the assignment is made, so never in a closed month. Should one be, it is refused with detail code 121 |
A closed or adjusting Reporting Period keeps its own errors (Transactions 106 and 107), which come first.
Errors #
Below are the Errors that are returned by the Month Close Resource.
| Detail Code | HTTP Status | Name | Definition |
|---|---|---|---|
| 101 | 400 | Missing Entity | Months can only be closed for an existing Entity |
| 102 | 400 | Unended Month | Only a month that has ended can be closed. Returned under through |
| 103 | 400 | Year End Month | The last month of a financial year is not month-closed: it stays open for adjusting entries and closes with its Reporting Period. Returned under through |
| 104 | 400 | Invalid Close Target | The month is not one the Entity’s close_frequency allows a close through. GET /month-close lists the targets. Returned under through |
| 105 | 400 | Nothing To Close | The books are already closed through the month, it belongs to a CLOSED Reporting Period, or nothing has been posted yet. Returned under through |
| 106 | 400 | Month Not Closed | Only a closed month can be reopened. Returned under month |
| 107 | 400 | Closed Year Month | The month, or a closed month after it, belongs to a CLOSED Reporting Period and cannot be reopened. Returned under month |
| 108 | 400 | Unreconciled Accounts | reconciliation_before_close is enforce and reconciliations are outstanding for the close. They are listed under blockers. Returned under reconciliation |
| 403 | 403 | Forbidden | Closing and reopening need a token with the books:close scope (error: insufficient_scope), and the caller must own the Entity |
| 422 | 422 | Validation | A field is missing, malformed or not recognised. The field is the key under errors |
Errors use the standard error envelope, which also lists the request-level codes (401, 403, 404, 429 and others) that any endpoint can return.