Opening Balances

Opening balances establish the starting position of your accounts when migrating to SpeyBooks. The Opening Balance Closure Engine (OBCE) parses a trial balance CSV, auto-maps accounts using a 5-tier strategy, validates the balance equation, and creates an immutable migration journal on confirm.

This guide covers the full import workflow from upload through to confirmation, including the account mapping strategy, clearing mode for outstanding documents, and the void/re-import cycle. For the full endpoint reference, see Opening Balances API.


The singleton constraint

Only one active opening balance journal is permitted per organisation. This is enforced by a partial unique index in the database (uniq_active_ob_per_org). If you need to re-import, void the existing journal first.

The status endpoint lets you check whether an active journal exists before attempting an upload.

Checking for an existing journal

Returns hasOpeningBalance: true with the transaction ID if an active journal exists, or false if the organisation is ready for import.


Uploading a trial balance

Upload a trial balance CSV via multipart form. The OBCE engine processes it in stages: parse, partition, map, and balance proof. The response includes the complete preview payload needed to render the mapping wizard UI, so there is no need for a separate GET call after upload.

Maximum file size is 5 MB. Only CSV files are accepted. The engine auto-detects dual-column (separate debit/credit columns) and signed single-column formats.

If an active opening balance journal already exists, the upload returns 409 with code SINGLETON_VIOLATION.

Cutover date

The journal date defaults to the last day of the previous month. You can override this when correcting mappings via the map endpoint.

API response format

All monetary values in the opening balances API are formatted as pounds (e.g. "1500.00"), not minor units. This differs from most SpeyBooks endpoints which use pence.


The 5-tier mapping strategy

The OBCE engine auto-maps each row from your CSV to your chart of accounts using five strategies, tried in order:

TierMethodConfidenceHow it works
1exact1.0Exact name match against chart of accounts
2code1.0Account code match (e.g. "1200" maps to acc_1200)
3dictionary0.9Built-in dictionary of common UK account names
4fuzzy0.7-0.9Fuzzy string matching with confidence threshold
5unmapped0.0No match found, requires manual resolution

When clearing mode is active, Trade Debtors and Trade Creditors are redirected before the 5-tier strategy runs, appearing as clearing_redirect with confidence 1.0.

User overrides via the map endpoint appear as method user_override with confidence 1.0 and take precedence over all auto-mapped results.


Balance proof

The balance equation must hold before the journal can be confirmed:

total debits = total credits

If the delta between debits and credits is within 5 pence (£0.05), the engine automatically injects a rounding adjustment line against account 7999 (Rounding). If the rounding account does not exist, it is created as a system expense account. Deltas greater than £0.05 block confirmation.

The balance proof object in the response shows totalDebit, totalCredit, delta, balanced, roundingInjected, and roundingAmount.


Clearing mode

Clearing mode routes Trade Debtors and Trade Creditors through migration clearing accounts instead of posting directly to the standard accounts:

Standard accountMigration clearing account
Trade Debtors (1200)MC_AR (1198)
Trade Creditors (2100)MC_AP (2198)

This enables the TMADD 6.0 zero-sum invariant for outstanding document imports. When you later import outstanding invoices or bills, their clearing journals offset against MC_AR/MC_AP, and the clearing accounts should net to zero.

Set clearing mode by passing clearingMode=clearing as a form field during upload. The default is direct (standard mapping). When clearing mode is active, the engine auto-creates the MC_AR and MC_AP system accounts if they do not exist.

For the full outstanding document import workflow, see Migration Guide.


Correcting mappings

After reviewing the auto-mapped results from upload, use the map endpoint to fix any mismatches. Send an array of source label to target account ID overrides, and optionally update the cutover date.

The engine re-runs the full preview pipeline and returns the complete updated payload (mappings, lines, balance proof, unmapped, canConfirm). There is no need for a separate GET call after remapping.


Confirming the journal

Confirmation passes a triple gate before creating the migration journal:

  1. canConfirm - the preview must have no errors and no unmapped accounts
  2. Singleton - no active opening balance journal exists
  3. Balance proof - total debits must equal total credits exactly

On success, the engine creates an immutable journal with source: 'migration_opening_balance', locked: true, and status: 'posted'. The transaction uses SET CONSTRAINTS ALL DEFERRED so the database balance trigger fires at commit after all lines are inserted.

The journal reference follows the pattern OB-{cutoverDate} (e.g. OB-2026-02-28). Four database triggers prevent subsequent modification of the committed journal.

Error responses

CodeStatusMeaning
not_found404Import does not exist or is not in pending status
SINGLETON_VIOLATION409An active OB journal already exists
not_confirmable422canConfirm is false (unmapped accounts or errors remain)
BALANCE_FAILED422Debits do not equal credits

Voiding and re-importing

Voiding sets voided_at on the transaction and marks the import as voided. The voided journal remains in the ledger for audit purposes but is excluded from balances and reports. Voiding restores the ability to upload new opening balances.

To re-import: void the existing journal, then upload a new CSV. The full cycle is: upload, review mappings, confirm, and if needed later, void and repeat.

Deleting a pending import

If you have uploaded a CSV but not yet confirmed, you can delete the pending import using DELETE /opening-balances/{id}. Only imports with status pending can be deleted. Completed or voided imports are part of the audit trail and cannot be removed.


Worked example: first-time setup

  1. Check status - GET /opening-balances/status to confirm no existing journal
  2. Export trial balance - from your previous accounting system as CSV
  3. Upload - POST /opening-balances/upload with the CSV and clearingMode=clearing if you plan to import outstanding invoices
  4. Review mappings - check the response for any unmapped accounts
  5. Fix mismatches - PATCH /opening-balances/dimp_2/map with overrides for any incorrect or missing mappings
  6. Verify balance proof - confirm balanced: true and check delta
  7. Confirm - POST /opening-balances/dimp_2/confirm to create the immutable migration journal
  8. Import outstanding documents - if using clearing mode, proceed to invoice imports to clear MC_AR/MC_AP

Related endpoints