{"record":{"id":"3783184ae17f25e3","repo":"actualbudget/actual","slug":"balance-is-non-zero-transferaccountid-is-required","errorCode":null,"errorMessage":"balance is non-zero: transferAccountId is required","messagePattern":"balance is non-zero: transferAccountId is required","errorType":"exception","errorClass":"APIError","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/accounts/app.ts","lineNumber":667,"sourceCode":"\n        rows.forEach(row => {\n          if (row.transfer_id) {\n            void db.updateTransaction({\n              id: row.transfer_id,\n              payee: null,\n              transfer_id: null,\n            });\n          }\n\n          void db.deleteTransaction({ id: row.id });\n        });\n\n        void db.deleteAccount({ id });\n        void db.deleteTransferPayee({ id: transferPayee.id });\n      });\n    } else {\n      if (balance !== 0 && transferAccountId == null) {\n        throw APIError('balance is non-zero: transferAccountId is required');\n      }\n\n      if (id === transferAccountId) {\n        throw APIError('transfer account can not be the account being closed');\n      }\n\n      await db.update('accounts', { id, closed: 1 });\n\n      // If there is a balance we need to transfer it to the specified\n      // account (and possibly categorize it)\n      if (balance !== 0 && transferAccountId) {\n        const transferPayee = await db.first<Pick<db.DbPayee, 'id'>>(\n          'SELECT id FROM payees WHERE transfer_acct = ?',\n          [transferAccountId],\n        );\n\n        if (!transferPayee) {\n          throw new Error(","sourceCodeStart":649,"sourceCodeEnd":685,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/accounts/app.ts#L649-L685","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait aql.query(q(\n  api.closeAccount({ id: accountId })\n));\n// after\nawait api.closeAccount({ id: accountId, transferAccountId: savingsAccountId });","handlingStrategy":"validation","validationCode":"const { balance } = await api.accountBalance(accountId);\nif (balance !== 0 && !transferAccountId) {\n  throw new Error(`Account ${accountId} has balance ${balance}; supply transferAccountId or zero it first`);\n}","typeGuard":"function canCloseAccount(a: { id: string; balance: number; transferAccountId?: string }): boolean {\n  return a.balance === 0 || typeof a.transferAccountId === 'string';\n}","tryCatchPattern":"try {\n  await api.closeAccount({ id: accountId, transferAccountId });\n} catch (e) {\n  if (e.message.includes('transferAccountId is required')) {\n    // zero the balance or pick a transfer target, then retry\n  }\n}","preventionTips":["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."],"tags":["api","accounts","validation","loot-core"],"backgroundTag":"transfer-account-required","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}