Transactions & Journals
Every financial event in SpeyBooks is recorded as a double-entry journal transaction. A transaction consists of two or more lines that must balance to zero: the sum of all debits (positive amounts) must equal the sum of all credits (negative amounts). This constraint is absolute and enforced before any database write occurs.
This guide covers creating manual journals, understanding the VAT annotation system, the draft/posted lifecycle, bank import categorisation, and the account ledger. For the full endpoint reference, see Transactions API.
The balance constraint
The fundamental rule of double-entry bookkeeping in SpeyBooks:
sum of all line amounts = 0
Positive amounts are debits, negative amounts are credits. If the lines do not sum to exactly zero, the API rejects the request with a 400 error. There are no tolerances, rounding adjustments, or auto-balancing. The API consumer is responsible for sending a mathematically complete journal.
Every transaction must have at least two lines.
Creating a journal
A transaction requires a date (YYYY-MM-DD), description (max 500 characters), and an array of lines. Each line requires an accountId (prefixed, e.g. acc_1200) and an amount (integer, minor units). Optional fields per line include vatRate, vatTreatment, contactId, and description (max 500 characters).
The status defaults to posted. Pass status: "draft" to create an editable draft that is excluded from balances and reports.
All referenced account IDs must exist and be active within the organisation. Invalid or inactive accounts return 400.
VAT annotations
VAT fields on transaction lines are tax reporting metadata. They tell the API how to calculate the VAT component of a line's amount for HMRC reporting. They do not create additional balancing lines, modify the journal total, or alter any other line.
When vatRate (integer 0-100) and vatTreatment are provided on a line, the API computes and stores vatAmount. The rate is first converted to a decimal (e.g. 20 becomes 0.20), shown as r below:
| Treatment | Meaning | Calculation |
|---|---|---|
exclusive | Amount is net of VAT | vatAmount = abs(amount) * r |
inclusive | Amount already contains VAT | vatAmount = abs(amount) - (abs(amount) / (1 + r)) |
none | No VAT | No calculation |
For example, recording a £100 net sale with 20% VAT requires three explicit lines:
- Trade Debtors (acc_1200): +12000 (debit, gross)
- Sales (acc_4000): -10000,
vatRate: 20,vatTreatment: "exclusive"(credit, net) - VAT Output (acc_2201): -2000 (credit, VAT)
The API computes vatAmount: 2000 on the Sales line for tax reporting, but it does not inject the VAT Output line. That is the consumer's responsibility.
Transaction statuses
| Status | Editable | In reports | Can reconcile |
|---|---|---|---|
draft | Yes | No | No |
posted | No | Yes | Yes |
Use PATCH /transactions/{id}/status to transition between draft and posted. Reconciled transactions cannot be reverted to draft - unreconcile first.
Source types and deletion rules
Each transaction records its origin:
| Source type | Created by | Can delete directly? |
|---|---|---|
manual | User via API | Yes (draft only) |
invoice | Invoice finalisation (draft to sent) | No, cancel the invoice |
bank_import | Bank import pipeline | No, void the import |
DELETE /transactions/{id} requires both status: "draft" and sourceType: "manual". System-generated transactions are deleted by acting on the source document, preserving the audit trail.
Line-level detail
The get endpoint returns the full transaction with all line fields:
| Field | Description |
|---|---|
id | Prefixed line ID (e.g. line_101) |
accountId, accountCode, accountName, accountType | Target account |
amount | Minor units, positive = debit, negative = credit |
vatRate | Integer percentage, or null |
vatAmount | API-calculated VAT component, or null |
vatTreatment | inclusive, exclusive, none, or null |
contactId, contactName | Optional contact reference |
description | Optional line-level description |
The totals object confirms the balance: debit and credit should be equal, and balanced should be true.
Categorising bank imports
Bank import transactions land with one line against the Suspense account (code 9999). The categorise endpoint moves this line to the correct account.
The categorise endpoint uses a different vatTreatment enum from transaction lines, reflecting HMRC VAT treatment categories:
| Value | Meaning |
|---|---|
standard | Standard rate (20%) |
reduced | Reduced rate (5%) |
zero | Zero-rated |
exempt | VAT exempt |
outside_scope | Outside the scope of VAT |
reverse_charge | Reverse charge |
You can set vatRate, vatTreatment, and contactId in the same request. If the transaction has no Suspense line (already categorised), the endpoint returns 400.
Categorisation actions are logged in the match log for the rules engine feedback loop, creating correction records when the original auto-categorisation was wrong.
Category suggestions
The suggest-category endpoint runs the categorisation rules engine against a transaction and returns a suggested account if a matching rule is found. It evaluates rules in priority order using the transaction description, reference, and Suspense line amount.
The response includes the suggested account, the rule name, and evaluation evidence (rule type, matched field, confidence). If no match is found, matched is false with a reason for debugging.
This is a read-only operation. Use the categorise endpoint to apply the suggestion. For details on how the rules engine works, see Categorisation Rules Guide.
Account ledger
The account ledger provides a traditional ledger view for any account, with opening balance, per-entry debit/credit columns, a running balance, and a closing balance. Only posted transactions are included. Results are sorted by date ascending and paginated (default 50 per page).
If a from date is provided, the opening balance is calculated as the sum of all posted transaction lines before that date. Without a from date, the opening balance is zero (showing all transactions from the beginning).
All monetary values (opening balance, debit, credit, running balance, closing balance) are in minor units.
Reconciliation
Marking a transaction as reconciled confirms it matches a bank statement entry. Only posted transactions can be reconciled.
Set reconciled: true to record the reconciliation timestamp, or false to clear it. Reconciled transactions cannot be reverted to draft status - you must unreconcile first, then change status.
For the full reconciliation workflow, see Bank Reconciliation Guide.
Listing and filtering
The list endpoint returns transactions paginated (default 50, max 100), sorted by date descending. Available filters:
| Parameter | Type | Purpose |
|---|---|---|
from / to | date | Date range (YYYY-MM-DD) |
status | string | draft or posted |
reconciled | boolean | Reconciliation status |
accountId | string | Transactions with a line on this account |
contactId | string | Transactions with a line referencing this contact |
search | string | Case-insensitive partial match on description and reference |
List results include aggregated totals (totalDebit, totalCredit) and account codes but not full line detail. Use the get endpoint for complete line-level data.
Worked example: manual journal entry
Recording a £500 office equipment purchase paid by bank transfer:
- Preview - calculate the amounts: £500 net + £100 VAT = £600 gross
- Create the journal -
POST /transactionswith three lines:- Office Equipment (acc_7000): +50000,
vatRate: 20,vatTreatment: "exclusive"(debit) - VAT Input (acc_2200): +10000 (debit)
- Bank (acc_1200): -60000 (credit)
- Office Equipment (acc_7000): +50000,
- Verify -
GET /transactions/txn_98to confirmbalanced: true - Reconcile -
PATCH /transactions/txn_98/reconcilewhen the payment appears on the bank statement
Related endpoints
- Transactions API - full endpoint reference
- Bank Reconciliation Guide - reconciliation workflow
- Categorisation Rules Guide - how the rules engine works
- Double-Entry Bookkeeping - the accounting model