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

  1. Pass a transferAccountId (a different account) so the balance is transferred on close.
  2. First bring the account balance to zero: reconcile, categorize, or transfer out the funds, then close without transferAccountId.
  3. Verify the balance with api/accountBalance (or account-balance query) to confirm why it is non-zero.
  4. 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

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


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/3783184ae17f25e3. Report an issue: GitHub.