actualbudget/actual · error
Transfer payee with account ID ${transferAccountId} not foun
Error message
Transfer payee with account ID ${transferAccountId} not found. What it means
closeAccount is moving the balance of the account being closed to a transfer account. It looks up the built-in transfer payee associated with the target account (payees.transfer_acct = transferAccountId). If no such payee row exists, Actual cannot record the balancing transfer transaction and throws this error.
Source
Thrown at packages/loot-core/src/server/accounts/app.ts:685
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.`,
);
}
await mainApp.handlers['transaction-add']({
id: uuidv4(),
payee: transferPayee.id,
amount: -balance,
account: id,
date: monthUtils.currentDay(),
notes: 'Closing account',
category: categoryId,
});
}
}
});
}
View on GitHub (pinned to d4334cb6e6)
Solutions
- Verify transferAccountId refers to an open account in the same budget file (run the query: SELECT id FROM accounts WHERE id = ?).
- Recreate the missing transfer payee: insert a payee row with transfer_acct set to the target account ID (or toggle a transfer between two accounts in the UI so Actual regenerates it).
- If the payee was deleted by cleanup scripts/SQL, restore it or clear transfer_acct handling by re-syncing the budget from a known-good copy.
- As a workaround, pass null for transferAccountId and let the user reconcile the closed account's balance manually instead of transferring.
Example fix
// before
await runHandler('accounts-close', { id: accountId, transferAccountId: someIdFromAnotherBudget });
// after
const acct = await q('accounts').select('id').where({ id: someIdFromAnotherBudget }).first();
const payee = await db.first('SELECT id FROM payees WHERE transfer_acct = ?', [someIdFromAnotherBudget]);
if (!payee) throw new Error('Target account has no transfer payee; recreate it or pass transferAccountId: null');
await runHandler('accounts-close', { id: accountId, transferAccountId: someIdFromAnotherBudget }); Defensive patterns
Strategy: validation
Validate before calling
const transferPayee = await db.first(
'SELECT id FROM payees WHERE transfer_acct = ?',
[transferAccountId],
);
if (!transferPayee) {
throw new Error(
`Cannot close account: transfer account ${transferAccountId} has no transfer payee. Recreate it or close without a transfer.`,
);
} Type guard
function hasTransferPayee(
p: Pick<{ id: string }, 'id'> | null | undefined,
): p is { id: string } {
return p != null && typeof p.id === 'string';
} Try / catch
try {
await send('accounts-close', { id: accountId, transferAccountId });
} catch (e) {
if (e.message.includes('Transfer payee')) {
await send('accounts-close', { id: accountId, transferAccountId: null });
} else {
throw e;
}
} Prevention
- Never delete payee rows with a non-null transfer_acct during manual DB cleanup.
- Validate transferAccountId belongs to the same budget before closing.
- After imports/migrations, run a query for accounts missing transfer payees and backfill them.
- Offer closing-without-transfer (transferAccountId: null) as a fallback in tooling.
When it happens
Trigger: Calling the 'accounts-close' handler (closeAccount) with transferAccountId set to an account ID that exists in the accounts table but has no corresponding payee row with transfer_acct = <transferAccountId> — i.e. the target account's auto-created transfer payee is missing from the payees table.
Common situations: Budgets edited or migrated from older versions where the transfer payee was deleted or never backfilled; manual cleanup of the payees table that removed 'orphan-looking' transfer payees; imports or merges that copied accounts without their transfer payees; passing an account ID from a different budget file.
Related errors
- Transfer payee with account ID ${id} not found.
- Bank with ID ${bankId} not found.
- Subtransaction amount is invalid, must be an integer: ${sub.
- `payeeName` is required when adding a transaction
- Unrecognized bank-sync provider: ${acctRow.account_sync_sour
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/251b16b45fe9d0b5.
Report an issue: GitHub.