diff --git a/src/main/java/de/ozgcloud/xta/client/XtaClient.java b/src/main/java/de/ozgcloud/xta/client/XtaClient.java
index b487ce9dcea7eecc81a8060f9e7c79c86dc6b928..33a7fb6de273a8d9b8918ea3eb2d782193e0a392 100644
--- a/src/main/java/de/ozgcloud/xta/client/XtaClient.java
+++ b/src/main/java/de/ozgcloud/xta/client/XtaClient.java
@@ -10,6 +10,7 @@ import jakarta.validation.constraints.NotNull;
 
 import de.ozgcloud.xta.client.config.XtaClientConfig;
 import de.ozgcloud.xta.client.core.XtaClientService;
+import de.ozgcloud.xta.client.exception.ClientInitializationException;
 import de.ozgcloud.xta.client.exception.ClientRuntimeException;
 import de.ozgcloud.xta.client.model.XtaIdentifier;
 import de.ozgcloud.xta.client.model.XtaMessage;
@@ -19,20 +20,22 @@ import de.ozgcloud.xta.client.model.XtaMessageStatus;
 import de.ozgcloud.xta.client.model.XtaTransportReport;
 import lombok.AccessLevel;
 import lombok.Builder;
-import lombok.Getter;
 import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
+import lombok.extern.log4j.Log4j2;
 
 @RequiredArgsConstructor(access = AccessLevel.MODULE)
 @Builder(access = AccessLevel.MODULE)
-@Getter(AccessLevel.MODULE)
-@Slf4j
+@Log4j2
 public class XtaClient {
 
 	private final XtaClientService service;
 	private final XtaClientConfig config;
 	private final FetchMessageParameterFactory fetchMessageParameterFactory;
 
+	public static XtaClient from(XtaClientConfig config) throws ClientInitializationException {
+		return XtaClientFactory.from(config).create();
+	}
+
 	public List<XtaTransportReport> fetchMessages(@NotNull Consumer<XtaMessage> processMessage) {
 		return config.getClientIdentifiers().stream()
 				.filter(service::checkAccountActive)
diff --git a/src/main/java/de/ozgcloud/xta/client/XtaClientFactory.java b/src/main/java/de/ozgcloud/xta/client/XtaClientFactory.java
index 4cc93a31671f9acbe497bc1c18fc4016a66ad785..1a38191f8e48de88d24472048886c696f68d5e82 100644
--- a/src/main/java/de/ozgcloud/xta/client/XtaClientFactory.java
+++ b/src/main/java/de/ozgcloud/xta/client/XtaClientFactory.java
@@ -30,6 +30,7 @@ public class XtaClientFactory {
 		return XtaClient.builder()
 				.config(config)
 				.service(xtaClientServiceFactory.create())
+				.fetchMessageParameterFactory(fetchMessageParameterFactory)
 				.build();
 	}
 }
diff --git a/src/main/java/de/ozgcloud/xta/client/config/XtaClientConfig.java b/src/main/java/de/ozgcloud/xta/client/config/XtaClientConfig.java
index b2b86031eabcbaa1d81d79884c548315ea2fe529..fed39a36ebad655d2d1691a14066d7037aae3fd4 100644
--- a/src/main/java/de/ozgcloud/xta/client/config/XtaClientConfig.java
+++ b/src/main/java/de/ozgcloud/xta/client/config/XtaClientConfig.java
@@ -11,12 +11,13 @@
  */
 package de.ozgcloud.xta.client.config;
 
+import static java.util.Collections.*;
+
 import java.util.List;
 import java.util.function.Predicate;
 
 import jakarta.validation.Valid;
 import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotEmpty;
 import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Positive;
 
@@ -33,8 +34,8 @@ import lombok.ToString;
 @ToString
 public class XtaClientConfig {
 
-	@NotEmpty(message = "at least one client identifier is required")
-	private final List<@Valid XtaIdentifier> clientIdentifiers;
+	@Builder.Default
+	private final List<@Valid XtaIdentifier> clientIdentifiers = emptyList();
 
 	@Builder.Default
 	private final Predicate<XtaMessageMetaData> isMessageSupported = null;
diff --git a/src/main/java/de/ozgcloud/xta/client/model/XtaFile.java b/src/main/java/de/ozgcloud/xta/client/model/XtaFile.java
index 89b2d1e6d1d02da3b75c5ac44b209bf5552894f2..2561f45d22fcc92d3d4e729c78a40d741d470bc3 100644
--- a/src/main/java/de/ozgcloud/xta/client/model/XtaFile.java
+++ b/src/main/java/de/ozgcloud/xta/client/model/XtaFile.java
@@ -10,7 +10,7 @@ import jakarta.validation.constraints.PositiveOrZero;
 
 import lombok.Builder;
 
-@Builder
+@Builder(toBuilder = true)
 public record XtaFile(
 		@NotNull DataHandler content,
 		@NotBlank String contentType,
diff --git a/src/test/java/de/ozgcloud/xta/client/XtaClientFactoryTest.java b/src/test/java/de/ozgcloud/xta/client/XtaClientFactoryTest.java
index a5fecab3f1f237a0821244a54bce8296d3f62cf9..1e3dbfcb7924306ca7bc604632bb68d7f97f1ab7 100644
--- a/src/test/java/de/ozgcloud/xta/client/XtaClientFactoryTest.java
+++ b/src/test/java/de/ozgcloud/xta/client/XtaClientFactoryTest.java
@@ -24,6 +24,8 @@ class XtaClientFactoryTest {
 	private XtaClientServiceFactory xtaClientServiceFactory;
 	@Mock
 	private XtaClientConfig config;
+	@Mock
+	private FetchMessageParameterFactory fetchMessageParameterFactory;
 
 	@InjectMocks
 	private XtaClientFactory factory;
@@ -41,24 +43,6 @@ class XtaClientFactoryTest {
 			when(xtaClientServiceFactory.create()).thenReturn(service);
 		}
 
-		@DisplayName("should have service")
-		@Test
-		@SneakyThrows
-		void shouldHaveService() {
-			var client = factory.create();
-
-			assertThat(client.getService()).isEqualTo(service);
-		}
-
-		@DisplayName("should have config")
-		@Test
-		@SneakyThrows
-		void shouldHaveConfig() {
-			var client = factory.create();
-
-			assertThat(client.getConfig()).isEqualTo(config);
-		}
-
 		@DisplayName("should call validate")
 		@Test
 		@SneakyThrows
@@ -67,5 +51,18 @@ class XtaClientFactoryTest {
 
 			verify(configValidator).validate(config);
 		}
+
+		@DisplayName("should return")
+		@Test
+		@SneakyThrows
+		void shouldReturn() {
+			assertThat(factory.create())
+					.usingRecursiveComparison()
+					.isEqualTo(XtaClient.builder()
+							.config(config)
+							.service(service)
+							.fetchMessageParameterFactory(fetchMessageParameterFactory)
+							.build());
+		}
 	}
 }
\ No newline at end of file
diff --git a/src/test/java/de/ozgcloud/xta/client/XtaClientITCase.java b/src/test/java/de/ozgcloud/xta/client/XtaClientITCase.java
index 547ab02aead29e2988dac9cbebe6606ea277b6cb..f4bb271710a531526fa5be5dfe7cbc28feb13d3e 100644
--- a/src/test/java/de/ozgcloud/xta/client/XtaClientITCase.java
+++ b/src/test/java/de/ozgcloud/xta/client/XtaClientITCase.java
@@ -1,37 +1,321 @@
 package de.ozgcloud.xta.client;
 
 import static de.ozgcloud.xta.client.extension.XtaServerSetupExtensionTestUtil.*;
+import static java.util.Collections.*;
 import static org.assertj.core.api.Assertions.*;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
+import de.ozgcloud.common.errorhandling.TechnicalException;
 import de.ozgcloud.xta.client.extension.XtaMessageExampleLoader;
 import de.ozgcloud.xta.client.extension.XtaTestServerSetupExtension;
+import de.ozgcloud.xta.client.model.XtaIdentifier;
 import de.ozgcloud.xta.client.model.XtaMessage;
+import de.ozgcloud.xta.client.model.XtaMessageMetaData;
 import de.ozgcloud.xta.client.model.XtaMessageStatus;
+import de.ozgcloud.xta.client.model.XtaTransportReport;
 import lombok.SneakyThrows;
 
 class XtaClientITCase {
 
 	@RegisterExtension
 	static final XtaTestServerSetupExtension XTA_TEST_SERVER_SETUP_EXTENSION = new XtaTestServerSetupExtension();
+	static final int TWO_MAX_LIST_ITEMS = 2;
+
+	private XtaClient setupClient;
+	private XtaClient testClient;
 
-	private XtaClient client;
+	private List<XtaMessageMetaData> supportCheckedMetadataItems;
+	private List<XtaMessage> processedMessages;
+	private Consumer<XtaMessage> processMessageDummy;
+	private Predicate<XtaMessageMetaData> isSupportedDummy;
 
 	@BeforeEach
 	@SneakyThrows
 	void setup() {
-		client = XTA_TEST_SERVER_SETUP_EXTENSION.getClient();
+		processMessageDummy = (message) -> {
+		};
+		supportCheckedMetadataItems = new ArrayList<>();
+		isSupportedDummy = (metaData) -> true;
+		processedMessages = new ArrayList<>();
+		setupClient = XTA_TEST_SERVER_SETUP_EXTENSION.getSetupClient();
+
+		closeMessagesForAllReaders();
+	}
+
+	private void closeMessagesForAllReaders() {
+		Stream.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3)
+				.forEach(clientId -> closeAllMessages(setupClient, clientId));
 	}
 
 	@DisplayName("fetch messages")
 	@Nested
 	class TestFetchMessages {
-		// TODO KOP-2733
+
+		private List<XtaMessage> sendMessages;
+
+		@BeforeEach
+		void setup() {
+			sendMessages = List.of(
+					createMessage("dfoerdermittel", AUTHOR_CLIENT_IDENTIFIER, READER_CLIENT_IDENTIFIER1),
+					createMessage("dfoerdermittel", AUTHOR_CLIENT_IDENTIFIER, READER_CLIENT_IDENTIFIER2),
+					createMessage("abgabe0401-kleiner-waffenschein", AUTHOR_CLIENT_IDENTIFIER, READER_CLIENT_IDENTIFIER2),
+					createMessage("versammlungsanzeige", AUTHOR_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER2),
+					createMessage("versammlungsanzeige", AUTHOR_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3),
+					createMessage("versammlungsanzeige", AUTHOR_CLIENT_IDENTIFIER3, READER_CLIENT_IDENTIFIER3)
+			);
+			sendMessages.forEach(message -> sendTestMessage(setupClient, message));
+		}
+
+		private XtaMessage createMessage(String messageLabel, XtaIdentifier author, XtaIdentifier reader) {
+			return XtaMessageExampleLoader.load(
+					XtaMessageExampleLoader.MessageExampleConfig.builder()
+							.messageLabel(messageLabel)
+							.reader(reader)
+							.author(author)
+							.build());
+		}
+
+		@DisplayName("should fetch no messages if no client identifier is configured")
+		@Test
+		void shouldFetchNoMessagesIfNoClientIdentifierIsConfigured() {
+			setupClientWithIdentifiers(emptyList());
+
+			var messages = fetchMessages();
+
+			assertThat(supportCheckedMetadataItems).isEmpty();
+			assertThat(processedMessages).isEmpty();
+			assertThat(messages).isEmpty();
+		}
+
+		@DisplayName("should fetch no messages if client identifier has no messages pending")
+		@Test
+		void shouldFetchNoMessagesIfClientIdentifierHasNoMessagesPending() {
+			setupClientWithIdentifiers(List.of(AUTHOR_CLIENT_IDENTIFIER));
+
+			var messages = fetchMessages();
+
+			assertThat(supportCheckedMetadataItems).isEmpty();
+			assertThat(processedMessages).isEmpty();
+			assertThat(messages).isEmpty();
+		}
+
+		@DisplayName("should fetch messages from first reader")
+		@Test
+		@SneakyThrows
+		void shouldFetchMessagesFromFirstReader() {
+			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1));
+
+			var transportReports = fetchMessages();
+
+			assertThat(supportCheckedMetadataItems).hasSize(1);
+			assertThatMessages(processedMessages).containExactlyInAnyOrder(sendMessages.getFirst());
+			assertThatTransportReports(transportReports)
+					.reportExactlyFor(processedMessages)
+					.haveExactlyGreenStatusFor(messageIdBySendIndex(0));
+		}
+
+		@DisplayName("should fetch messages from second reader")
+		@Test
+		void shouldFetchMessagesFromSecondReader() {
+			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER2));
+
+			var transportReports = fetchMessages();
+
+			assertThat(supportCheckedMetadataItems).hasSize(3);
+			assertThatMessages(processedMessages).containExactlyInAnyOrder(sendMessages.get(1), sendMessages.get(2), sendMessages.get(3));
+			assertThatTransportReports(transportReports)
+					.reportExactlyFor(processedMessages)
+					.haveExactlyGreenStatusFor(messageIdBySendIndex(1), messageIdBySendIndex(2), messageIdBySendIndex(3));
+		}
+
+		@DisplayName("should fetch messages from first and second reader")
+		@Test
+		void shouldFetchMessagesFromFirstAndSecondReader() {
+			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2));
+
+			var transportReports = fetchMessages();
+
+			assertThat(supportCheckedMetadataItems).hasSize(1 + 3);
+			assertThatMessages(processedMessages).containExactlyInAnyOrder(
+					sendMessages.get(0),
+					sendMessages.get(1), sendMessages.get(2), sendMessages.get(3)
+			);
+			assertThatTransportReports(transportReports)
+					.reportExactlyFor(processedMessages)
+					.haveExactlyGreenStatusFor(
+							messageIdBySendIndex(0),
+							messageIdBySendIndex(1), messageIdBySendIndex(2), messageIdBySendIndex(3)
+					);
+		}
+
+		@DisplayName("should fetch messages from first, second and third reader")
+		@Test
+		void shouldFetchMessagesFromFirstSecondAndThirdReader() {
+			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3));
+
+			var transportReports = fetchMessages();
+
+			assertThat(supportCheckedMetadataItems).hasSize(1 + 3 + 2);
+			assertThatMessages(processedMessages).containExactlyInAnyOrder(
+					sendMessages.get(0),
+					sendMessages.get(1), sendMessages.get(2), sendMessages.get(3),
+					sendMessages.get(4), sendMessages.get(5)
+			);
+			assertThatTransportReports(transportReports)
+					.reportExactlyFor(processedMessages)
+					.haveExactlyGreenStatusFor(
+							messageIdBySendIndex(0),
+							messageIdBySendIndex(1), messageIdBySendIndex(2), messageIdBySendIndex(3),
+							messageIdBySendIndex(4), messageIdBySendIndex(5)
+					);
+		}
+
+		@DisplayName("should close messages only if no exception occurs during processing, with no exception for author1")
+		@Test
+		void shouldCloseMessagesOnlyIfNoExceptionOccursDuringProcessingWithNoExceptionForAuthor1() {
+			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3));
+			processMessageDummy = message -> throwRuntimeExceptionExceptForAuthorIdentifier(message, AUTHOR_CLIENT_IDENTIFIER);
+
+			var transportReports = fetchMessages();
+
+			assertThat(supportCheckedMetadataItems).hasSize(1 + 3 + 2);
+			assertThatMessages(processedMessages).containExactlyInAnyOrder(
+					sendMessages.get(0),
+					sendMessages.get(1), sendMessages.get(2), sendMessages.get(3),
+					sendMessages.get(4), sendMessages.get(5)
+			);
+			assertThatTransportReports(transportReports)
+					.reportExactlyFor(processedMessages)
+					.haveExactlyGreenStatusFor(
+							messageIdBySendIndex(0),
+							messageIdBySendIndex(1), messageIdBySendIndex(2)
+					);
+		}
+
+		@DisplayName("should close messages only if no exception occurs during processing, with no exception for author2")
+		@Test
+		void shouldCloseMessagesOnlyIfNoExceptionOccursDuringProcessingWithNoExceptionForAuthor2() {
+			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3));
+			processMessageDummy = message -> throwRuntimeExceptionExceptForAuthorIdentifier(message, AUTHOR_CLIENT_IDENTIFIER2);
+
+			var transportReports = fetchMessages();
+
+			if (processedMessages.size() != 5) {
+				assertThat(supportCheckedMetadataItems).hasSize(1 + 3 + 2);
+				assertThatMessages(processedMessages).containExactlyInAnyOrder(
+						sendMessages.get(0),
+						sendMessages.get(1), sendMessages.get(2), sendMessages.get(3),
+						sendMessages.get(4), sendMessages.get(5)
+				);
+				assertThatTransportReports(transportReports)
+						.reportExactlyFor(processedMessages)
+						.haveExactlyGreenStatusFor(
+								messageIdBySendIndex(3),
+								messageIdBySendIndex(4)
+						);
+			} else {
+				// If (by chance) sendMessages.get(1), sendMessages.get(2) are fetched first, both are not closed due to the runtime exception.
+				// Which results in the warning: "No message has been closed although more are pending. Try increasing max list items."
+				// and sendMessages.get(3) not being fetched/checked or processed.
+
+				assertThat(supportCheckedMetadataItems).hasSize(1 + 3 - 1 + 2);
+				assertThatMessages(processedMessages).containExactlyInAnyOrder(
+						sendMessages.get(0),
+						sendMessages.get(1), sendMessages.get(2),
+						sendMessages.get(4), sendMessages.get(5)
+				);
+				assertThatTransportReports(transportReports)
+						.reportExactlyFor(processedMessages)
+						.haveExactlyGreenStatusFor(
+								messageIdBySendIndex(4)
+						);
+			}
+		}
+
+		private void throwRuntimeExceptionExceptForAuthorIdentifier(XtaMessage message, XtaIdentifier authorIdentifier) {
+			var authorId = message.metaData().authorIdentifier().value();
+			var readerId = message.metaData().readerIdentifier().value();
+			if (!authorId.equals(authorIdentifier.value())) {
+				throw new RuntimeException("Test exception for message with author '%s' and reader '%s'!".formatted(authorId, readerId));
+			}
+		}
+
+		@DisplayName("should process messages only if supported, with support for author1")
+		@Test
+		void shouldProcessMessagesOnlyIfSupportedWithSupportForAuthor1() {
+			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3));
+			isSupportedDummy = metaData -> metaData.authorIdentifier().value().equals(AUTHOR_CLIENT_IDENTIFIER.value());
+
+			var transportReports = fetchMessages();
+
+			assertThat(supportCheckedMetadataItems).hasSize(1 + 3 + 2);
+			assertThatMessages(processedMessages).containExactlyInAnyOrder(sendMessages.get(0), sendMessages.get(1), sendMessages.get(2));
+			assertThatTransportReports(transportReports)
+					.reportExactlyFor(processedMessages)
+					.haveExactlyGreenStatusFor(messageIdBySendIndex(0), messageIdBySendIndex(1), messageIdBySendIndex(2));
+		}
+
+		@DisplayName("should process messages only if supported, with support for author2")
+		@Test
+		void shouldProcessMessagesOnlyIfSupportedWithSupportForAuthor2() {
+			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3));
+			isSupportedDummy = metaData -> metaData.authorIdentifier().value().equals(AUTHOR_CLIENT_IDENTIFIER2.value());
+
+			var transportReports = fetchMessages();
+
+			assertThat(supportCheckedMetadataItems).hasSize(1 + 3 + 2);
+			assertThatMessages(processedMessages).containExactlyInAnyOrder(sendMessages.get(3), sendMessages.get(4));
+			assertThatTransportReports(transportReports)
+					.reportExactlyFor(processedMessages)
+					.haveExactlyGreenStatusFor(messageIdBySendIndex(3), messageIdBySendIndex(4));
+		}
+
+		@SneakyThrows
+		private void setupClientWithIdentifiers(List<XtaIdentifier> identifiers) {
+			testClient = XtaClient.from(
+					XTA_TEST_SERVER_SETUP_EXTENSION.createSpecificClientConfigBuilder()
+							.clientIdentifiers(identifiers)
+							.maxListItems(TWO_MAX_LIST_ITEMS)
+							.isMessageSupported(metaData -> {
+								supportCheckedMetadataItems.add(metaData);
+								return isSupportedDummy.test(metaData);
+							})
+							.build()
+			);
+		}
+
+		private String messageIdBySendIndex(int sendIndex) {
+			var expectedMessageMetadata = withoutMessageIdAndSize(sendMessages.get(sendIndex).metaData());
+			var messageIds = processedMessages.stream()
+					.map(XtaMessage::metaData)
+					.filter(metaData -> withoutMessageIdAndSize(metaData).equals(expectedMessageMetadata))
+					.map(XtaMessageMetaData::messageId)
+					.toList();
+			if (messageIds.size() != 1) {
+				throw new TechnicalException(
+						"Expected exactly one message id for send index %d, but found %d!".formatted(sendIndex, messageIds.size())
+								+ "Ensure that all test messages have unique metadata!");
+			}
+			return messageIds.getFirst();
+		}
+
+		private List<XtaTransportReport> fetchMessages() {
+			return testClient.fetchMessages((message) -> {
+				processedMessages.add(message);
+				processMessageDummy.accept(message);
+			});
+		}
 	}
 
 	@DisplayName("send message")
@@ -45,7 +329,7 @@ class XtaClientITCase {
 			var messageConfig = XtaMessageExampleLoader.MessageExampleConfig.builder()
 					.messageLabel("dfoerdermittel")
 					.reader(READER_CLIENT_IDENTIFIER1)
-					.author(READER_CLIENT_IDENTIFIER1)
+					.author(AUTHOR_CLIENT_IDENTIFIER)
 					.build();
 			message = XtaMessageExampleLoader.load(messageConfig);
 		}
@@ -54,11 +338,10 @@ class XtaClientITCase {
 		@Test
 		@SneakyThrows
 		void shouldReturnTransportReportWithOpenStatus() {
-			var transportReport = client.sendMessage(message);
+			var transportReport = setupClient.sendMessage(message);
 
 			assertThat(transportReport.status()).isEqualTo(XtaMessageStatus.OPEN);
 		}
-
 	}
 }
 
diff --git a/src/test/java/de/ozgcloud/xta/client/config/XtaConfigValidatorTest.java b/src/test/java/de/ozgcloud/xta/client/config/XtaConfigValidatorTest.java
index 88672c08a8a8895e2073bf7e479fcd7811083e80..0f6625caa806b1126f632dd4e9deec433aa312e6 100644
--- a/src/test/java/de/ozgcloud/xta/client/config/XtaConfigValidatorTest.java
+++ b/src/test/java/de/ozgcloud/xta/client/config/XtaConfigValidatorTest.java
@@ -1,8 +1,8 @@
 package de.ozgcloud.xta.client.config;
 
-import static java.util.Collections.*;
 import static org.assertj.core.api.Assertions.*;
 
+import java.util.List;
 import java.util.function.UnaryOperator;
 
 import org.junit.jupiter.api.DisplayName;
@@ -10,8 +10,9 @@ import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 
-import de.ozgcloud.xta.client.factory.XtaClientConfigTestFactory;
 import de.ozgcloud.xta.client.exception.ClientInitializationException;
+import de.ozgcloud.xta.client.factory.XtaClientConfigTestFactory;
+import de.ozgcloud.xta.client.model.XtaIdentifier;
 import lombok.SneakyThrows;
 
 class XtaConfigValidatorTest {
@@ -43,11 +44,11 @@ class XtaConfigValidatorTest {
 			validator.validate(config);
 		}
 
-		@DisplayName("should throw without client identifiers")
+		@DisplayName("should throw with blank client identifier")
 		@Test
-		void shouldThrowWithoutIdentifiers() {
+		void shouldThrowWithBlankClientIdentifier() {
 			var config = XtaClientConfigTestFactory.createBuilder()
-					.clientIdentifiers(emptyList())
+					.clientIdentifiers(List.of(XtaIdentifier.builder().value("").build()))
 					.build();
 
 			assertThatThrownBy(() -> validator.validate(config))
diff --git a/src/test/java/de/ozgcloud/xta/client/extension/XtaMessageExampleLoader.java b/src/test/java/de/ozgcloud/xta/client/extension/XtaMessageExampleLoader.java
index acd02145e99684623327c7b8df12de151447fd15..8fa45ece8e7128c5eae684261ad4fd84c89faaee 100644
--- a/src/test/java/de/ozgcloud/xta/client/extension/XtaMessageExampleLoader.java
+++ b/src/test/java/de/ozgcloud/xta/client/extension/XtaMessageExampleLoader.java
@@ -131,7 +131,7 @@ public class XtaMessageExampleLoader {
 
 	private static List<XtaFile> mapXtaFiles(List<Map<String, Object>> attachmentFiles, String resourcePrefix) {
 		return attachmentFiles.stream()
-				.map(messageFileMap -> mapXtaFile(messageFileMap, resourcePrefix, null))
+				.map(messageFileMap -> mapXtaFile(messageFileMap, resourcePrefix, (path, content) -> content))
 				.toList();
 	}
 
diff --git a/src/test/java/de/ozgcloud/xta/client/extension/XtaServerSetupExtensionTestUtil.java b/src/test/java/de/ozgcloud/xta/client/extension/XtaServerSetupExtensionTestUtil.java
index 4ebb6c6a2b5235c7310238ef6ad286689c5a9c14..467f5700e14fd5fe85ae60216d2cb2e4e5457f4a 100644
--- a/src/test/java/de/ozgcloud/xta/client/extension/XtaServerSetupExtensionTestUtil.java
+++ b/src/test/java/de/ozgcloud/xta/client/extension/XtaServerSetupExtensionTestUtil.java
@@ -1,15 +1,26 @@
 package de.ozgcloud.xta.client.extension;
 
+import static java.util.Collections.*;
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.Arrays;
 import java.util.List;
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 import de.ozgcloud.xta.client.XtaClient;
 import de.ozgcloud.xta.client.config.XtaClientConfig;
 import de.ozgcloud.xta.client.core.WrappedXtaService;
 import de.ozgcloud.xta.client.core.XtaClientService;
 import de.ozgcloud.xta.client.exception.ClientRuntimeException;
+import de.ozgcloud.xta.client.model.XtaFile;
 import de.ozgcloud.xta.client.model.XtaIdentifier;
 import de.ozgcloud.xta.client.model.XtaMessage;
 import de.ozgcloud.xta.client.model.XtaMessageMetaData;
+import de.ozgcloud.xta.client.model.XtaMessageStatus;
+import de.ozgcloud.xta.client.model.XtaTransportReport;
 import genv3.de.xoev.transport.xta.x211.CodeFehlernummer;
 import genv3.de.xoev.transport.xta.x211.MessageSchemaViolationException;
 import genv3.de.xoev.transport.xta.x211.ParameterIsNotValidException;
@@ -26,6 +37,17 @@ public class XtaServerSetupExtensionTestUtil {
 			.category("DMS Schleswig-Holstein")
 			.name("Generischer Antragsdienst")
 			.build();
+	public static final XtaIdentifier AUTHOR_CLIENT_IDENTIFIER2 = XtaIdentifier.builder()
+			.value("ehp:010100100000")
+			.category("Engagement- und Hobbyportal (FIM Sender)")
+			.name("OSI-Onlinedienst Schleswig-Holstein Versammlungsanzeige Test")
+			.build();
+	public static final XtaIdentifier AUTHOR_CLIENT_IDENTIFIER3 = XtaIdentifier.builder()
+			.value("ehp:010200100000")
+			.category("Engagement- und Hobbyportal (FIM Sender2)")
+			.name("OSI-Onlinedienst Hamburg Versammlungsanzeige Test")
+			.build();
+
 	public static final XtaIdentifier READER_CLIENT_IDENTIFIER2 = XtaIdentifier.builder()
 			.value("gae:test-environment@ozg-cloud.de")
 			.category("Generischer Antragsempfänger")
@@ -36,10 +58,15 @@ public class XtaServerSetupExtensionTestUtil {
 			.category("Generischer Antragsempfänger")
 			.name("OZG-Cloud Dev")
 			.build();
+	public static final XtaIdentifier READER_CLIENT_IDENTIFIER3 = XtaIdentifier.builder()
+			.value("vbe:010510440100")
+			.category("Versammlungsbehörde (FIM Empfänger)")
+			.name("Kreisordnungsbehörde Dithmarschen")
+			.build();
 
 	public static XtaClientConfig.XtaClientConfigBuilder createClientConfigBuilder() {
 		return XtaClientConfig.builder()
-				.clientIdentifiers(List.of(AUTHOR_CLIENT_IDENTIFIER, READER_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER1))
+				.clientIdentifiers(emptyList())
 				.logSoapRequests(true)
 				.logSoapResponses(true);
 	}
@@ -97,9 +124,160 @@ public class XtaServerSetupExtensionTestUtil {
 		return (T) field.get(object);
 	}
 
+	public static XtaMessageMetaData withoutMessageIdAndSize(XtaMessageMetaData metaData) {
+		return metaData.toBuilder()
+				.messageId(null)
+				.messageSize(null)
+				.build();
+	}
+
+	public record MessagesAssert(List<XtaMessage> processedMessages) {
+		public MessagesAssert containExactlyInAnyOrder(XtaMessage... messages) {
+			try {
+				// Assert equal message counts
+				assertThat(processedMessages).hasSize(messages.length);
+
+				// Assert equal metadata (ignoring message id and size since they should be null before sending)
+				assertThat(processedMessages)
+						.extracting(XtaMessage::metaData)
+						.extracting(XtaServerSetupExtensionTestUtil::withoutMessageIdAndSize)
+						.containsExactlyInAnyOrderElementsOf(Arrays.stream(messages)
+								.map(XtaMessage::metaData)
+								.map(XtaServerSetupExtensionTestUtil::withoutMessageIdAndSize)
+								.toList());
+
+				// Assert equal message file and attachment files without content (ignoring size since it may be null before sending)
+				UnaryOperator<XtaFile> withoutContentAndSize = xtaFile -> xtaFile.toBuilder()
+						.content(null)
+						.size(null)
+						.build();
+				assertThat(processedMessages)
+						.extracting(XtaMessage::messageFile)
+						.extracting(withoutContentAndSize)
+						.containsExactlyInAnyOrderElementsOf(Arrays.stream(messages)
+								.map(XtaMessage::messageFile)
+								.map(withoutContentAndSize)
+								.toList());
+				UnaryOperator<List<XtaFile>> filesWithoutContentAndSize = fileList -> fileList.stream().map(withoutContentAndSize).toList();
+				assertThat(processedMessages)
+						.extracting(XtaMessage::attachmentFiles)
+						.extracting(filesWithoutContentAndSize)
+						.containsExactlyInAnyOrderElementsOf(Arrays.stream(messages)
+								.map(XtaMessage::attachmentFiles)
+								.map(filesWithoutContentAndSize)
+								.toList());
+
+				// Assert equal content of message file
+				var listOfMessageFileContents = processedMessages.stream()
+						.map(XtaMessage::messageFile)
+						.map(XtaServerSetupExtensionTestUtil::readBytesOfXtaFile)
+						.toList();
+				var exceptedListOfMessageFileContents = Arrays.stream(messages)
+						.map(XtaMessage::messageFile)
+						.map(XtaServerSetupExtensionTestUtil::readBytesOfXtaFile)
+						.toList();
+				Function<byte[], Integer> contentLength = b -> b.length;
+				assertThat(listOfMessageFileContents)
+						.extracting(contentLength)
+						.containsExactlyInAnyOrderElementsOf(exceptedListOfMessageFileContents.stream().map(contentLength).toList());
+				assertThat(listOfMessageFileContents).containsExactlyInAnyOrderElementsOf(exceptedListOfMessageFileContents);
+
+				// Assert equal content of attachment files
+				var attachmentFileContents = processedMessages.stream()
+						.map(XtaMessage::attachmentFiles)
+						.map(XtaServerSetupExtensionTestUtil::readBytesOfXtaFiles)
+						.toList();
+				var exceptedAttachmentFileContents = Arrays.stream(messages)
+						.map(XtaMessage::attachmentFiles)
+						.map(XtaServerSetupExtensionTestUtil::readBytesOfXtaFiles)
+						.toList();
+				Function<List<byte[]>, List<Integer>> contentLengths = contentList -> contentList.stream().map(contentLength).toList();
+				assertThat(attachmentFileContents)
+						.extracting(contentLengths)
+						.containsExactlyInAnyOrderElementsOf(exceptedAttachmentFileContents.stream().map(contentLengths).toList());
+
+				assertThat(attachmentFileContents)
+						.usingElementComparator((a, b) -> {
+							var sizeComparison = Integer.compare(a.size(), b.size());
+							return sizeComparison != 0
+									? sizeComparison
+									: IntStream.range(0, a.size())
+											.map(i -> Arrays.compare(a.get(i), b.get(i)))
+											.filter(i -> i != 0)
+											.findFirst()
+											.orElse(0);
+						})
+						.containsExactlyInAnyOrderElementsOf(exceptedAttachmentFileContents);
+
+			} catch (AssertionError | RuntimeException e) {
+				log.error("Messages do not exactly contain excepted messages!");
+				throw e;
+			}
+			return this;
+		}
+	}
+
+	public static MessagesAssert assertThatMessages(List<XtaMessage> processedMessages) {
+		return new MessagesAssert(processedMessages);
+	}
+
+	public record TransportReportsAssert(List<XtaTransportReport> transportReports) {
+		public TransportReportsAssert reportExactlyFor(List<XtaMessage> processedMessages) {
+			try {
+				assertThat(transportReports).hasSize(processedMessages.size());
+				// Compare message ids
+				assertThat(transportReports)
+						.extracting(XtaTransportReport::metaData)
+						.extracting(XtaMessageMetaData::messageId)
+						.containsExactlyElementsOf(processedMessages.stream()
+								.map(XtaMessage::metaData)
+								.map(XtaMessageMetaData::messageId)
+								.toList());
+				// Compare message metadata
+				assertThat(transportReports)
+						.extracting(XtaTransportReport::metaData)
+						.containsExactlyElementsOf(processedMessages.stream()
+								.map(XtaMessage::metaData)
+								.toList());
+			} catch (AssertionError | RuntimeException e) {
+				log.error("TransportReports do not exactly match messages metadata!");
+				throw e;
+			}
+			return this;
+		}
+
+		public TransportReportsAssert haveExactlyGreenStatusFor(String... messageIds) {
+			try {
+				var setOfMessageIds = Arrays.stream(messageIds).collect(Collectors.toSet());
+
+				assertThat(transportReports)
+						.allMatch(transportReport ->
+								transportReport.status().equals(XtaMessageStatus.GREEN) == setOfMessageIds
+										.contains(transportReport.metaData().messageId())
+						);
+			} catch (AssertionError | RuntimeException e) {
+				log.error("TransportReports do not have excepted green status for messageIds!");
+				throw e;
+			}
+			return this;
+		}
+	}
+
+	public static TransportReportsAssert assertThatTransportReports(List<XtaTransportReport> transportReports) {
+		return new TransportReportsAssert(transportReports);
+	}
+
+	private static List<byte[]> readBytesOfXtaFiles(List<XtaFile> xtaFiles) {
+		return xtaFiles.stream()
+				.map(XtaServerSetupExtensionTestUtil::readBytesOfXtaFile)
+				.toList();
+	}
+
 	@SneakyThrows
-	public static byte[] extractMessageFileContent(XtaMessage xtaMessage) {
-		return xtaMessage.messageFile().content().getInputStream().readAllBytes();
+	private static byte[] readBytesOfXtaFile(XtaFile xtaFile) {
+		try (var inputStream = xtaFile.content().getInputStream()) {
+			return inputStream.readAllBytes();
+		}
 	}
 
 }
diff --git a/src/test/java/de/ozgcloud/xta/client/extension/XtaTestServerSetupExtension.java b/src/test/java/de/ozgcloud/xta/client/extension/XtaTestServerSetupExtension.java
index 2e58da0c6444b0eb7c7648a4ac55dbe2fe47f1de..f5e29c42d92cde5206a7fcafe9126a1766b2f40e 100644
--- a/src/test/java/de/ozgcloud/xta/client/extension/XtaTestServerSetupExtension.java
+++ b/src/test/java/de/ozgcloud/xta/client/extension/XtaTestServerSetupExtension.java
@@ -6,7 +6,6 @@ import java.util.Objects;
 
 import org.junit.jupiter.api.extension.AfterAllCallback;
 import org.junit.jupiter.api.extension.BeforeAllCallback;
-import org.junit.jupiter.api.extension.BeforeEachCallback;
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.testcontainers.utility.DockerImageName;
 
@@ -22,7 +21,7 @@ import lombok.extern.slf4j.Slf4j;
 @Getter
 @Setter
 @Slf4j
-public class XtaTestServerSetupExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback {
+public class XtaTestServerSetupExtension implements BeforeAllCallback, AfterAllCallback {
 
 	private static final DockerImageName XTA_TEST_SERVER_IMAGE = DockerImageName.parse("docker.ozg-sh.de/xta-test-server")
 			.withTag("1.6.0");
@@ -33,10 +32,11 @@ public class XtaTestServerSetupExtension implements BeforeAllCallback, AfterAllC
 	private static final String XTA_TEST_SERVER_TRUSTSTORE_PATH = "store/xta-test-server-truststore.jks";
 	private static final String XTA_TEST_SERVER_TRUSTSTORE_PASSWORD = "password";
 
-	private XtaClient client;
-	private XtaClientConfig config;
+	private XtaClient setupClient;
 	private XtaClientFactory clientFactory;
 	private XtaTestServerContainer xtaServerContainer;
+	private XtaClientConfig.KeyStore clientCertKeyStore;
+	private XtaClientConfig.KeyStore trustStore;
 
 	@Override
 	@SneakyThrows
@@ -44,9 +44,26 @@ public class XtaTestServerSetupExtension implements BeforeAllCallback, AfterAllC
 		if (xtaServerContainer != null) {
 			return;
 		}
-
 		setupServer();
-		client = setupClient();
+		setupClient();
+	}
+
+	@SneakyThrows
+	private void setupClient() {
+		clientCertKeyStore = XtaClientConfig.KeyStore.builder()
+				.content(readBytesFromResource(JOHN_SMITH_KEYSTORE_PATH))
+				.type("PKCS12")
+				.password(JOHN_SMITH_KEYSTORE_PASSWORD.toCharArray())
+				.build();
+		trustStore = XtaClientConfig.KeyStore.builder()
+				.content(readBytesFromResource(XTA_TEST_SERVER_TRUSTSTORE_PATH))
+				.type("JKS")
+				.password(XTA_TEST_SERVER_TRUSTSTORE_PASSWORD.toCharArray())
+				.build();
+		setupClient = XtaClient.from(createSpecificClientConfigBuilder()
+				.logSoapRequests(false)
+				.logSoapResponses(false)
+				.build());
 	}
 
 	private void setupServer() {
@@ -66,43 +83,13 @@ public class XtaTestServerSetupExtension implements BeforeAllCallback, AfterAllC
 	}
 
 	@SneakyThrows
-	XtaClient setupClient() {
-
-		var clientCertKeyStore = XtaClientConfig.KeyStore.builder()
-				.content(readBytesFromResource(JOHN_SMITH_KEYSTORE_PATH))
-				.type("PKCS12")
-				.password(JOHN_SMITH_KEYSTORE_PASSWORD.toCharArray())
-				.build();
-		var trustStore = XtaClientConfig.KeyStore.builder()
-				.content(readBytesFromResource(XTA_TEST_SERVER_TRUSTSTORE_PATH))
-				.type("JKS")
-				.password(XTA_TEST_SERVER_TRUSTSTORE_PASSWORD.toCharArray())
-				.build();
-
-		config = createClientConfigBuilder()
+	public XtaClientConfig.XtaClientConfigBuilder createSpecificClientConfigBuilder() {
+		return createClientConfigBuilder()
 				.managementServiceUrl(xtaServerContainer.getManagementPortUrl())
 				.sendServiceUrl(xtaServerContainer.getSendPortUrl())
 				.msgBoxServiceUrl(xtaServerContainer.getMsgBoxPortUrl())
 				.clientCertKeystore(clientCertKeyStore)
-				.trustStore(trustStore)
-				.build();
-		clientFactory = XtaClientFactory.from(config);
-		return clientFactory.create();
-	}
-
-	@Override
-	@SneakyThrows
-	public void beforeEach(ExtensionContext context) {
-		closeAllMessages(client, READER_CLIENT_IDENTIFIER1);
-	}
-
-	@SneakyThrows
-	public String sendTestMessage() {
-		return XtaServerSetupExtensionTestUtil.sendTestMessage(client, XtaMessageExampleLoader.MessageExampleConfig.builder()
-				.messageLabel("dfoerdermittel")
-				.reader(READER_CLIENT_IDENTIFIER1)
-				.author(READER_CLIENT_IDENTIFIER1)
-				.build());
+				.trustStore(trustStore);
 	}
 
 	@SneakyThrows