actualbudget/actual · error · APIError
balance is non-zero: transferAccountId is required
Error message
balance is non-zero: transferAccountId is required
What it means
When closing an account through the API (closeAccount in loot-core), if the account's balance is not zero the caller must supply transferAccountId — the account that will absorb the remaining balance. The server throws this APIError when balance !== 0 and transferAccountId is null/undefined, refusing to close an account that still holds money without saying where it should go.
Source
Thrown at packages/loot-core/src/server/accounts/app.ts:667
rows.forEach(row => {
if (row.transfer_id) {
void db.updateTransaction({
id: row.transfer_id,
payee: null,
transfer_id: null,
});
}
void db.deleteTransaction({ id: row.id });
});
void db.deleteAccount({ id });
void db.deleteTransferPayee({ id: transferPayee.id });
});
} else {
if (balance !== 0 && transferAccountId == null) {
throw APIError('balance is non-zero: transferAccountId is required');
}
if (id === transferAccountId) {
throw APIError('transfer account can not be the account being closed');
}
await db.update('accounts', { id, closed: 1 });
// If there is a balance we need to transfer it to the specified
// account (and possibly categorize it)
if (balance !== 0 && transferAccountId) {
const transferPayee = await db.first<Pick<db.DbPayee, 'id'>>(
'SELECT id FROM payees WHERE transfer_acct = ?',
[transferAccountId],
);
if (!transferPayee) {
throw new Error(View on GitHub (pinned to d4334cb6e6)
Solutions
- Pass a transferAccountId (a different account) so the balance is transferred on close.
- First bring the account balance to zero: reconcile, categorize, or transfer out the funds, then close without transferAccountId.
- Verify the balance with api/accountBalance (or account-balance query) to confirm why it is non-zero.
- If closing should keep the balance history, use the UI flow which prompts for a transfer target.
Example fix
// before
await aql.query(q(
api.closeAccount({ id: accountId })
));
// after
await api.closeAccount({ id: accountId, transferAccountId: savingsAccountId }); Defensive patterns
Strategy: validation
Validate before calling
const { balance } = await api.accountBalance(accountId);
if (balance !== 0 && !transferAccountId) {
throw new Error(`Account ${accountId} has balance ${balance}; supply transferAccountId or zero it first`);
} Type guard
function canCloseAccount(a: { id: string; balance: number; transferAccountId?: string }): boolean {
return a.balance === 0 || typeof a.transferAccountId === 'string';
} Try / catch
try {
await api.closeAccount({ id: accountId, transferAccountId });
} catch (e) {
if (e.message.includes('transferAccountId is required')) {
// zero the balance or pick a transfer target, then retry
}
} Prevention
- Check the account balance with the account-balance query before closing.
- Always supply transferAccountId when programmatically closing non-empty accounts.
- Reconcile transactions first so the balance is intentionally zero.
- Mirror server-side validation in scripts to fail fast with a clearer message.
When it happens
Trigger: Calling api/closeAccount (or the UI's close-account flow via the API) with { id } for an account whose computed balance is non-zero and without a transferAccountId field.
Common situations: Programmatic API scripts closing accounts after reconciling but before zeroing them; automations that assume closing archives the account without balancing; account still has unreconciled transactions giving an unexpected non-zero balance.
Related errors
- transfer account can not be the account being closed
- Invalid --name: must be a non-empty string.
- No update fields provided. Use --name or --offbudget.
- Unknown payee name normalization: ${String(normalization)}
- `date` is required when adding a transaction
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/3783184ae17f25e3.
Report an issue: GitHub.