diff --git a/pom.xml b/pom.xml
index b942c53a5dd01548fee6ae7d81bd110e56e68088..f47d0f176c8d12fb4fcf1a816044c878b88a91da 100755
--- a/pom.xml
+++ b/pom.xml
@@ -266,6 +266,21 @@
             <version>2.0.11</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi</artifactId>
+            <version>5.3.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+            <version>5.3.0</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.13.0</version>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/src/main/java/no/nibio/vips/logic/service/ObservationService.java b/src/main/java/no/nibio/vips/logic/service/ObservationService.java
index daf359258690e26b9c4a7393a5b2ce1e945554c3..c23dd322845d1f9a574eed3192676edd64ea4a7d 100755
--- a/src/main/java/no/nibio/vips/logic/service/ObservationService.java
+++ b/src/main/java/no/nibio/vips/logic/service/ObservationService.java
@@ -32,6 +32,7 @@ import no.nibio.vips.logic.entity.rest.ObservationListItem;
 import no.nibio.vips.logic.entity.rest.PointMappingResponse;
 import no.nibio.vips.logic.entity.rest.ReferencedPoint;
 import no.nibio.vips.logic.messaging.MessagingBean;
+import no.nibio.vips.logic.util.ExcelFileGenerator;
 import no.nibio.vips.logic.util.GISEntityUtil;
 import no.nibio.vips.logic.util.Globals;
 import org.jboss.resteasy.annotations.GZIP;
@@ -59,6 +60,8 @@ import java.net.URI;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -130,12 +133,13 @@ public class ObservationService {
     }
 
     /**
-     * @param organizationId Database ID of the organization
-     * @param pestId         Database ID of the pest
-     * @param cropId         Database ID of the crop
-     * @param cropCategoryId cropCategoryId Database IDs of the crop category/categories
-     * @param fromStr        format "yyyy-MM-dd"
-     * @param toStr          format "yyyy-MM-dd"
+     * @param organizationId          Database ID of the organization
+     * @param observationTimeSeriesId Database ID of the observation time series
+     * @param pestId                  Database ID of the pest
+     * @param cropId                  Database ID of the crop
+     * @param cropCategoryId          cropCategoryId Database IDs of the crop category/categories
+     * @param fromStr                 format "yyyy-MM-dd"
+     * @param toStr                   format "yyyy-MM-dd"
      * @return Observation objects for which the user is authorized to observe with properties relevant for lists
      */
     @GET
@@ -158,6 +162,55 @@ public class ObservationService {
         return Response.ok().entity(this.getFilteredObservationListItems(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive)).build();
     }
 
+    /**
+     * @param organizationId          Database ID of the organization
+     * @param observationTimeSeriesId Database ID of the observation time series
+     * @param pestId                  Database ID of the pest
+     * @param cropId                  Database ID of the crop
+     * @param cropCategoryId          cropCategoryId Database IDs of the crop category/categories
+     * @param fromStr                 format "yyyy-MM-dd"
+     * @param toStr                   format "yyyy-MM-dd"
+     * @return Observation objects for which the user is authorized to observe with properties relevant for lists
+     */
+    @GET
+    @Path("list/filter/{organizationId}/xlsx")
+    @GZIP
+    @Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+    public Response getFilteredObservationListItemsAsXlsx(
+            @PathParam("organizationId") Integer organizationId,
+            @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId,
+            @QueryParam("pestId") Integer pestId,
+            @QueryParam("cropId") Integer cropId,
+            @QueryParam("cropCategoryId") List<Integer> cropCategoryId,
+            @QueryParam("from") String fromStr,
+            @QueryParam("to") String toStr,
+            @QueryParam("userUUID") String userUUID,
+            @QueryParam("locale") String localeStr,
+            @QueryParam("isPositive") Boolean isPositive
+    ) {
+        LOGGER.info("Generate xlsx file for observations");
+        VipsLogicUser user = getVipsLogicUser(userUUID);
+        ULocale locale = new ULocale(localeStr != null ? localeStr :
+                user != null ? user.getOrganizationId().getDefaultLocale() :
+                        userBean.getOrganization(organizationId).getDefaultLocale());
+
+
+        LocalDateTime now = LocalDateTime.now();
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+        String filenameTimestamp = now.format(formatter);
+
+        try {
+            byte[] excelFile = ExcelFileGenerator.generateExcel(getFilteredObservationListItems(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive), locale);
+
+            return Response
+                    .ok(excelFile)
+                    .header("Content-Disposition", "attachment; filename=\"" +filenameTimestamp+ "-observations.xlsx\"")
+                    .build();
+        } catch (IOException e) {
+            return Response.serverError().entity("Error generating Excel file: " + e.getMessage()).build();
+        }
+    }
+
     private List<ObservationListItem> getFilteredObservationListItems(
             Integer organizationId,
             Integer observationTimeSeriesId,
@@ -169,11 +222,7 @@ public class ObservationService {
             String userUUID,
             String localeStr,
             Boolean isPositive) {
-        VipsLogicUser user = (VipsLogicUser) httpServletRequest.getSession().getAttribute("user");
-
-        if (user == null && userUUID != null) {
-            user = userBean.findVipsLogicUser(UUID.fromString(userUUID));
-        }
+        VipsLogicUser user = getVipsLogicUser(userUUID);
         ULocale locale = new ULocale(localeStr != null ? localeStr :
                 user != null ? user.getOrganizationId().getDefaultLocale() :
                         userBean.getOrganization(organizationId).getDefaultLocale());
@@ -766,14 +815,14 @@ public class ObservationService {
     }
 
     /**
-     * @param organizationId Database id of the organization
+     * @param organizationId          Database id of the organization
      * @param observationTimeSeriesId Database id of the observation time series
-     * @param pestId         Database id of the pest
-     * @param cropId         Database id of the crop
-     * @param cropCategoryId Database ids of the crop categories
-     * @param fromStr        format "yyyy-MM-dd"
-     * @param toStr          format "yyyy-MM-dd"
-     * @param user           The user that requests this (used for authorization)
+     * @param pestId                  Database id of the pest
+     * @param cropId                  Database id of the crop
+     * @param cropCategoryId          Database ids of the crop categories
+     * @param fromStr                 format "yyyy-MM-dd"
+     * @param toStr                   format "yyyy-MM-dd"
+     * @param user                    The user that requests this (used for authorization)
      * @return A list of observations that meets the filter criteria
      */
     private List<Observation> getFilteredObservationsFromBackend(
@@ -803,10 +852,10 @@ public class ObservationService {
             LOGGER.info("Return {} masked public observations for unregistered user", retVal.size());
             return sortObservationsByDateAndId(retVal);
         }
-         // Else: This is a registered user without special privileges. Show public observations + user's own
+        // Else: This is a registered user without special privileges. Show public observations + user's own
         // Making sure we don't add duplicates
-        Set<Integer> obsIds = retVal.stream().map(o->o.getObservationId()).collect(Collectors.toSet());
-        retVal.addAll(observationBean.getObservationsForUser(user).stream().filter(o->!obsIds.contains(o.getObservationId())).collect(Collectors.toList()));
+        Set<Integer> obsIds = retVal.stream().map(o -> o.getObservationId()).collect(Collectors.toSet());
+        retVal.addAll(observationBean.getObservationsForUser(user).stream().filter(o -> !obsIds.contains(o.getObservationId())).collect(Collectors.toList()));
         LOGGER.info("Return {} masked public observations and user's own observations for registered user {}", retVal.size(), user.getUserId());
         return sortObservationsByDateAndId(retVal);
     }
@@ -1085,6 +1134,21 @@ public class ObservationService {
                 .collect(Collectors.toList());
     }
 
+    /**
+     * Find VipsLogic user from session or given userUUID
+     *
+     * @param userUUID the UUID of the user
+     * @return the corresponding VipsLogicUser
+     */
+    private VipsLogicUser getVipsLogicUser(String userUUID) {
+        VipsLogicUser user = (VipsLogicUser) httpServletRequest.getSession().getAttribute("user");
+        if (user == null && userUUID != null) {
+            user = userBean.findVipsLogicUser(UUID.fromString(userUUID));
+        }
+        return user;
+    }
+
+
     /**
      * Utility method for getting the string value of a given property in a given map.
      *
diff --git a/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java b/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java
new file mode 100644
index 0000000000000000000000000000000000000000..4380680bd9a820e78107e340796ee2a4256e8206
--- /dev/null
+++ b/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java
@@ -0,0 +1,54 @@
+package no.nibio.vips.logic.util;
+
+import com.ibm.icu.util.ULocale;
+import no.nibio.vips.logic.entity.rest.ObservationListItem;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.apache.poi.ss.usermodel.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.ResourceBundle;
+
+public class ExcelFileGenerator {
+    public static byte[] generateExcel(List<ObservationListItem> filteredObservationListItems, ULocale locale) throws IOException {
+        try (XSSFWorkbook workbook = new XSSFWorkbook();
+             ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+
+            ResourceBundle rb = ResourceBundle.getBundle("no.nibio.vips.logic.i18n.vipslogictexts", locale.toLocale());
+            // Prøvde å bruke i18n, fikk Invalid char (/) found at index (13) in sheet name 'Observasjoner/f?rstefunn'
+            Sheet sheet = workbook.createSheet("Observations");
+
+            Row headerRow = sheet.createRow(0);
+            headerRow.createCell(0).setCellValue(rb.getString("timeOfObservation"));
+            headerRow.createCell(1).setCellValue(rb.getString("organism"));
+            headerRow.createCell(2).setCellValue(rb.getString("cropOrganismId"));
+            headerRow.createCell(3).setCellValue(rb.getString("observationHeading"));
+
+            int rowNum = 1;
+            for (ObservationListItem item : filteredObservationListItems) {
+                // TODO Better way of getting current timezone?
+                LocalDate localDateOfObservation = item.getTimeOfObservation().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+                Row row = sheet.createRow(rowNum++);
+                row.createCell(0).setCellValue(localDateOfObservation.format(formatter));
+                row.createCell(1).setCellValue(item.getOrganismName());
+                row.createCell(2).setCellValue(item.getCropOrganismName());
+                row.createCell(3).setCellValue(item.getObservationHeading());
+
+            }
+
+            // Auto-size columns to fit content
+            for (int i = 0; i <= 3; i++) {
+                sheet.autoSizeColumn(i);
+            }
+
+            workbook.write(out);
+            return out.toByteArray();
+        }
+    }
+}
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
index 1e03844cc22e2c12fe721b2eb34d16f56e097f44..db00fc266365213c9c50b7885c415baa578a339a 100755
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
@@ -354,7 +354,7 @@ greeting = Velkommen til
 
 groupMembers = Gruppemedlemmer
 
-heading = Overskrift
+heading = Tittel
 
 help = Hjelp
 
@@ -558,7 +558,7 @@ observationDataField_unit = M\u00e5leenhet
 
 observationDeleted = Observasjonen ble slettet
 
-observationHeading = Observasjons-overskrift
+observationHeading = Observasjonstittel
 
 observationMap = Observasjonskart
 
@@ -894,7 +894,7 @@ thresholdRelativeHumidity = Terskelverdi relativ luftfuktighet (%)
 
 tillageMethod = Jordarbeiding
 
-timeOfObservation = Observasjonstidspunkt
+timeOfObservation = Observasjonsdato
 
 timeZone = Tidssone