Your Accounting Period, From Companies House
What changed
In Settings, a limited company now sets its accounting period by confirming itself from Companies House, rather than choosing a year end from a short dropdown of common dates. You enter your company number, the app reads your company back to you, the name, registered office, status, incorporation date and accounting reference date, and you confirm it. A sole trader sees a single line instead: your accounting period follows the tax year, so there is nothing to set here. The old Financial Year End dropdown, which could not express most real year ends and wrote nothing your corporation tax deadline depended on, is gone.
A limited company that has not yet confirmed its accounting reference date cannot post invoices, record dividends, or import transactions. The ledger boundary must be set before the ledger can be used. This is now enforced at the database layer, not left to the application to check.
A sole trader registered through the same flow is seeded with the correct UK tax year period automatically. A limited company is born with no period at all, which is the honest state: a company number and an accounting reference date exist only once you confirm them from Companies House, so no default is written on your behalf.
Why it matters
The dashboard in 5.88.0 prompts you to confirm your accounting reference date before it will seal a corporation tax deadline against it. This is where you do that, and the way you do it is the point. Your year end is read from Companies House, the record that actually decides it, and confirmed by you. It is never typed, and it is never inferred from a default. The date your corporation tax deadline rests on is therefore one you confirmed from the source that governs it, not a guess the app made on your behalf.
It can be set only before you record any activity. Once your ledger has a posting, your accounting period is a boundary that decides which transactions fall in which year, and moving it would re-bucket what you have already recorded. So the control sets it once, while the ledger is still empty, and it is fixed from your first transaction onward. That is a deliberate limit, not a missing feature: a period boundary is not a thing a posted ledger should let you move by hand.
The posting block for an unconfirmed limited company is the same limit, expressed at the ledger rather than the UI. Even if something reaches the database directly, the rule holds.
And a sole trader is never shown a Companies House lookup, because a sole trader has no company number and no corporation tax period. The screen you see is the one that fits the business you declared, and nothing else.
How it is enforced
The registration branch. A limited company is now born with no accounting period row and no tax year hint in its settings. A sole trader receives both, exactly as before. The branch is on the declared legal form, read at registration and written once. Neither default is applied to the other entity type.
The posting block. A database trigger fires before every transaction insert. If the inserting organisation is a limited company that has not yet confirmed an accounting period, the insert is refused and the caller receives a clear precondition code. The trigger sits at the ledger, so it catches every route that writes to it: invoices, credit notes, dividends, opening balances, and any future writer, without the application needing to remember to check.
{
"error": {
"code": "conflict",
"message": "Set the accounting reference date before posting transactions."
}
}
The trigger’s rule is “never confirmed”, meaning a company that has never sealed an accounting period, not merely one with no period currently open. This distinction matters once period close and rollover land: a company that has confirmed a period and is legitimately between periods is not blocked. The block falls only on companies that have never confirmed their year end.
The trigger’s security context mirrors the existing closed-period trigger. It runs under the application role’s tenant context, so its reads are scoped to the inserting organisation and it cannot cross-read another tenant’s data.
The confirmation flow. The lookup reads, the confirm writes, and only the confirm writes. Entering your company number and looking it up is advisory: it fetches your company and shows it to you, and it changes nothing. The single act that sets your period is the confirmation, so what is sealed is always what you saw and agreed to, never a value a stale or altered client could slip past you.
The control offers to set only when the period can still be set. Whether it is settable is read from the same condition the seal itself enforces, the ledger being at its starting point, so the screen never offers you an action the boundary would then refuse. If a transaction is recorded in the moment between the confirmation being shown and you confirming it, the set declines and the screen settles to its fixed state rather than raising an error, because by then the refusal is the correct answer.
{
"error": {
"code": "organisation_not_empty",
"message": "The accounting period can only be set before any transactions are recorded."
}
}
The view you see is gated on the legal form you declared, read as a fact at the data layer (migration 122). A declared company gets the Companies House flow, a declared sole trader gets the tax-year line, and a business whose form is not yet declared is treated as a company rather than being told, falsely, that it is a sole trader. No screen asserts a tax basis you did not choose.
The seal accepts only a year end that could be one. It must be a real month-end, since an accounting reference date always is, and it must fall within twelve months of the period start. A company whose first period runs a little over twelve months, which is the common case, therefore stays honestly empty rather than sealing a single span that is not yet a valid period, and waits for the first-year split still to come.
How it is proven
The accounting period flow is backed by anchors that open real database connections against the live schema, set a tenant context and roll back, so they prove the read-back, the settable condition and the refusal the way production runs them, not the way a mock would. They pin the company that is read and confirmed, the period that is sealed only at the starting point, the refusal once activity exists, and the form gate that keeps a sole trader out of the company flow.
The posting block is proven by nine anchors in the registration period-seed suite, all exercised against the live schema, with a tenth anchor deferred and named below. Four arms prove the block directly at the ledger: a limited company with no accounting period is refused and receives the precondition response; a sole trader is admitted; a limited company that has confirmed its year end is admitted; and an organisation whose legal form is not yet declared is admitted. Two further anchors prove the precondition routes to the correct HTTP status through each posting path, so the distinction between a missing period and a closed period, both carried in the same error class, is verified end to end. The registration branch is proven two-sided: a sole trader registration produces exactly one tax-year period row; a limited company registration produces none.
The corporation tax payment date the sealed period drives is proven over a closed set of thirty-one dates, every month end across a leap and a non-leap year plus ordinary, boundary and overflow days, so the end-of-month rule is exhausted rather than sampled (migration 123). The declared legal form it reads is a data-layer fact (migration 122). The posting block sits at the database layer (migration 124).
The release crossed the sixteen-phase deploy pipeline and the documentation pipeline’s six gates with 374 executable anchors green and zero violations, every migration registering itself against the live database on apply.
Operational impact
Settings gains the accounting period control in place of the removed dropdown. Your declared legal form is now returned by the organisation endpoint, so the app reads it from one place rather than inferring it from billing state:
curl https://app.speybooks.com/api/v1/organisation \
-H "Authorization: Bearer $SPEYBOOKS_TOKEN"
{
"data": {
"entityType": "limited_company",
"companyNumber": "SC000000"
}
}
The dividends and opening balances routes now surface a clean precondition response rather than a server error when a limited company without a confirmed period attempts to post. No other response shapes changed. There are no breaking changes.
Endpoint count: 215, unchanged.
Deferred
Four things are honestly not finished. A company that began recording transactions before it set its accounting period cannot yet set or correct it here, because the period can only be sealed while the ledger is empty. Until the change path for an established company lands, such a company sees its company details as read-only and its corporation tax deadline stays honestly absent rather than sealed on a default. The first-year split, where a period a little over twelve months becomes two corporation tax periods with two deadlines, is still the next piece, so a company in that position sees its accounting reference date confirmed but no deadlines in the tax timeline yet. The incorporation date in the confirmation is shown in the source format pending a pass to British long form. And one further proof anchor is owed: the dashboard’s attention-count behaviour on a company with no period is confirmed by derivation but does not yet have its own executable anchor, which is why nine of the suite’s ten checks run today and the tenth is carried.
Until these land, nothing is sealed on an unconfirmed or invalid date. The control would rather show you read-only details, or an honest gap, than a year end it cannot stand behind.