Skip to content

Periods, locking and the close

What stops someone posting into a closed month? NetSuite has a real accounting period record, locked per subsidiary and per module. Xero and QuickBooks have a single date. That difference makes 'period' something your system owns rather than something you read.

Concepts
7
Divergences
3
Structural
1

Endpoints

Xero

GET /Organisation

PeriodLockDate, EndOfYearLockDate

QBO

GET /preferences

AccountingInfoPrefs.BookCloseDate

NetSuite

SuiteQL: FROM accountingperiod

A record per period

The same concept in three vocabularies

Read across a row. Where a cell is absent, the concept has nowhere to live in that system — which is a design constraint, not a gap to paper over.

NativeNative
— A field or object exists for it
PartialPartial
— Exists with a caveat that changes what you can build
DerivedDerived
— Not stored — you compute it
AbsentAbsent
— Nothing in the API
Periods, locking and the close fields compared across Xero, QuickBooks Online and NetSuite
ConceptXeroQBONetSuite
Period object
Absent

None

Two lock dates on the organisation

Absent

None

A book close date in preferences

Native

accountingperiod

First class, per year / quarter / month

Granularity
Partial

Two dates, org-wide

PeriodLockDate blocks all but advisers; EndOfYearLockDate blocks everyone

Partial

One date, optionally password protected

Native

Per period, per subsidiary, and separately for AP, AR and GL

Enforced on API writes
Partial

Depends on role

Before EndOfYearLockDate always fails; before PeriodLockDate fails unless the connected user is an adviser

Partial

Partial

The close date is advisory on some paths

Native

Yes, strictly

Per book and per subsidiary

Fiscal year definition
Native

Organisation.FinancialYearEndDay / Month

Native

CompanyInfo fiscal year start month

Native

Period calendar records

Supports 4-4-5 and custom calendars

Adjusting entries
Partial

Ordinary manual journals

Partial

JournalEntry.Adjustment

Native

Adjustment period + isadjust

Reopen
Partial

Move the date

No record that it moved

Partial

Move the date

Native

Explicit reopen, audited

Year-end / retained earnings roll
Native

Automatic to a system account

Native

Automatic

Native

Explicit year-end close process

What actually differs, and what you do about it

A difference stated without a response is trivia. Each of these carries the handling.

Structural

Two of the three have no concept of a period

Xero · QBO

Xero and QBO have cut-off dates — two in Xero, whose effect on an API write depends on the connected user's role. There is no object that says 'August 2026 is closed', no state, no history of when it closed, and no per-module or per-entity granularity. Everything a close checklist needs to track exists nowhere in the source.

Do — Period state is owned by our system, with the ledger's lock date as one input signal. The close checklist writes to us; the lock date is a check we run against them.

Sharp edge

QuickBooks' close date does not reliably block writes

QBO

The book close date is enforced in the UI with a password prompt, and behaves inconsistently through the API depending on the path. You can end up with a posting inside a closed period that nobody authorised.

Do — Enforce the cut-off on our side before the write, for every source. Do not rely on the ledger to refuse.

Sharp edge

NetSuite locks per subsidiary and per module

NetSuite

August can be closed for AP and open for GL, closed for subsidiary 2 and open for subsidiary 3. A single 'is this period closed' boolean is not expressible.

Do — Model the lock as a set of scoped rules, not a date. The UI answers 'can I post this document' rather than 'is the period closed'.

The type the components consume

periods.ts
type PeriodLock = {
  /** A rule, not a date — NetSuite locks per module and per entity. */
  scope: { entityId?: string; module?: "ap" | "ar" | "gl" | "all" };
  lockedThrough: string;         // calendar date, inclusive
  source: SourceRef;
};

type Period = {
  id: string;
  label: string;                 // "August 2026"
  start: string;
  end: string;
  /** Ours. Xero and QBO have no period state to read. */
  status: "open" | "in_close" | "closed" | "reopened";
  locks: PeriodLock[];
  ownedBy: "us" | "source";
};

const canPost = (p: Period, date: string, scope: PostScope) =>
  p.locks.every((l) => !scopeMatches(l.scope, scope) || date > l.lockedThrough);

What this changes in the interface

The payoff. Every difference above that reaches the surface, and what the component does about it.

Period state is ours, not the ledger's
CloseChecklist owns the period lifecycle and shows the ledger's lock date as one control among several — never as the period's status.
The lock is knowable before the write
CutoffWarning renders on the form, naming the date, before the user types. Edit affordances inside a locked period are disabled, not merely unsuccessful.
Locks can be scoped
The question the UI asks is 'can this document be posted', answered per document, not 'is the period closed', answered per month.

Where the accounting is explained

What consumes this payload

The normalised object above is what the components read. These are the surfaces it feeds, the words for the concepts in it, and the document that explains the accounting.

Compared acrossXero docsQuickBooks Online docsNetSuite docs

Works in your QuickBooks Online or Xero file

Get your books closed every month

Lopifi is building AI bookkeeping that keeps your books closed on US GAAP as transactions happen, with a CPA review every quarter, for $200 a month. We’re not open yet.

$200/month when we open. No card now.