# Periods, locking and the close

> 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.

Source: https://lopifi.com/apis/periods
Part of: Lopifi — US GAAP books on autopilot, reviewed by a CPA (https://lopifi.com)

---

**What stops someone posting into a closed month?** Check the lock before offering the edit. A disabled control that names the lock date is correct; a 400 after a minute of typing is not.

## Divergences between Xero, QuickBooks Online and NetSuite

### Two of the three have no concept of a period

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.

**Handling:** 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.

### QuickBooks' close date does not reliably block writes

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.

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

### NetSuite locks per subsidiary and per module

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.

**Handling:** 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'.

## Canonical shape

```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);
```
