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

  1. Verify transferAccountId refers to an open account in the same budget file (run the query: SELECT id FROM accounts WHERE id = ?).
  2. 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).
  3. 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.
  4. 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

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


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