actualbudget/actual · error · APIError
transfer account can not be the account being closed
Error message
transfer account can not be the account being closed
What it means
closeAccount rejects attempts to close an account into itself: if the supplied transferAccountId equals the id of the account being closed, the transfer would be a no-op while the account's balance would silently vanish, so the server throws 'transfer account can not be the account being closed'.
Source
Thrown at packages/loot-core/src/server/accounts/app.ts:671
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(
`Transfer payee with account ID ${transferAccountId} not found.`,
);
}
View on GitHub (pinned to d4334cb6e6)
Solutions
- Choose a different transferAccountId than the account being closed.
- If no transfer is needed, bring the balance to zero first and close without transferAccountId.
- Add a client-side check id !== transferAccountId before invoking the API.
Example fix
// before
await api.closeAccount({ id: accId, transferAccountId: accId });
// after
const target = accId === savingsId ? checkingId : savingsId;
await api.closeAccount({ id: accId, transferAccountId: target }); Defensive patterns
Strategy: validation
Validate before calling
if (transferAccountId === accountId) {
throw new Error('transferAccountId must differ from the account being closed');
} Type guard
function hasValidTransferTarget(a: { id: string; transferAccountId?: string }): boolean {
return a.transferAccountId == null || a.transferAccountId !== a.id;
} Try / catch
try {
await api.closeAccount({ id: accountId, transferAccountId });
} catch (e) {
if (e.message.includes('can not be the account being closed')) {
transferAccountId = await promptForDifferentTransferAccount();
return closeAccountSafely(accountId, transferAccountId);
}
} Prevention
- Never default the transfer target to the account being closed in UIs or scripts.
- Assert id !== transferAccountId before calling closeAccount.
- In batch jobs, derive the transfer target from a mapping rather than the loop variable.
- When balance is zero, omit transferAccountId entirely to sidestep the check.
When it happens
Trigger: Calling closeAccount with { id: X, transferAccountId: X } for a non-zero-balance account — the destination account for the balance transfer is the same account being closed.
Common situations: Scripts that set transferAccountId from the same variable as the account id (copy-paste or templating bug); UI integrations that default the transfer dropdown to the closing account; batch-closing code that assigns a fallback transfer account without checking identity.
Related errors
- balance is non-zero: transferAccountId is required
- 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/1e9081c5ba9eb98e.
Report an issue: GitHub.