actualbudget/actual · error

Transfer payee with account ID ${id} not found.

Error message

Transfer payee with account ID ${id} not found.

What it means

When closing an account that has a linked transfer account, closeAccount looks up the transfer payee (the auto-created payee whose transfer_acct points at the account being closed). If that payee is missing the invariant is broken and the close is aborted, since Actual must retarget the transfer payee's transactions to keep transfers consistent.

Source

Thrown at packages/loot-core/src/server/accounts/app.ts:640

    // If there are no transactions, we can simply delete the account
    if (numTransactions === 0) {
      await db.deleteAccount({ id });
    } else if (forced) {
      const rows = db.runQuery<
        Pick<db.DbViewTransaction, 'id' | 'transfer_id'>
      >(
        'SELECT id, transfer_id FROM v_transactions WHERE account = ?',
        [id],
        true,
      );

      const transferPayee = await db.first<Pick<db.DbPayee, 'id'>>(
        'SELECT id FROM payees WHERE transfer_acct = ?',
        [id],
      );

      if (!transferPayee) {
        throw new Error(`Transfer payee with account ID ${id} not found.`);
      }

      await batchMessages(async () => {
        // TODO: what this should really do is send a special message that
        // automatically marks the tombstone value for all transactions
        // within an account... or something? This is problematic
        // because another client could easily add new data that
        // should be marked as deleted.

        rows.forEach(row => {
          if (row.transfer_id) {
            void db.updateTransaction({
              id: row.transfer_id,
              payee: null,
              transfer_id: null,
            });
          }

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Recreate the missing transfer payee: insert a payee with transfer_acct set to the account id, then retry closeAccount.
  2. Inspect the payees table ('SELECT * FROM payees WHERE transfer_acct = ?') to confirm whether the link exists before closing.
  3. If the transfer relationship is unwanted, close the account without a transfer account so this path is skipped.
  4. Restore the payee from a budget backup if the data was deleted accidentally.

Example fix

// before
await send('close-account', { id, transferAccountId, forceCloseAccount });

// after
const payee = await q('SELECT id FROM payees WHERE transfer_acct = ?', [id]);
if (!payee) {
  await send('create-transfer-payee', { accountId: id }); // recreate missing payee
}
await send('close-account', { id, transferAccountId, forceCloseAccount });
Defensive patterns

Strategy: validation

Validate before calling

const transferPayee = await db.first('SELECT id FROM payees WHERE transfer_acct = ?', [accountId]);
if (!transferPayee && transferAccountId) {
  throw new Error('Transfer payee missing; recreate it or close without a transfer account');
}

Type guard

async function hasTransferPayee(accountId: string): Promise<boolean> {
  return !!(await db.first('SELECT id FROM payees WHERE transfer_acct = ?', [accountId]));
}

Try / catch

try {
  await send('close-account', { id, transferAccountId, forceCloseAccount });
} catch (e) {
  if (/Transfer payee with account ID .* not found/.test(e.message)) {
    // recreate payee or retry with forceCloseAccount / no transfer account
  } else throw e;
}

Prevention

When it happens

Trigger: Calling closeAccount with forceCloseAccount/transfer account options on an account whose transfer payee row was deleted directly (manual SQL, older data, or a buggy migration), so 'SELECT id FROM payees WHERE transfer_acct = ?' returns nothing.

Common situations: Budgets edited via raw SQL or the API deleting payees without cleaning transfer_acct links; data imported from other tools lacking transfer payees; budgets restored from partial backups.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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