Month Close

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 #

PropertyTypeDefinition
MonthDateThe first day of the month
StatusStringclosed while the month is closed, open once it has been reopened
Closed ByIntegerThe id of the user who last closed the month
Closed AtDatetimeWhen the month was last closed
Anchor SequenceIntegerThe position of the last entry in the Entity’s ledger chain when the month was last closed (0 for an empty ledger)
Anchor HashStringThe hash of that entry

Indirect Properties #

PropertyTypeDefinition
EventsListEvery close and reopen of the month: who, with which token, when and why. Events are never changed or removed
Balance CheckpointsListThe balance of every Account with a balance at the end of the month, while the month is closed
Close FrequencyStringThe Entity’s close_frequency: which month-ends a close can be made through
Reconciliation Before CloseStringThe 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’s year_start rather than from January:
Close FrequencyCan close through
monthly (default)the end of any month but the year’s last
quarterlythe end of the 3rd, 6th and 9th month of the financial year
half_yearlythe 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:close scope. books:write is not enough. Every user of the books can read the months and their history with books: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:

SettingEffect
off (default)No check
warnThe close goes ahead, and lists the outstanding reconciliations under warnings
enforceThe 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, so warn and enforce change 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 #

RequestDated in a closed month
POST /transaction with post_transaction: true, POST /transaction/{id}/postRefused, Transactions detail code 120
Creating, updating or deleting a draft TransactionAllowed
Creating, updating or deleting a Balance, or restoring one from the Recycle BinRefused, Balances detail code 109
Removing allocations (POST /assignment/deallocate) when the Transaction, an Assignment or a Transaction it cleared is dated in a closed monthRefused, Assignments detail code 122
An exchange difference posted by an AssignmentDated 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 CodeHTTP StatusNameDefinition
101400Missing EntityMonths can only be closed for an existing Entity
102400Unended MonthOnly a month that has ended can be closed. Returned under through
103400Year End MonthThe 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
104400Invalid Close TargetThe month is not one the Entity’s close_frequency allows a close through. GET /month-close lists the targets. Returned under through
105400Nothing To CloseThe books are already closed through the month, it belongs to a CLOSED Reporting Period, or nothing has been posted yet. Returned under through
106400Month Not ClosedOnly a closed month can be reopened. Returned under month
107400Closed Year MonthThe month, or a closed month after it, belongs to a CLOSED Reporting Period and cannot be reopened. Returned under month
108400Unreconciled Accountsreconciliation_before_close is enforce and reconciliations are outstanding for the close. They are listed under blockers. Returned under reconciliation
403403ForbiddenClosing and reopening need a token with the books:close scope (error: insufficient_scope), and the caller must own the Entity
422422ValidationA 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.