diff --git a/pom.xml b/pom.xml
index 89aeabe7bc1dbd241930a1c1612acdd3f6ca9d7c..a10e6b554ecaedaa58f9d6056c279432c30eee7a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -97,6 +97,11 @@
 			<scope>provided</scope>
 		</dependency>
 
+		<dependency>
+			<groupId>org.apache.logging.log4j</groupId>
+			<artifactId>log4j-core</artifactId>
+		</dependency>
+
 		<!-- Test -->
 		<dependency>
 			<groupId>org.apache.commons</groupId>
@@ -137,7 +142,6 @@
 			<artifactId>snakeyaml</artifactId>
 			<scope>test</scope>
 		</dependency>
-
 	</dependencies>
 
 	<build>
@@ -145,6 +149,29 @@
 			<plugin>
 				<groupId>org.apache.maven.plugins</groupId>
 				<artifactId>maven-compiler-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>log4j2-plugin-processor</id>
+						<goals>
+							<goal>compile</goal>
+							<goal>testCompile</goal>
+						</goals>
+						<phase>process-classes</phase>
+						<configuration>
+							<proc>only</proc>
+							<annotationProcessorPaths>
+								<path>
+									<groupId>org.apache.logging.log4j</groupId>
+									<artifactId>log4j-core</artifactId>
+									<version>${log4j2.version}</version>
+								</path>
+							</annotationProcessorPaths>
+							<annotationProcessors>
+								<processor>org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor</processor>
+							</annotationProcessors>
+						</configuration>
+					</execution>
+				</executions>
 			</plugin>
 			<plugin>
 				<groupId>org.apache.maven.plugins</groupId>
diff --git a/src/main/java/de/ozgcloud/xta/client/FetchMessageParameter.java b/src/main/java/de/ozgcloud/xta/client/FetchMessageParameter.java
new file mode 100644
index 0000000000000000000000000000000000000000..b2ae07f21cede17f6b93ffc23531ec347381340b
--- /dev/null
+++ b/src/main/java/de/ozgcloud/xta/client/FetchMessageParameter.java
@@ -0,0 +1,36 @@
+package de.ozgcloud.xta.client;
+
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import de.ozgcloud.xta.client.model.XtaIdentifier;
+import de.ozgcloud.xta.client.model.XtaMessage;
+import de.ozgcloud.xta.client.model.XtaMessageMetaData;
+
+record FetchMessageParameter(
+		XtaIdentifier clientIdentifier,
+		Consumer<XtaMessage> processMessage,
+		Set<String> viewedMessageIds
+) {
+
+	public FetchMessageParameter withViewedMessageIdsFrom(List<XtaMessageMetaData> messageMetaData) {
+		return new FetchMessageParameter(this.clientIdentifier, this.processMessage,
+				collectSetOfViewedMessageIds(messageMetaData, this.viewedMessageIds));
+	}
+
+	private Set<String> collectSetOfViewedMessageIds(List<XtaMessageMetaData> messageMetaData, Set<String> processedMessageIds) {
+		return Stream.concat(
+				processedMessageIds.stream(),
+				messageMetaData.stream()
+						.map(XtaMessageMetaData::messageId)
+		).collect(Collectors.toSet());
+	}
+
+	public boolean hasMessageAlreadyBeenViewed(XtaMessageMetaData messageMetaData) {
+		return viewedMessageIds.contains(messageMetaData.messageId());
+	}
+
+}
diff --git a/src/main/java/de/ozgcloud/xta/client/XtaClient.java b/src/main/java/de/ozgcloud/xta/client/XtaClient.java
index a22c002cbb4d5bea9d0881ac7e4378a028523ed9..8d5d8b47a2af6bc8968e555b855185015b06d1e7 100644
--- a/src/main/java/de/ozgcloud/xta/client/XtaClient.java
+++ b/src/main/java/de/ozgcloud/xta/client/XtaClient.java
@@ -1,113 +1,171 @@
 package de.ozgcloud.xta.client;
 
+import static java.util.Collections.*;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
 import jakarta.validation.Valid;
-import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
 
 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.core.XtaExceptionHandler;
+import de.ozgcloud.xta.client.exception.XtaClientRuntimeException;
+import de.ozgcloud.xta.client.exception.XtaClientException;
+import de.ozgcloud.xta.client.exception.XtaClientInitializationException;
 import de.ozgcloud.xta.client.model.XtaIdentifier;
 import de.ozgcloud.xta.client.model.XtaMessage;
-import de.ozgcloud.xta.client.model.XtaMessageAndTransportReport;
+import de.ozgcloud.xta.client.model.XtaMessageMetaData;
 import de.ozgcloud.xta.client.model.XtaMessageMetaDataListing;
+import de.ozgcloud.xta.client.model.XtaMessageStatus;
 import de.ozgcloud.xta.client.model.XtaTransportReport;
-import genv3.de.xoev.transport.xta.x211.InvalidMessageIDException;
-import genv3.de.xoev.transport.xta.x211.MessageSchemaViolationException;
-import genv3.de.xoev.transport.xta.x211.MessageVirusDetectionException;
-import genv3.de.xoev.transport.xta.x211.ParameterIsNotValidException;
-import genv3.de.xoev.transport.xta.x211.PermissionDeniedException;
-import genv3.de.xoev.transport.xta.x211.SyncAsyncException;
-import genv3.de.xoev.transport.xta.x211.XTAWSTechnicalProblemException;
 import lombok.AccessLevel;
 import lombok.Builder;
-import lombok.Getter;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
 
 @RequiredArgsConstructor(access = AccessLevel.MODULE)
 @Builder(access = AccessLevel.MODULE)
-@Getter(AccessLevel.MODULE)
+@Log4j2
 public class XtaClient {
 
-	private final WrappedXtaService service;
+	private final XtaClientService service;
 	private final XtaClientConfig config;
+	private final XtaExceptionHandler exceptionHandler;
 
-	/**
-	 * Fetch metadata of pending messages sent to the {@code xtaIdentifier}. The returned listing contains at most
-	 * {@link de.ozgcloud.xta.client.config.XtaClientConfig#getMaxListItems() maxListItems} messages. To fetch the next messages, use
-	 * {@link #getNextMessagesMetadata(String)}. Note that {@code xtaIdentifier} has to be configured as a
-	 * {@link de.ozgcloud.xta.client.config.XtaClientConfig#getClientIdentifiers() clientIdentifiers}.
-	 *
-	 * @param clientIdentifier the client identifier value to fetch messages for
-	 * @return the listing result with metadata of messages
-	 */
-	public XtaMessageMetaDataListing getMessagesMetadata(@NotBlank String clientIdentifier)
-			throws XTAWSTechnicalProblemException, PermissionDeniedException {
-		var identifier = deriveIdentifier(clientIdentifier);
-		service.checkAccountActive(identifier);
-		return getStatusList(identifier);
-	}
-
-	/**
-	 * Fetch metadata of pending messages sent to the {@code xtaIdentifier}. This method skips checks but otherwise behaves exactly as
-	 * {@link #getMessagesMetadata(String)}.
-	 */
-	public XtaMessageMetaDataListing getNextMessagesMetadata(@NotBlank String clientIdentifier)
-			throws XTAWSTechnicalProblemException, PermissionDeniedException {
-		return getStatusList(deriveIdentifier(clientIdentifier));
-	}
-
-	private XtaMessageMetaDataListing getStatusList(XtaIdentifier clientIdentifier) throws XTAWSTechnicalProblemException, PermissionDeniedException {
-		return service.getStatusList(clientIdentifier, config.getMaxListItems());
-	}
-
-	/**
-	 * Fetch the message content, close the message, and then fetch the transport report for the given {@code messageId} and reader identifier
-	 * {@code clientIdentifier}.
-	 *
-	 * @param clientIdentifier Identifier of the reading client
-	 * @param messageId        Identifier of the message to fetch
-	 * @return The message and transport report
-	 */
-	public XtaMessageAndTransportReport getMessage(@NotBlank String clientIdentifier, @NotBlank String messageId)
-			throws XTAWSTechnicalProblemException, PermissionDeniedException, InvalidMessageIDException {
-		var identifier = deriveIdentifier(clientIdentifier);
-
-		var message = service.getMessage(messageId, identifier);
-		service.close(messageId, identifier);
-
-		var transportReport = service.getTransportReport(messageId, identifier);
-		return XtaMessageAndTransportReport.builder()
-				.message(message)
-				.transportReport(transportReport)
-				.build();
-	}
-
-	public XtaTransportReport sendMessage(@Valid XtaMessage messageWithoutMessageId)
-			throws XTAWSTechnicalProblemException, PermissionDeniedException, InvalidMessageIDException, SyncAsyncException,
-			MessageVirusDetectionException, MessageSchemaViolationException, ParameterIsNotValidException {
+	static final String NO_MESSAGE_CLOSED_WARNING = "No message has been closed although more are pending. Try increasing max list items.";
 
-		var metaData = messageWithoutMessageId.metaData();
-		var authorIdentifier = metaData.authorIdentifier();
-		service.checkAccountActive(authorIdentifier);
-		service.lookupService(metaData.service(), metaData.readerIdentifier(), authorIdentifier);
+	public static XtaClient from(XtaClientConfig config) throws XtaClientInitializationException {
+		return XtaClientFactory.from(config).create();
+	}
+
+	public List<XtaTransportReport> fetchMessages(@NotNull Consumer<XtaMessage> processMessage) throws XtaClientException {
+		try {
+			return fetchMessagesRaw(processMessage);
+		} catch (RuntimeException exception) {
+			throw exceptionHandler.deriveXtaClientException(exception);
+		}
+	}
 
-		var messageId = service.createMessageId(authorIdentifier);
-		service.sendMessage(createXtaMessageWithMessageId(messageWithoutMessageId, messageId));
+	List<XtaTransportReport> fetchMessagesRaw(Consumer<XtaMessage> processMessage) {
+		return config.getClientIdentifiers().stream()
+				.filter(service::checkAccountActive)
+				.map(clientIdentifier -> initializeFetchMessageParameter(clientIdentifier, processMessage))
+				.flatMap(this::fetchMessagesForClientIdentifier)
+				.toList();
+	}
 
-		return service.getTransportReport(messageId, authorIdentifier);
+	FetchMessageParameter initializeFetchMessageParameter(XtaIdentifier clientIdentifier, Consumer<XtaMessage> processMessage) {
+		return new FetchMessageParameter(clientIdentifier, processMessage, emptySet());
 	}
 
-	XtaMessage createXtaMessageWithMessageId(XtaMessage messageWithoutMessageId, String messageId) {
-		return messageWithoutMessageId.toBuilder()
-				.metaData(messageWithoutMessageId.metaData().toBuilder()
-						.messageId(messageId)
-						.build())
-				.build();
+	Stream<XtaTransportReport> fetchMessagesForClientIdentifier(FetchMessageParameter parameter) {
+		return service.getStatusList(parameter.clientIdentifier())
+				.map(listing -> {
+					var transportReports = fetchMessagesForListing(listing.messages(), parameter);
+					return Stream.concat(transportReports.stream(),
+							checkMoreMessagesAvailable(listing, transportReports)
+									? fetchMessagesForClientIdentifier(parameter.withViewedMessageIdsFrom(listing.messages()))
+									: Stream.empty());
+				})
+				.orElse(Stream.empty());
 	}
 
-	XtaIdentifier deriveIdentifier(String xtaIdentifier) {
-		return config.getClientIdentifiers().stream()
-				.filter(id -> id.value().equals(xtaIdentifier))
-				.findFirst()
-				.orElseThrow(() -> new IllegalArgumentException("Unknown identifier: " + xtaIdentifier));
+	boolean checkMoreMessagesAvailable(XtaMessageMetaDataListing listing, List<XtaTransportReport> transportReports) {
+		return checkExtraPendingMessagesAvailable(listing) && checkSomeMessageHasBeenClosed(transportReports);
+	}
+
+	boolean checkExtraPendingMessagesAvailable(XtaMessageMetaDataListing listing) {
+		return listing.messages().size() < listing.pendingMessageCount().intValue();
+	}
+
+	boolean checkSomeMessageHasBeenClosed(List<XtaTransportReport> transportReports) {
+		var someTransportReportHasClosedStatus = transportReports.stream()
+				.anyMatch(t -> !t.status().equals(XtaMessageStatus.OPEN));
+		if (!someTransportReportHasClosedStatus) {
+			logWarnForNoMessageClosed();
+		}
+		return someTransportReportHasClosedStatus;
+	}
+
+	void logWarnForNoMessageClosed() {
+		log.warn(NO_MESSAGE_CLOSED_WARNING);
 	}
+
+	List<XtaTransportReport> fetchMessagesForListing(
+			List<XtaMessageMetaData> messageMetaDataItems,
+			FetchMessageParameter parameter
+	) {
+		return messageMetaDataItems.stream()
+				.filter(metaData -> checkMessageShouldBeFetched(metaData, parameter))
+				.map(metaData -> fetchMessage(metaData, parameter))
+				.flatMap(Optional::stream)
+				.toList();
+	}
+
+	boolean checkMessageShouldBeFetched(XtaMessageMetaData messageMetaData, FetchMessageParameter parameter) {
+		return !parameter.hasMessageAlreadyBeenViewed(messageMetaData) && isMessageSupported(messageMetaData);
+	}
+
+	boolean isMessageSupported(XtaMessageMetaData messageMetaData) {
+		return Optional.ofNullable(config.getIsMessageSupported())
+				.map(predicate -> predicate.test(messageMetaData))
+				.orElse(true);
+	}
+
+	Optional<XtaTransportReport> fetchMessage(XtaMessageMetaData messageMetaData, FetchMessageParameter parameter) {
+		return service.getMessage(messageMetaData)
+				.flatMap(message -> processMesssageAndFetchTransportReport(message, parameter));
+	}
+
+	Optional<XtaTransportReport> processMesssageAndFetchTransportReport(XtaMessage message, FetchMessageParameter parameter) {
+		processMessageAndCloseIfNoException(message, parameter.processMessage());
+		return service.getTransportReport(message.metaData());
+	}
+
+	void processMessageAndCloseIfNoException(XtaMessage message, Consumer<XtaMessage> processMessage) {
+		var messageId = message.metaData().messageId();
+		try {
+			processMessage.accept(message);
+			log.debug("Processing of message '{}' succeeded! Closing message.", messageId);
+			service.closeMessage(message);
+		} catch (RuntimeException exception) {
+			logErrorForMessageProcessingFailure(messageId, exception);
+		}
+	}
+
+	void logErrorForMessageProcessingFailure(String messageId, RuntimeException exception) {
+		log.error("Processing of message '%s' failed! Not closing message.".formatted(messageId), exception);
+	}
+
+	public XtaTransportReport sendMessage(@Valid XtaMessage message) throws XtaClientException {
+		try {
+			return sendMessageRaw(message);
+		} catch (RuntimeException exception) {
+			throw exceptionHandler.deriveXtaClientException(exception);
+		}
+	}
+
+	XtaTransportReport sendMessageRaw(@Valid XtaMessage messageWithoutMessageId) {
+		var metaData = messageWithoutMessageId.metaData();
+		throwExceptionIfAccountInactive(metaData.authorIdentifier());
+		throwExceptionIfServiceNotAvailable(metaData);
+		return service.sendMessage(messageWithoutMessageId);
+	}
+
+	void throwExceptionIfServiceNotAvailable(XtaMessageMetaData metaData) {
+		if (!service.lookupService(metaData)) {
+			throw new XtaClientRuntimeException("Service %s is not available!".formatted(metaData.service()));
+		}
+	}
+
+	void throwExceptionIfAccountInactive(XtaIdentifier clientIdentifier) {
+		if (!service.checkAccountActive(clientIdentifier)) {
+			throw new XtaClientRuntimeException("Account %s is not active!".formatted(clientIdentifier.value()));
+		}
+	}
+
 }
diff --git a/src/main/java/de/ozgcloud/xta/client/XtaClientFactory.java b/src/main/java/de/ozgcloud/xta/client/XtaClientFactory.java
index 8d9c3ec43f972419a8b33ee3dd2335d0fb51ba01..e5429aec7a7c5ee0762362333985eab6b22a74d4 100644
--- a/src/main/java/de/ozgcloud/xta/client/XtaClientFactory.java
+++ b/src/main/java/de/ozgcloud/xta/client/XtaClientFactory.java
@@ -2,8 +2,9 @@ package de.ozgcloud.xta.client;
 
 import de.ozgcloud.xta.client.config.XtaClientConfig;
 import de.ozgcloud.xta.client.config.XtaConfigValidator;
-import de.ozgcloud.xta.client.core.WrappedXtaServiceFactory;
-import de.ozgcloud.xta.client.exception.ClientInitializationException;
+import de.ozgcloud.xta.client.core.XtaClientServiceFactory;
+import de.ozgcloud.xta.client.core.XtaExceptionHandlerFactory;
+import de.ozgcloud.xta.client.exception.XtaClientInitializationException;
 import lombok.Builder;
 import lombok.RequiredArgsConstructor;
 
@@ -12,22 +13,25 @@ import lombok.RequiredArgsConstructor;
 public class XtaClientFactory {
 
 	private final XtaConfigValidator configValidator;
-	private final WrappedXtaServiceFactory wrappedXtaServiceFactory;
+	private final XtaClientServiceFactory xtaClientServiceFactory;
 	private final XtaClientConfig config;
+	private final XtaExceptionHandlerFactory exceptionHandlerFactory;
 
 	public static XtaClientFactory from(final XtaClientConfig config) {
 		return XtaClientFactory.builder()
 				.config(config)
 				.configValidator(XtaConfigValidator.builder().build())
-				.wrappedXtaServiceFactory(WrappedXtaServiceFactory.from(config))
+				.xtaClientServiceFactory(XtaClientServiceFactory.from(config))
+				.exceptionHandlerFactory(XtaExceptionHandlerFactory.builder().build())
 				.build();
 	}
 
-	public XtaClient create() throws ClientInitializationException {
+	public XtaClient create() throws XtaClientInitializationException {
 		configValidator.validate(config);
 		return XtaClient.builder()
 				.config(config)
-				.service(wrappedXtaServiceFactory.create())
+				.service(xtaClientServiceFactory.create())
+				.exceptionHandler(exceptionHandlerFactory.create())
 				.build();
 	}
 }
diff --git a/src/main/java/de/ozgcloud/xta/client/codes/BusinessScenario.java b/src/main/java/de/ozgcloud/xta/client/codes/BusinessScenario.java
deleted file mode 100644
index 46bd4247bceaab4aa30f1b1a394e078d729cf110..0000000000000000000000000000000000000000
--- a/src/main/java/de/ozgcloud/xta/client/codes/BusinessScenario.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- *     XTA-Client Java Library
- *     Copyright (C) 2021 Koordinierungsstelle für IT-Standards (KoSIT)
- *
- *     This program is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     This program is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package de.ozgcloud.xta.client.codes;
-
-import lombok.Getter;
-
-/**
- * Einige vordefinierte Werte für "business.scenario" (Fachkontext/Verfahren). Werte die nicht auftauchen können über den
- * Stringsetter gesetzt werden.
- *
- * @see <a href="https://www.xrepository.de/details/urn:de:xta:codeliste:business.scenario">business.scenario</a>
- */
-@Getter
-public enum BusinessScenario {
-
-  /**
-   * Fachkontext XBAULEITPLANUNG.
-   */
-  XBAULEITPLANUNG_DATA("XBAULEITPLANUNG_DATA"),
-
-  /**
-   * Fachkontext XDOMEAREG.
-   */
-  XDOMEAREG_DATA("XDOMEAREG_DATA"),
-
-  /**
-   * Fachkontext XInneres.
-   */
-  XINNERES_DATA("XINNERES_DATA"),
-
-  /**
-   * Fachkontext XAusländer.
-   */
-  XAUSLAENDER_DATA("XAUSLAENDER_DATA"),
-
-  /**
-   * Fachkontext OSCI-XMeld.
-   */
-  XMELD_DATA("XMELD_DATA"),
-
-  /**
-   * Fachkontext XPersonenstand.
-   */
-  XPERSONENSTAND_DATA("XPERSONENSTAND_DATA"),
-
-  /**
-   * Fachkontext XhD.
-   */
-  XHD_DATA("XHD_DATA"),
-
-  /**
-   * Fachkontext XGewerbeanzeige.
-   */
-  GEWERBE_DATA("GEWERBE_DATA");
-
-  private String code;
-
-  BusinessScenario(String code) {
-    this.code = code;
-  }
-}
diff --git a/src/main/java/de/ozgcloud/xta/client/codes/IdentifierType.java b/src/main/java/de/ozgcloud/xta/client/codes/IdentifierType.java
deleted file mode 100644
index 7b80214807430ef96ed0af1f915e38a247c8e716..0000000000000000000000000000000000000000
--- a/src/main/java/de/ozgcloud/xta/client/codes/IdentifierType.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- *     XTA-Client Java Library
- *     Copyright (C) 2021 Koordinierungsstelle für IT-Standards (KoSIT)
- *
- *     This program is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     This program is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package de.ozgcloud.xta.client.codes;
-
-import lombok.Getter;
-
-/**
- * Einige vordefinierte Werte für "type.of.party.identifier". Werte die nicht auftauchen können über den Stringsetter gestzt werden * {@link
- * Identifier (final String, String)}
- *
- * @see <a href"https://www.xrepository.de/details/urn:de:xta:codeliste:type.of.party.identifier">type.of.party.identifier</a>
- */
-@Getter
-public enum IdentifierType {
-
-  /**
-   * XÖV-DVDV-Infrastruktur: Identifizierungsschema gemäß DVDV-Behördenschlüssel	Nach den Regeln dieses Identifizierungsschemas
-   * ist z.B. die Kennung "psw:01002110" aufgebaut.
-   */
-  XOEV("xoev"),
-
-  /**
-   * SAFE-Infrastruktur des Justizressorts: Identifizierungsschema gemäß SAFE Nach den Regeln dieses Identifizierungsschemas ist
-   * z.B. die Kennung "safe-1363359638330-001060793" aufgebaut.
-   */
-  JUSTIZ("justiz");
-
-  private String code;
-
-  IdentifierType(String code) {
-    this.code = code;
-  }
-}
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 184ce5b94144de8d8104d8da8fc46ffaf151cd69..14bdf0499aa8265eb430f31a09c266200a26e64e 100644
--- a/src/main/java/de/ozgcloud/xta/client/config/XtaClientConfig.java
+++ b/src/main/java/de/ozgcloud/xta/client/config/XtaClientConfig.java
@@ -1,25 +1,17 @@
-/**
- * XTA-Client Java Library Copyright (C) 2021 Koordinierungsstelle für IT-Standards (KoSIT)
- *
- * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free
- * Software Foundation, either version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
 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;
 
 import de.ozgcloud.xta.client.model.XtaIdentifier;
+import de.ozgcloud.xta.client.model.XtaMessageMetaData;
 import lombok.Builder;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
@@ -31,8 +23,11 @@ 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;
 
 	@NotBlank
 	private final String managementServiceUrl;
diff --git a/src/main/java/de/ozgcloud/xta/client/config/XtaConfigValidator.java b/src/main/java/de/ozgcloud/xta/client/config/XtaConfigValidator.java
index 95821648a72da25c33e0eaed5598b30df0239606..f0fd961da8aadf56d9cb7b64c243e03e2208c8e6 100644
--- a/src/main/java/de/ozgcloud/xta/client/config/XtaConfigValidator.java
+++ b/src/main/java/de/ozgcloud/xta/client/config/XtaConfigValidator.java
@@ -7,7 +7,7 @@ import java.util.stream.Collectors;
 import jakarta.validation.Validation;
 import jakarta.validation.Validator;
 
-import de.ozgcloud.xta.client.exception.ClientInitializationException;
+import de.ozgcloud.xta.client.exception.XtaClientInitializationException;
 import lombok.Builder;
 import lombok.RequiredArgsConstructor;
 
@@ -22,10 +22,10 @@ public class XtaConfigValidator {
 		}
 	}
 
-	public void validate(final XtaClientConfig config) throws ClientInitializationException {
+	public void validate(final XtaClientConfig config) throws XtaClientInitializationException {
 		var violations = VALIDATOR.validate(config);
 		if (!violations.isEmpty()) {
-			throw new ClientInitializationException("Client configuration is invalid:\n" + violations.stream()
+			throw new XtaClientInitializationException("Client configuration is invalid:\n" + violations.stream()
 					.map(v -> "'%s' %s".formatted(v.getPropertyPath().toString(), v.getMessage()))
 					.collect(Collectors.joining("\n")));
 		}
diff --git a/src/main/java/de/ozgcloud/xta/client/core/WrappedXtaServiceFactory.java b/src/main/java/de/ozgcloud/xta/client/core/WrappedXtaServiceFactory.java
index 6a909b5709cce39cb5e369519fb28da62c56ee4a..9d89e96b5885d64c69c69687fb4bfd250c04dd5b 100644
--- a/src/main/java/de/ozgcloud/xta/client/core/WrappedXtaServiceFactory.java
+++ b/src/main/java/de/ozgcloud/xta/client/core/WrappedXtaServiceFactory.java
@@ -3,7 +3,7 @@ package de.ozgcloud.xta.client.core;
 import org.mapstruct.factory.Mappers;
 
 import de.ozgcloud.xta.client.config.XtaClientConfig;
-import de.ozgcloud.xta.client.exception.ClientInitializationException;
+import de.ozgcloud.xta.client.exception.XtaClientInitializationException;
 import de.ozgcloud.xta.client.mapper.RequestMapper;
 import de.ozgcloud.xta.client.mapper.ResponseMapper;
 import lombok.Builder;
@@ -25,7 +25,7 @@ public class WrappedXtaServiceFactory {
 				.build();
 	}
 
-	public WrappedXtaService create() throws ClientInitializationException {
+	public WrappedXtaService create() throws XtaClientInitializationException {
 		return WrappedXtaService.builder()
 				.ports(xtaPortTripleFactory.create())
 				.requestMapper(requestMapper)
diff --git a/src/main/java/de/ozgcloud/xta/client/core/XtaClientService.java b/src/main/java/de/ozgcloud/xta/client/core/XtaClientService.java
new file mode 100644
index 0000000000000000000000000000000000000000..d61028941a5ed2ec58f05e29deb9f0f0050ce9e6
--- /dev/null
+++ b/src/main/java/de/ozgcloud/xta/client/core/XtaClientService.java
@@ -0,0 +1,144 @@
+package de.ozgcloud.xta.client.core;
+
+import java.util.Optional;
+
+import de.ozgcloud.xta.client.config.XtaClientConfig;
+import de.ozgcloud.xta.client.exception.XtaClientRuntimeException;
+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.XtaMessageMetaDataListing;
+import de.ozgcloud.xta.client.model.XtaTransportReport;
+import genv3.de.xoev.transport.xta.x211.InvalidMessageIDException;
+import genv3.de.xoev.transport.xta.x211.MessageSchemaViolationException;
+import genv3.de.xoev.transport.xta.x211.MessageVirusDetectionException;
+import genv3.de.xoev.transport.xta.x211.ParameterIsNotValidException;
+import genv3.de.xoev.transport.xta.x211.PermissionDeniedException;
+import genv3.de.xoev.transport.xta.x211.SyncAsyncException;
+import genv3.de.xoev.transport.xta.x211.XTAWSTechnicalProblemException;
+import lombok.Builder;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Builder
+@RequiredArgsConstructor
+@Slf4j
+public class XtaClientService {
+
+	private final WrappedXtaService service;
+	private final XtaClientConfig config;
+
+	static final String TRANSPORT_REPORT_FAILED_ERROR = "Failed to get transport report!";
+
+	public Optional<XtaTransportReport> getTransportReport(XtaMessageMetaData messageMetaData) {
+		try {
+			return Optional.of(getTransportReportOrThrowException(messageMetaData));
+		} catch (XtaClientRuntimeException e) {
+			logError(TRANSPORT_REPORT_FAILED_ERROR, e);
+			return Optional.empty();
+		}
+	}
+
+	public void closeMessage(XtaMessage message) {
+		var messageId = message.metaData().messageId();
+		var readerClientIdentifier = message.metaData().readerIdentifier();
+		try {
+			service.close(messageId, readerClientIdentifier);
+		} catch (XTAWSTechnicalProblemException | PermissionDeniedException | InvalidMessageIDException | RuntimeException e) {
+			logError("Failed to close message! '%s' (reader: %s)".formatted(messageId, readerClientIdentifier), e);
+		}
+	}
+
+	public Optional<XtaMessage> getMessage(XtaMessageMetaData messageMetaData) {
+		var messageId = messageMetaData.messageId();
+		var readerClientIdentifier = messageMetaData.readerIdentifier();
+		try {
+			return Optional.of(service.getMessage(messageId, readerClientIdentifier));
+		} catch (XTAWSTechnicalProblemException | PermissionDeniedException | InvalidMessageIDException | RuntimeException e) {
+			logError("Failed to get message by id ! '%s' (reader: %s)".formatted(messageId, readerClientIdentifier.value()), e);
+			return Optional.empty();
+		}
+	}
+
+	public Optional<XtaMessageMetaDataListing> getStatusList(XtaIdentifier clientIdentifier) {
+		try {
+			return Optional.of(service.getStatusList(clientIdentifier, config.getMaxListItems()));
+		} catch (PermissionDeniedException | XTAWSTechnicalProblemException | RuntimeException e) {
+			logError("Failed to get status list!", e);
+			return Optional.empty();
+		}
+	}
+
+	void logError(String message, Exception e) {
+		log.error(message, e);
+	}
+
+	public boolean checkAccountActive(XtaIdentifier clientIdentifier) {
+		try {
+			service.checkAccountActive(clientIdentifier);
+			return true;
+		} catch (XTAWSTechnicalProblemException | RuntimeException e) {
+			throw new XtaClientRuntimeException("Failed to check account active!", e);
+		} catch (PermissionDeniedException e) {
+			return false;
+		}
+	}
+
+	public XtaTransportReport sendMessage(XtaMessage messageWithoutMessageId) {
+		var message = getXtaMessageWithMessageId(messageWithoutMessageId);
+		try {
+			service.sendMessage(message);
+			return getTransportReportOrThrowException(message.metaData());
+		} catch (XTAWSTechnicalProblemException | PermissionDeniedException | SyncAsyncException | ParameterIsNotValidException |
+				MessageVirusDetectionException | MessageSchemaViolationException | RuntimeException e) {
+			throw new XtaClientRuntimeException("Failed to send message!", e);
+		}
+	}
+
+	XtaMessage getXtaMessageWithMessageId(XtaMessage messageWithoutMessageId) {
+		var authorIdentifier = messageWithoutMessageId.metaData().authorIdentifier();
+		try {
+			var messageId = service.createMessageId(authorIdentifier);
+			return createXtaMessageWithMessageId(messageWithoutMessageId, messageId);
+		} catch (XTAWSTechnicalProblemException | PermissionDeniedException | RuntimeException e) {
+			throw new XtaClientRuntimeException("Failed to create message id!", e);
+		}
+	}
+
+	XtaMessage createXtaMessageWithMessageId(XtaMessage messageWithoutMessageId, String messageId) {
+		return messageWithoutMessageId.toBuilder()
+				.metaData(messageWithoutMessageId.metaData().toBuilder()
+						.messageId(messageId)
+						.build())
+				.build();
+	}
+
+	XtaTransportReport getTransportReportOrThrowException(XtaMessageMetaData messageMetaData) {
+		var messageId = messageMetaData.messageId();
+		var authorId = messageMetaData.authorIdentifier();
+		try {
+			return service.getTransportReport(messageId, authorId);
+		} catch (XTAWSTechnicalProblemException | PermissionDeniedException | InvalidMessageIDException | RuntimeException e) {
+			throw new XtaClientRuntimeException(
+					"Failed to get transport report! (messageId: %s, reader: %s)".formatted(messageId, authorId.value()), e);
+		}
+	}
+
+	public boolean lookupService(XtaMessageMetaData messageMetaData) {
+		try {
+			return service.lookupService(
+					messageMetaData.service(),
+					messageMetaData.readerIdentifier(),
+					messageMetaData.authorIdentifier()
+			);
+		} catch (XTAWSTechnicalProblemException | PermissionDeniedException | ParameterIsNotValidException | RuntimeException exception) {
+			logWarning("Failed to lookup service for message '%s'!".formatted(messageMetaData.messageId()), exception);
+			return false;
+		}
+	}
+
+	void logWarning(String message, Exception exception) {
+		log.warn(message, exception);
+	}
+
+}
diff --git a/src/main/java/de/ozgcloud/xta/client/core/XtaClientServiceFactory.java b/src/main/java/de/ozgcloud/xta/client/core/XtaClientServiceFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..0343c5060aa5669b1b41c10ba1e74d4b95f2aec4
--- /dev/null
+++ b/src/main/java/de/ozgcloud/xta/client/core/XtaClientServiceFactory.java
@@ -0,0 +1,26 @@
+package de.ozgcloud.xta.client.core;
+
+import de.ozgcloud.xta.client.config.XtaClientConfig;
+import de.ozgcloud.xta.client.exception.XtaClientInitializationException;
+import lombok.Builder;
+
+@Builder
+public class XtaClientServiceFactory {
+
+	private final WrappedXtaServiceFactory wrappedXtaServiceFactory;
+	private final XtaClientConfig config;
+
+	public static XtaClientServiceFactory from(XtaClientConfig config) {
+		return XtaClientServiceFactory.builder()
+				.config(config)
+				.wrappedXtaServiceFactory(WrappedXtaServiceFactory.from(config))
+				.build();
+	}
+
+	public XtaClientService create() throws XtaClientInitializationException {
+		return XtaClientService.builder()
+				.config(config)
+				.service(wrappedXtaServiceFactory.create())
+				.build();
+	}
+}
diff --git a/src/main/java/de/ozgcloud/xta/client/core/XtaExceptionHandler.java b/src/main/java/de/ozgcloud/xta/client/core/XtaExceptionHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..a31641c1ada3c3743ec3500a731703948469f59c
--- /dev/null
+++ b/src/main/java/de/ozgcloud/xta/client/core/XtaExceptionHandler.java
@@ -0,0 +1,67 @@
+package de.ozgcloud.xta.client.core;
+
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import de.ozgcloud.xta.client.exception.XtaClientRuntimeException;
+import de.ozgcloud.xta.client.exception.XtaClientException;
+import genv3.de.xoev.transport.xta.x211.ExceptionType;
+import lombok.Builder;
+import lombok.RequiredArgsConstructor;
+
+@Builder
+@RequiredArgsConstructor
+public class XtaExceptionHandler {
+
+	static final String UNEXPECTED_ERROR_MESSAGE = "An unexpected error occurred. Please report this to the xta-client maintainers.";
+
+	public XtaClientException deriveXtaClientException(RuntimeException exception) {
+		if (exception instanceof XtaClientRuntimeException xtaClientRuntimeException) {
+			return deriveXtaClientExceptionFromClientRuntimeException(xtaClientRuntimeException);
+		}
+		return new XtaClientException(UNEXPECTED_ERROR_MESSAGE, exception);
+	}
+
+	XtaClientException deriveXtaClientExceptionFromClientRuntimeException(XtaClientRuntimeException exception) {
+		var cause = exception.getCause();
+		var detailMessageLines = getDetailLines(cause);
+		var message = Stream.concat(
+				Stream.of(exception.getMessage()),
+				detailMessageLines
+		).collect(Collectors.joining("\n"));
+		return new XtaClientException(message, cause);
+	}
+
+	Stream<String> getDetailLines(Throwable cause) {
+		return Optional.ofNullable(cause)
+				.filter(Exception.class::isInstance)
+				.map(Exception.class::cast)
+				.map(this::deriveDetailLinesFromException)
+				.orElse(Stream.empty());
+	}
+
+	Stream<String> deriveDetailLinesFromException(Exception exception) {
+		return getExceptionType(exception)
+				.map(ExceptionType::getErrorCode)
+				.map(num -> Stream.of(exception.getMessage(), "[%s] %s".formatted(num.getCode(), num.getName())))
+				.orElse(Stream.empty());
+	}
+
+	private Optional<ExceptionType> getExceptionType(Exception exception) {
+		return getPrivateFieldValue(exception, "faultInfo")
+				.filter(ExceptionType.class::isInstance)
+				.map(ExceptionType.class::cast);
+	}
+
+	private Optional<Object> getPrivateFieldValue(Object object, String fieldName) {
+		try {
+			var field = object.getClass().getDeclaredField(fieldName);
+			field.setAccessible(true);
+			return Optional.of(field.get(object));
+		} catch (NoSuchFieldException | IllegalAccessException | RuntimeException e) {
+			return Optional.empty();
+		}
+	}
+
+}
diff --git a/src/main/java/de/ozgcloud/xta/client/core/XtaExceptionHandlerFactory.java b/src/main/java/de/ozgcloud/xta/client/core/XtaExceptionHandlerFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..67cb8a8d0ec426a01843e9c70ca56da7a93b673d
--- /dev/null
+++ b/src/main/java/de/ozgcloud/xta/client/core/XtaExceptionHandlerFactory.java
@@ -0,0 +1,14 @@
+package de.ozgcloud.xta.client.core;
+
+import lombok.Builder;
+import lombok.RequiredArgsConstructor;
+
+@Builder
+@RequiredArgsConstructor
+public class XtaExceptionHandlerFactory {
+
+	public XtaExceptionHandler create() {
+		return XtaExceptionHandler.builder()
+				.build();
+	}
+}
diff --git a/src/main/java/de/ozgcloud/xta/client/core/XtaPortTripleFactory.java b/src/main/java/de/ozgcloud/xta/client/core/XtaPortTripleFactory.java
index 3da0a62c8fbd0c2e44f2aaf9eb5a35eeea2e8bbd..908dc1e6270c9a2d85b524c33976af5b15ea6018 100644
--- a/src/main/java/de/ozgcloud/xta/client/core/XtaPortTripleFactory.java
+++ b/src/main/java/de/ozgcloud/xta/client/core/XtaPortTripleFactory.java
@@ -14,7 +14,7 @@ import org.apache.cxf.message.Message;
 import org.apache.cxf.transport.http.HTTPConduit;
 
 import de.ozgcloud.xta.client.config.XtaClientConfig;
-import de.ozgcloud.xta.client.exception.ClientInitializationException;
+import de.ozgcloud.xta.client.exception.XtaClientInitializationException;
 import genv3.de.xoev.transport.xta.x211.XTAService;
 import lombok.Builder;
 import lombok.RequiredArgsConstructor;
@@ -41,7 +41,7 @@ public class XtaPortTripleFactory {
 				.build();
 	}
 
-	public XtaPortTriple create() throws ClientInitializationException {
+	public XtaPortTriple create() throws XtaClientInitializationException {
 		log.debug("[createXtaService] Using config: {}", config);
 		return new XtaPortTriple(
 				configurePort(config.getManagementServiceUrl(), xtaService.getManagementPort()),
@@ -50,13 +50,13 @@ public class XtaPortTripleFactory {
 		);
 	}
 
-	private <T> T configurePort(final String endpointUrl, final T port) throws ClientInitializationException {
+	private <T> T configurePort(final String endpointUrl, final T port) throws XtaClientInitializationException {
 		var bindingProvider = (BindingProvider) port;
 		configureBinding(endpointUrl, bindingProvider);
 		return port;
 	}
 
-	void configureBinding(final String endpointUrl, final BindingProvider port) throws ClientInitializationException {
+	void configureBinding(final String endpointUrl, final BindingProvider port) throws XtaClientInitializationException {
 		configureRequestContext(endpointUrl, port.getRequestContext());
 		configureClient(getClientFromPort(port));
 	}
@@ -74,7 +74,7 @@ public class XtaPortTripleFactory {
 		));
 	}
 
-	void configureClient(Client client) throws ClientInitializationException {
+	void configureClient(Client client) throws XtaClientInitializationException {
 		if (config.isLogSoapRequests()) {
 			configureOutInterceptors(client.getOutInterceptors());
 		}
@@ -105,7 +105,7 @@ public class XtaPortTripleFactory {
 		return inInterceptor;
 	}
 
-	void configureHttpConduit(HTTPConduit conduit) throws ClientInitializationException {
+	void configureHttpConduit(HTTPConduit conduit) throws XtaClientInitializationException {
 		conduit.setTlsClientParameters(tlsClientParametersFactory.create());
 	}
 }
diff --git a/src/main/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactory.java b/src/main/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactory.java
index fee35b47d2f185f4b17fa617780ecfaa274b8891..f314f4f2e39274bdb1d94767de35d2b4454adc6c 100644
--- a/src/main/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactory.java
+++ b/src/main/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactory.java
@@ -15,7 +15,7 @@ import org.apache.cxf.configuration.jsse.TLSClientParameters;
 import org.apache.hc.core5.ssl.SSLContextBuilder;
 
 import de.ozgcloud.xta.client.config.XtaClientConfig;
-import de.ozgcloud.xta.client.exception.ClientInitializationException;
+import de.ozgcloud.xta.client.exception.XtaClientInitializationException;
 import lombok.Builder;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -29,7 +29,7 @@ public class XtaTLSClientParametersFactory {
 
 	private final XtaClientConfig config;
 
-	public TLSClientParameters create() throws ClientInitializationException {
+	public TLSClientParameters create() throws XtaClientInitializationException {
 		log.debug("[createXtaTLsParameters] Using config: {}", config);
 		var sslContext = createXtaSslContext();
 
@@ -38,7 +38,7 @@ public class XtaTLSClientParametersFactory {
 		return parameters;
 	}
 
-	public SSLContext createXtaSslContext() throws ClientInitializationException {
+	public SSLContext createXtaSslContext() throws XtaClientInitializationException {
 		try {
 			var sslContextBuilder = createSSLContextBuilder();
 			var clientCertKeystore = config.getClientCertKeystore();
@@ -52,7 +52,7 @@ public class XtaTLSClientParametersFactory {
 			return sslContextBuilder.build();
 		} catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException | UnrecoverableKeyException | IOException |
 				CertificateException e) {
-			throw new ClientInitializationException("Failed to create SSL context: " + e.getMessage(), e);
+			throw new XtaClientInitializationException("Failed to create SSL context: " + e.getMessage(), e);
 		}
 	}
 
diff --git a/src/main/java/de/ozgcloud/xta/client/exception/ClientException.java b/src/main/java/de/ozgcloud/xta/client/exception/ClientException.java
deleted file mode 100644
index 0b898337a04b230eb57d217e42f025a854de6eda..0000000000000000000000000000000000000000
--- a/src/main/java/de/ozgcloud/xta/client/exception/ClientException.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- *     XTA-Client Java Library
- *     Copyright (C) 2021 Koordinierungsstelle für IT-Standards (KoSIT)
- *
- *     This program is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     This program is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package de.ozgcloud.xta.client.exception;
-
-
-/**
- * Allgemeiner Fehler zum signalisieren von clientseitigen Problemen.
- */
-public class ClientException extends Exception {
-
-  public ClientException(final String message, final Throwable cause) {
-    super(message, cause);
-  }
-
-  public ClientException(final String message) {
-    super(message);
-  }
-}
diff --git a/src/main/java/de/ozgcloud/xta/client/exception/ClientInitializationException.java b/src/main/java/de/ozgcloud/xta/client/exception/ClientInitializationException.java
deleted file mode 100644
index 5a1b2a792bf7e5f7ba5c823ec790facbf0dca490..0000000000000000000000000000000000000000
--- a/src/main/java/de/ozgcloud/xta/client/exception/ClientInitializationException.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- *     XTA-Client Java Library
- *     Copyright (C) 2021 Koordinierungsstelle für IT-Standards (KoSIT)
- *
- *     This program is free software: you can redistribute it and/or modify
- *     it under the terms of the GNU General Public License as published by
- *     the Free Software Foundation, either version 3 of the License, or
- *     (at your option) any later version.
- *
- *     This program is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *     GNU General Public License for more details.
- *
- *     You should have received a copy of the GNU General Public License
- *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-package de.ozgcloud.xta.client.exception;
-
-/**
- * Spezieller Fehler zum signalisieren von clientseitigen Problemen während der Initialisierung.
- *
- * @author hmaterny
- */
-public class ClientInitializationException extends ClientException {
-
-  public ClientInitializationException(final String message, final Throwable cause) {
-    super(message, cause);
-  }
-
-  public ClientInitializationException(final String message) {
-    super(message);
-  }
-}
diff --git a/src/main/java/de/ozgcloud/xta/client/exception/XtaClientException.java b/src/main/java/de/ozgcloud/xta/client/exception/XtaClientException.java
new file mode 100644
index 0000000000000000000000000000000000000000..f2530d0647cd923735513241270c51bdbfd12b11
--- /dev/null
+++ b/src/main/java/de/ozgcloud/xta/client/exception/XtaClientException.java
@@ -0,0 +1,17 @@
+package de.ozgcloud.xta.client.exception;
+
+
+/**
+ * A generic exception during xta client method execution.
+ *
+ */
+public class XtaClientException extends Exception {
+
+  public XtaClientException(final String message, final Throwable cause) {
+    super(message, cause);
+  }
+
+  public XtaClientException(final String message) {
+    super(message);
+  }
+}
diff --git a/src/main/java/de/ozgcloud/xta/client/exception/XtaClientInitializationException.java b/src/main/java/de/ozgcloud/xta/client/exception/XtaClientInitializationException.java
new file mode 100644
index 0000000000000000000000000000000000000000..0e361320c45fc3be0580d3f4ffdd3733fdba072d
--- /dev/null
+++ b/src/main/java/de/ozgcloud/xta/client/exception/XtaClientInitializationException.java
@@ -0,0 +1,16 @@
+package de.ozgcloud.xta.client.exception;
+
+/**
+ * A generic exception during xta client initialization.
+ *
+ */
+public class XtaClientInitializationException extends XtaClientException {
+
+  public XtaClientInitializationException(final String message, final Throwable cause) {
+    super(message, cause);
+  }
+
+  public XtaClientInitializationException(final String message) {
+    super(message);
+  }
+}
diff --git a/src/main/java/de/ozgcloud/xta/client/exception/XtaClientRuntimeException.java b/src/main/java/de/ozgcloud/xta/client/exception/XtaClientRuntimeException.java
new file mode 100644
index 0000000000000000000000000000000000000000..9c7d4b89b154d0d76a18d9a00044413fa911f816
--- /dev/null
+++ b/src/main/java/de/ozgcloud/xta/client/exception/XtaClientRuntimeException.java
@@ -0,0 +1,12 @@
+package de.ozgcloud.xta.client.exception;
+
+public class XtaClientRuntimeException extends RuntimeException {
+
+	public XtaClientRuntimeException(String message) {
+		super(message);
+	}
+
+	public XtaClientRuntimeException(String message, Throwable cause) {
+		super(message, cause);
+	}
+}
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/main/java/de/ozgcloud/xta/client/xdomea/XdomeaMetaDataValidator.java b/src/main/java/de/ozgcloud/xta/client/xdomea/XdomeaMetaDataValidator.java
index eafbad0b39346f495d20d52f05b770d214a983c5..7a5c363a7bb2d942438b6009188716dc1bd76e5b 100644
--- a/src/main/java/de/ozgcloud/xta/client/xdomea/XdomeaMetaDataValidator.java
+++ b/src/main/java/de/ozgcloud/xta/client/xdomea/XdomeaMetaDataValidator.java
@@ -5,7 +5,7 @@ import static de.ozgcloud.xta.client.xdomea.mapper.MetadataMapper.*;
 import java.util.List;
 import java.util.Set;
 
-import de.ozgcloud.xta.client.exception.ClientException;
+import de.ozgcloud.xta.client.exception.XtaClientException;
 import de.ozgcloud.xta.client.model.XtaFile;
 import de.ozgcloud.xta.client.model.XtaMessageMetaData;
 import de.ozgcloud.xta.client.xdomea.reader.XdomeaXmlValues;
@@ -26,7 +26,7 @@ public class XdomeaMetaDataValidator {
 			XtaFile xdomeaZipFile,
 			XdomeaXmlValues xdomeaXmlValues,
 			XtaMessageMetaData messageMetadataData
-	) throws ClientException {
+	) throws XtaClientException {
 		validateIdentifierPrefixes(
 				xdomeaXmlValues.authorIdPrefix(),
 				xdomeaXmlValues.readerIdPrefix()
@@ -39,14 +39,14 @@ public class XdomeaMetaDataValidator {
 		validatePrimaryDocumentReferences(xdomeaZipFile, xdomeaXmlValues.primaryDocumentNames());
 	}
 
-	void validateIdentifierPrefixes(String authorIdPrefix, String readerIdPrefix) throws ClientException {
+	void validateIdentifierPrefixes(String authorIdPrefix, String readerIdPrefix) throws XtaClientException {
 		validateIdentifierPrefix(authorIdPrefix, AUTHOR_ID_PREFIX, AUTHOR_ID_PREFIX_NAME);
 		validateIdentifierPrefix(readerIdPrefix, READER_ID_PREFIX, READER_ID_PREFIX_NAME);
 	}
 
-	void validateIdentifierPrefix(String prefix, String expectedPrefix, String prefixName) throws ClientException {
+	void validateIdentifierPrefix(String prefix, String expectedPrefix, String prefixName) throws XtaClientException {
 		if (!expectedPrefix.equals(normalizePrefix(prefix))) {
-			throw new ClientException("Expect prefix of %s identifier to be '%s'! (actual: %s)".formatted(prefixName, expectedPrefix, prefix));
+			throw new XtaClientException("Expect prefix of %s identifier to be '%s'! (actual: %s)".formatted(prefixName, expectedPrefix, prefix));
 		}
 	}
 
@@ -54,19 +54,19 @@ public class XdomeaMetaDataValidator {
 		return prefix.endsWith(":") ? prefix.substring(0, prefix.length() - 1) : prefix;
 	}
 
-	void validatePrimaryDocumentReferences(XtaFile xdomeaZipFile, List<String> primaryDocumentNames) throws ClientException {
+	void validatePrimaryDocumentReferences(XtaFile xdomeaZipFile, List<String> primaryDocumentNames) throws XtaClientException {
 		var entryNames = Set.copyOf(zipFileEntryReader.getEntryNames(xdomeaZipFile.content()));
 		for (var primaryDocumentName : primaryDocumentNames) {
 			if (!entryNames.contains(primaryDocumentName)) {
-				throw new ClientException("Primary document reference '%s' not found in xdomea zip file!".formatted(primaryDocumentName));
+				throw new XtaClientException("Primary document reference '%s' not found in xdomea zip file!".formatted(primaryDocumentName));
 			}
 		}
 	}
 
-	void validateZipFileName(String xdomeaZipFileName, String processId, String messageTypeCode) throws ClientException {
+	void validateZipFileName(String xdomeaZipFileName, String processId, String messageTypeCode) throws XtaClientException {
 		var expectedXdomeaZipFileName = "%s_%s.zip".formatted(processId, messageTypeCode);
 		if (!expectedXdomeaZipFileName.equals(xdomeaZipFileName)) {
-			throw new ClientException(
+			throw new XtaClientException(
 					"Expect xdomea zip file name to equal '%s'! (actual: '%s')".formatted(expectedXdomeaZipFileName, xdomeaZipFileName)
 			);
 		}
diff --git a/src/main/java/de/ozgcloud/xta/client/xdomea/XdomeaXtaMessageCreator.java b/src/main/java/de/ozgcloud/xta/client/xdomea/XdomeaXtaMessageCreator.java
index 2bd66d76bee063031959336990e92acea12daeb2..05c1949899aef4596294624f72055859fd210620 100644
--- a/src/main/java/de/ozgcloud/xta/client/xdomea/XdomeaXtaMessageCreator.java
+++ b/src/main/java/de/ozgcloud/xta/client/xdomea/XdomeaXtaMessageCreator.java
@@ -11,7 +11,7 @@ import org.w3c.dom.Document;
 import org.xml.sax.SAXException;
 
 import de.ozgcloud.common.errorhandling.TechnicalException;
-import de.ozgcloud.xta.client.exception.ClientException;
+import de.ozgcloud.xta.client.exception.XtaClientException;
 import de.ozgcloud.xta.client.model.XtaFile;
 import de.ozgcloud.xta.client.model.XtaMessage;
 import de.ozgcloud.xta.client.model.XtaMessageMetaData;
@@ -32,7 +32,11 @@ public class XdomeaXtaMessageCreator {
 	private final ZipFileEntryReader zipFileEntryReader;
 	private final XdomeaMetaDataValidator metaDataValidator;
 
-	public XtaMessage createMessage(XtaFile xdomeaZipFile) throws ClientException {
+	public static XdomeaXtaMessageCreator createInstance() {
+		return XdomeaXtaMessageCreatorFactory.createInstance().create();
+	}
+
+	public XtaMessage createMessage(XtaFile xdomeaZipFile) throws XtaClientException {
 		return XtaMessage.builder()
 				.metaData(deriveValidMetaData(xdomeaZipFile))
 				.messageFile(xdomeaZipFile)
@@ -40,18 +44,18 @@ public class XdomeaXtaMessageCreator {
 				.build();
 	}
 
-	XtaMessageMetaData deriveValidMetaData(XtaFile xdomeaZipFile) throws ClientException {
+	XtaMessageMetaData deriveValidMetaData(XtaFile xdomeaZipFile) throws XtaClientException {
 		try {
 			var xdomeaXmlValues = readXdomeaXmlValues(xdomeaZipFile);
 			var messageMetadataData = metadataMapper.mapXtaMessageMetadata(xdomeaXmlValues);
 			metaDataValidator.validate(xdomeaZipFile, xdomeaXmlValues, messageMetadataData);
 			return messageMetadataData;
 		} catch (TechnicalException e) {
-			throw new ClientException("Failed to derive valid message metadata from xdomea document!", e);
+			throw new XtaClientException("Failed to derive valid message metadata from xdomea document!", e);
 		}
 	}
 
-	XdomeaXmlValues readXdomeaXmlValues(XtaFile xdomeaZipFile) throws ClientException {
+	XdomeaXmlValues readXdomeaXmlValues(XtaFile xdomeaZipFile) throws XtaClientException {
 		var document = readXdomeaXmlDocument(xdomeaZipFile);
 		return xdomeaValueReader.readValues(document);
 	}
diff --git a/src/main/java/de/ozgcloud/xta/client/xdomea/reader/XdomeaValueReader.java b/src/main/java/de/ozgcloud/xta/client/xdomea/reader/XdomeaValueReader.java
index 90dcbeb54d465a6b50381ce74dc0ad77d744301f..52f3a87e813bf7f8666c6c8d7a08c3ec09c65aaf 100644
--- a/src/main/java/de/ozgcloud/xta/client/xdomea/reader/XdomeaValueReader.java
+++ b/src/main/java/de/ozgcloud/xta/client/xdomea/reader/XdomeaValueReader.java
@@ -7,7 +7,7 @@ import java.util.stream.Collectors;
 
 import org.w3c.dom.Document;
 
-import de.ozgcloud.xta.client.exception.ClientException;
+import de.ozgcloud.xta.client.exception.XtaClientException;
 import lombok.Builder;
 import lombok.RequiredArgsConstructor;
 
@@ -34,7 +34,7 @@ public class XdomeaValueReader {
 
 	private final Map<String, XmlValueReader> xmlValueReaders;
 
-	public XdomeaXmlValues readValues(Document xdomeaXmlDocument) throws ClientException {
+	public XdomeaXmlValues readValues(Document xdomeaXmlDocument) throws XtaClientException {
 		return XdomeaXmlValues.builder()
 				.processId(readRequiredValue(xdomeaXmlDocument, PROCESS_ID_XPATH))
 				.messageTypeCode(readRequiredValue(xdomeaXmlDocument, MESSAGE_TYPE_ID_SUFFIX_XPATH))
@@ -52,11 +52,11 @@ public class XdomeaValueReader {
 				.toList();
 	}
 
-	String readRequiredValue(Document xdomeaXmlDocument, String xpathString) throws ClientException {
+	String readRequiredValue(Document xdomeaXmlDocument, String xpathString) throws XtaClientException {
 		return getXmlValueReader(xpathString)
 				.readNonEmptyTexts(xdomeaXmlDocument)
 				.findFirst()
-				.orElseThrow(() -> new ClientException("Required value " + xpathString + " not found in xdomea xml document!"));
+				.orElseThrow(() -> new XtaClientException("Required value " + xpathString + " not found in xdomea xml document!"));
 	}
 
 	XmlValueReader getXmlValueReader(String xpath) {
diff --git a/src/test/java/de/ozgcloud/xta/client/FetchMessageParameterTest.java b/src/test/java/de/ozgcloud/xta/client/FetchMessageParameterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4ae4671f86a94f8c4d55f714ef651c9ea8bb4649
--- /dev/null
+++ b/src/test/java/de/ozgcloud/xta/client/FetchMessageParameterTest.java
@@ -0,0 +1,108 @@
+package de.ozgcloud.xta.client;
+
+import static de.ozgcloud.xta.client.factory.MessageMetaDataTestFactory.*;
+import static de.ozgcloud.xta.client.factory.XtaClientConfigTestFactory.*;
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+
+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 de.ozgcloud.xta.client.factory.XtaMessageMetaDataTestFactory;
+import de.ozgcloud.xta.client.model.XtaMessage;
+
+class FetchMessageParameterTest {
+
+	private Consumer<XtaMessage> processMessage;
+
+	private FetchMessageParameter parameter;
+
+	@BeforeEach
+	void setup() {
+		processMessage = message -> {
+		};
+		parameter = new FetchMessageParameter(SELF_IDENTIFIER, processMessage, Set.of(MESSAGE_ID2));
+
+	}
+
+	@DisplayName("with viewed message ids from")
+	@Nested
+	class TestWithViewedMessageIdsFrom {
+
+		@DisplayName("should keep client identifier")
+		@Test
+		void shouldKeepClientIdentifier() {
+			var result = parameter.withViewedMessageIdsFrom(Collections.emptyList());
+
+			assertThat(result.clientIdentifier()).isEqualTo(SELF_IDENTIFIER);
+		}
+
+		@DisplayName("should keep process message")
+		@Test
+		void shouldKeepProcessMessage() {
+			var result = parameter.withViewedMessageIdsFrom(Collections.emptyList());
+
+			assertThat(result.processMessage()).isEqualTo(processMessage);
+		}
+
+		@DisplayName("should return copy")
+		@Test
+		void shouldReturnCopy() {
+			var result = parameter.withViewedMessageIdsFrom(Collections.emptyList());
+
+			assertThat(result.viewedMessageIds()).containsExactly(MESSAGE_ID2);
+		}
+
+		@DisplayName("should return copy with additional viewed message ids")
+		@Test
+		void shouldReturnCopyWithAdditionalViewedMessageIds() {
+			var result = parameter.withViewedMessageIdsFrom(List.of(XtaMessageMetaDataTestFactory.create()));
+
+			assertThat(result.viewedMessageIds()).containsExactlyInAnyOrder(MESSAGE_ID2, MESSAGE_ID);
+		}
+
+		@DisplayName("should not change original with additional viewed message ids")
+		@Test
+		void shouldNotChangeOriginalWithAdditionalViewedMessageIds() {
+			parameter.withViewedMessageIdsFrom(List.of(XtaMessageMetaDataTestFactory.create()));
+
+			assertThat(parameter.viewedMessageIds()).containsExactly(MESSAGE_ID2);
+		}
+	}
+
+	@DisplayName("has message already been viewed")
+	@Nested
+	class TestHasMessageAlreadyBeenViewed {
+
+		@DisplayName("should return true if message id is in viewed message ids")
+		@Test
+		void shouldReturnTrueIfMessageIdIsInViewedMessageIds() {
+			var messageMetaData = XtaMessageMetaDataTestFactory.createBuilder()
+					.messageId(MESSAGE_ID2)
+					.build();
+
+			var result = parameter.hasMessageAlreadyBeenViewed(messageMetaData);
+
+			assertThat(result).isTrue();
+		}
+
+		@DisplayName("should return false if message id is not in viewed message ids")
+		@Test
+		void shouldReturnFalseIfMessageIdIsNotInViewedMessageIds() {
+			var messageMetaData = XtaMessageMetaDataTestFactory.createBuilder()
+					.messageId(MESSAGE_ID3)
+					.build();
+
+			var result = parameter.hasMessageAlreadyBeenViewed(messageMetaData);
+
+			assertThat(result).isFalse();
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/src/test/java/de/ozgcloud/xta/client/XtaClientFactoryTest.java b/src/test/java/de/ozgcloud/xta/client/XtaClientFactoryTest.java
index 30771668e180b8e605fce3fda7579e4d36e92f35..2461d7d296a5830831890f4dec672de33a3a2e53 100644
--- a/src/test/java/de/ozgcloud/xta/client/XtaClientFactoryTest.java
+++ b/src/test/java/de/ozgcloud/xta/client/XtaClientFactoryTest.java
@@ -12,8 +12,10 @@ import org.mockito.Mock;
 
 import de.ozgcloud.xta.client.config.XtaClientConfig;
 import de.ozgcloud.xta.client.config.XtaConfigValidator;
-import de.ozgcloud.xta.client.core.WrappedXtaService;
-import de.ozgcloud.xta.client.core.WrappedXtaServiceFactory;
+import de.ozgcloud.xta.client.core.XtaClientService;
+import de.ozgcloud.xta.client.core.XtaClientServiceFactory;
+import de.ozgcloud.xta.client.core.XtaExceptionHandler;
+import de.ozgcloud.xta.client.core.XtaExceptionHandlerFactory;
 import lombok.SneakyThrows;
 
 class XtaClientFactoryTest {
@@ -21,9 +23,11 @@ class XtaClientFactoryTest {
 	@Mock
 	private XtaConfigValidator configValidator;
 	@Mock
-	private WrappedXtaServiceFactory wrappedXtaServiceFactory;
+	private XtaClientServiceFactory xtaClientServiceFactory;
 	@Mock
 	private XtaClientConfig config;
+	@Mock
+	private XtaExceptionHandlerFactory xtaExceptionHandlerFactory;
 
 	@InjectMocks
 	private XtaClientFactory factory;
@@ -31,32 +35,16 @@ class XtaClientFactoryTest {
 	@DisplayName("create")
 	@Nested
 	class TestCreate {
-
 		@Mock
-		private WrappedXtaService service;
+		private XtaClientService service;
+		@Mock
+		private XtaExceptionHandler exceptionHandler;
 
 		@BeforeEach
 		@SneakyThrows
 		void mock() {
-			when(wrappedXtaServiceFactory.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);
+			when(xtaClientServiceFactory.create()).thenReturn(service);
+			when(xtaExceptionHandlerFactory.create()).thenReturn(exceptionHandler);
 		}
 
 		@DisplayName("should call validate")
@@ -67,5 +55,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)
+							.exceptionHandler(exceptionHandler)
+							.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 cf0c0bc44e15d973e457666aeeb5ac3332943a1a..64681cfe9abead1d25b6f0d8626241b0f90244a7 100644
--- a/src/test/java/de/ozgcloud/xta/client/XtaClientITCase.java
+++ b/src/test/java/de/ozgcloud/xta/client/XtaClientITCase.java
@@ -1,149 +1,312 @@
 package de.ozgcloud.xta.client;
 
+import static de.ozgcloud.xta.client.XtaClient.*;
 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 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.xta.client.exception.XtaClientException;
+import de.ozgcloud.xta.client.extension.StaticStringListAppender;
 import de.ozgcloud.xta.client.extension.XtaMessageExampleLoader;
-import de.ozgcloud.xta.client.extension.XtaServerSetupExtensionTestUtil;
 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 genv3.de.xoev.transport.xta.x211.InvalidMessageIDException;
+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 silentTestClient;
+	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<>();
+		silentTestClient = XTA_TEST_SERVER_SETUP_EXTENSION.getSilentTestClient();
+
+		StaticStringListAppender.clearLogLines();
+		closeMessagesForAllReaders();
 	}
 
-	@DisplayName("get messages metadata")
-	@Nested
-	class TestGetMessagesMetadata {
+	private void closeMessagesForAllReaders() {
+		var silentTestClientConfig = XTA_TEST_SERVER_SETUP_EXTENSION.getSilentTestClientConfig();
+		closeAllMessages(silentTestClientConfig, READER_CLIENT_IDENTIFIER1);
+		closeAllMessages(silentTestClientConfig, READER_CLIENT_IDENTIFIER2);
+		closeAllMessages(silentTestClientConfig, READER_CLIENT_IDENTIFIER3);
+	}
 
-		@DisplayName("with no messages")
-		@Nested
-		class TestWithNoMessages {
+	@DisplayName("fetch messages")
+	@Nested
+	class TestFetchMessages {
 
-			@DisplayName("should return zero pending messages")
-			@Test
-			@SneakyThrows
-			void shouldReturnZeroPendingMessages() {
-				var result = client.getMessagesMetadata(READER_CLIENT_IDENTIFIER1.value());
+		private List<XtaMessage> sendMessages;
+		private List<String> sendMessageIds;
 
-				assertThat(result.pendingMessageCount()).isZero();
-			}
+		@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)
+			);
+			sendMessageIds = sendMessages.stream()
+					.map(message -> sendTestMessage(silentTestClient, message))
+					.toList();
 		}
 
-		@DisplayName("with one message")
-		@Nested
-		class TestWithOneMessage {
-
-			@BeforeEach
-			void setup() {
-				XTA_TEST_SERVER_SETUP_EXTENSION.sendTestMessage();
-			}
+		private XtaMessage createMessage(String messageLabel, XtaIdentifier author, XtaIdentifier reader) {
+			return XtaMessageExampleLoader.load(
+					XtaMessageExampleLoader.MessageExampleConfig.builder()
+							.messageLabel(messageLabel)
+							.reader(reader)
+							.author(author)
+							.build());
+		}
 
-			@DisplayName("should return one pending message for client")
-			@Test
-			@SneakyThrows
-			void shouldReturnOnePendingMessageClient() {
-				var result = client.getMessagesMetadata(READER_CLIENT_IDENTIFIER1.value());
+		@DisplayName("should throw exception on connection failure")
+		@Test
+		@SneakyThrows
+		void shouldThrowExceptionOnConnectionFailure() {
+			setupClientWithoutTrustStore();
 
-				assertThat(result.pendingMessageCount()).isOne();
-			}
+			assertThatThrownBy(() -> testClient.fetchMessages((message) -> fail("Should not process any message!")))
+					.isInstanceOf(XtaClientException.class);
+		}
 
-			@DisplayName("should return no pending message for another client")
-			@Test
-			@SneakyThrows
-			void shouldReturnNoPendingMessageForAnotherClient() {
-				var result = client.getMessagesMetadata(READER_CLIENT_IDENTIFIER2.value());
+		@DisplayName("should fetch no messages if no client identifier is configured")
+		@Test
+		void shouldFetchNoMessagesIfNoClientIdentifierIsConfigured() {
+			setupClientWithIdentifiers(emptyList());
 
-				assertThat(result.pendingMessageCount()).isZero();
-			}
+			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));
 
-	@DisplayName("get message")
-	@Nested
-	class TestGetMessage {
+			var messages = fetchMessages();
 
-		private String messageId;
-		private XtaMessage message;
+			assertThat(supportCheckedMetadataItems).isEmpty();
+			assertThat(processedMessages).isEmpty();
+			assertThat(messages).isEmpty();
+		}
 
-		@BeforeEach
+		@DisplayName("should fetch messages from first reader")
+		@Test
 		@SneakyThrows
-		void setup() {
-			var messageConfig = XtaMessageExampleLoader.MessageExampleConfig.builder()
-					.messageLabel("dfoerdermittel")
-					.reader(READER_CLIENT_IDENTIFIER1)
-					.author(AUTHOR_CLIENT_IDENTIFIER)
-					.build();
-			message = XtaMessageExampleLoader.load(messageConfig);
-			messageId = XtaServerSetupExtensionTestUtil.sendTestMessage(client, message);
+		void shouldFetchMessagesFromFirstReader() {
+			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1));
+
+			var transportReports = fetchMessages();
+
+			assertThat(supportCheckedMetadataItems).hasSize(1);
+			assertThatMessages(processedMessages).containExactlyInAnyOrder(sendMessages.getFirst());
+			assertThatTransportReports(transportReports)
+					.reportExactlyFor(processedMessages)
+					.haveExactlyClosedStatusFor(messageIdBySendIndex(0));
 		}
 
-		@DisplayName("should return message with green status")
+		@DisplayName("should fetch messages from second reader")
 		@Test
-		@SneakyThrows
-		void shouldReturnMessageWithGreenStatus() {
-			var result = client.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId);
+		void shouldFetchMessagesFromSecondReader() {
+			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER2));
 
-			assertThat(result.message().metaData().messageId()).isEqualTo(messageId);
-			assertThat(result.transportReport().metaData().messageId()).isEqualTo(messageId);
-			assertThat(result.transportReport().status()).isEqualTo(XtaMessageStatus.GREEN);
+			var transportReports = fetchMessages();
+
+			assertThat(supportCheckedMetadataItems).hasSize(3);
+			assertThatMessages(processedMessages).containExactlyInAnyOrder(sendMessages.get(1), sendMessages.get(2), sendMessages.get(3));
+			assertThatTransportReports(transportReports)
+					.reportExactlyFor(processedMessages)
+					.haveExactlyClosedStatusFor(messageIdBySendIndex(1), messageIdBySendIndex(2), messageIdBySendIndex(3));
 		}
 
-		@DisplayName("should return message with correct message file content")
+		@DisplayName("should fetch messages from first and second reader")
 		@Test
-		@SneakyThrows
-		void shouldReturnMessageWithCorrectMessageFileContent() {
-			var messageContent = extractMessageFileContent(message);
+		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)
+					.haveExactlyClosedStatusFor(
+							messageIdBySendIndex(0),
+							messageIdBySendIndex(1), messageIdBySendIndex(2), messageIdBySendIndex(3)
+					);
+		}
 
-			var result = client.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId);
-			var resultContent = extractMessageFileContent(result.message());
+		@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)
+					.haveExactlyClosedStatusFor(
+							messageIdBySendIndex(0),
+							messageIdBySendIndex(1), messageIdBySendIndex(2), messageIdBySendIndex(3),
+							messageIdBySendIndex(4), messageIdBySendIndex(5)
+					);
+		}
 
-			assertThat(messageContent).isEqualTo(resultContent);
+		@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);
+			assertThat(hasLogLineContaining(NO_MESSAGE_CLOSED_WARNING)).isFalse();
+			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)
+					.haveExactlyClosedStatusFor(
+							messageIdBySendIndex(0),
+							messageIdBySendIndex(1), messageIdBySendIndex(2)
+					);
 		}
 
-		@DisplayName("should not show message id for a closed message in status list")
+		@DisplayName("should close messages only if no exception occurs during processing, with no exception for author3")
 		@Test
-		@SneakyThrows
-		void shouldNotShowMessageIdForClosedMessageInStatusList() {
-			assertThatNoException().isThrownBy(() -> client.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId));
-			var metadataResult = client.getMessagesMetadata(READER_CLIENT_IDENTIFIER1.value());
-			if (!metadataResult.messages().isEmpty()) {
-				assertThat(metadataResult.messages()).allMatch(metadata -> !messageId.equals(metadata.messageId()));
+		void shouldCloseMessagesOnlyIfNoExceptionOccursDuringProcessingWithNoExceptionForAuthor3() {
+			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3));
+			processMessageDummy = message -> throwRuntimeExceptionExceptForAuthorIdentifier(message, AUTHOR_CLIENT_IDENTIFIER3);
+
+			var transportReports = fetchMessages();
+
+			assertThat(supportCheckedMetadataItems).hasSize(1 + 2 + 2);
+			assertThat(hasLogLineContaining(NO_MESSAGE_CLOSED_WARNING)).isTrue();
+			assertThatMessages(processedMessages).containMetaDataExactlyInAnyOrder(
+					sendMessages.get(0).metaData(),
+					supportCheckedMetadataItems.get(1), supportCheckedMetadataItems.get(2),
+					sendMessages.get(4).metaData(), sendMessages.get(5).metaData()
+			);
+			assertThatTransportReports(transportReports)
+					.reportExactlyFor(processedMessages)
+					.haveExactlyClosedStatusFor(
+							messageIdBySendIndex(5)
+					);
+		}
+
+		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 throw invalid message id exception for modified message id")
+		@DisplayName("should process messages only if supported, with support for author1")
 		@Test
-		void shouldThrowInvalidMessageIdExceptionForModifiedMessageId() {
-			assertThatThrownBy(() -> client.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId + "1"))
-					.isInstanceOf(InvalidMessageIDException.class);
+		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);
+			assertThat(hasLogLineContaining(NO_MESSAGE_CLOSED_WARNING)).isFalse();
+			assertThatMessages(processedMessages).containExactlyInAnyOrder(sendMessages.get(0), sendMessages.get(1), sendMessages.get(2));
+			assertThatTransportReports(transportReports)
+					.reportExactlyFor(processedMessages)
+					.haveExactlyClosedStatusFor(messageIdBySendIndex(0), messageIdBySendIndex(1), messageIdBySendIndex(2));
 		}
 
-		@DisplayName("should throw invalid message id exception for other client")
+		@DisplayName("should process messages only if supported, with support for author3")
 		@Test
-		void shouldThrowInvalidMessageIdExceptionForOtherClient() {
-			assertThatThrownBy(() -> client.getMessage(READER_CLIENT_IDENTIFIER2.value(), messageId))
-					.isInstanceOf(InvalidMessageIDException.class);
+		void shouldProcessMessagesOnlyIfSupportedWithSupportForAuthor3() {
+			setupClientWithIdentifiers(List.of(READER_CLIENT_IDENTIFIER1, READER_CLIENT_IDENTIFIER2, READER_CLIENT_IDENTIFIER3));
+			isSupportedDummy = metaData -> metaData.authorIdentifier().value().equals(AUTHOR_CLIENT_IDENTIFIER3.value());
+
+			var transportReports = fetchMessages();
+
+			assertThat(supportCheckedMetadataItems).hasSize(1 + 2 + 2);
+			assertThat(hasLogLineContaining(NO_MESSAGE_CLOSED_WARNING)).isTrue();
+			assertThatMessages(processedMessages).containExactlyInAnyOrder(sendMessages.get(5));
+			assertThatTransportReports(transportReports)
+					.reportExactlyFor(processedMessages)
+					.haveExactlyClosedStatusFor(messageIdBySendIndex(5));
+		}
+
+		@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) {
+			return sendMessageIds.get(sendIndex);
+		}
+
+		@SneakyThrows
+		private List<XtaTransportReport> fetchMessages() {
+			return testClient.fetchMessages((message) -> {
+				processedMessages.add(message);
+				processMessageDummy.accept(message);
+			});
 		}
 	}
 
@@ -158,20 +321,41 @@ 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);
 		}
 
+		@DisplayName("should throw exception on connection failure")
+		@Test
+		@SneakyThrows
+		void shouldThrowExceptionOnConnectionFailure() {
+			setupClientWithoutTrustStore();
+
+			assertThatThrownBy(() -> testClient.sendMessage(message))
+					.isInstanceOf(XtaClientException.class);
+		}
+
 		@DisplayName("should return transport report with open status")
 		@Test
 		@SneakyThrows
 		void shouldReturnTransportReportWithOpenStatus() {
-			var transportReport = client.sendMessage(message);
+			var transportReport = silentTestClient.sendMessage(message);
 
 			assertThat(transportReport.status()).isEqualTo(XtaMessageStatus.OPEN);
 		}
+	}
 
+	@SneakyThrows
+	private void setupClientWithoutTrustStore() {
+		testClient = XtaClient.from(
+				XTA_TEST_SERVER_SETUP_EXTENSION.createSpecificClientConfigBuilder()
+						.trustStore(null)
+						.clientIdentifiers(List.of(READER_CLIENT_IDENTIFIER1))
+						.maxListItems(TWO_MAX_LIST_ITEMS)
+						.isMessageSupported(metaData -> fail("Should not process any message!"))
+						.build()
+		);
 	}
 }
 
diff --git a/src/test/java/de/ozgcloud/xta/client/XtaClientRemoteITCase.java b/src/test/java/de/ozgcloud/xta/client/XtaClientRemoteITCase.java
index 42f008a07a14ec9c27d72fd802907dcead5db1af..0ceac4857e86725204502ed1d2828a864c891ce7 100644
--- a/src/test/java/de/ozgcloud/xta/client/XtaClientRemoteITCase.java
+++ b/src/test/java/de/ozgcloud/xta/client/XtaClientRemoteITCase.java
@@ -1,8 +1,17 @@
 package de.ozgcloud.xta.client;
 
+import static de.ozgcloud.xta.client.XtaClientITCase.*;
 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.Ignore;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
@@ -13,14 +22,17 @@ import org.junit.jupiter.api.extension.RegisterExtension;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 
+import de.ozgcloud.xta.client.config.XtaClientConfig;
+import de.ozgcloud.xta.client.extension.StaticStringListAppender;
 import de.ozgcloud.xta.client.extension.XtaMessageExampleLoader;
 import de.ozgcloud.xta.client.extension.XtaRemoteServerSetupExtension;
 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.xdomea.XdomeaXtaMessageCreatorFactory;
-import genv3.de.xoev.transport.xta.x211.InvalidMessageIDException;
-import genv3.de.xoev.transport.xta.x211.MessageSchemaViolationException;
+import de.ozgcloud.xta.client.model.XtaTransportReport;
+import de.ozgcloud.xta.client.xdomea.XdomeaXtaMessageCreator;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 
@@ -32,134 +44,205 @@ import lombok.extern.slf4j.Slf4j;
 				"This test requires the path KOP_SH_KIEL_{DEV,TEST}_PATH and password KOP_SH_KIEL_{DEV,TEST}_PASSWORD of KOP_SH_KIEL_DEV.p12 and OZG-CLOUD_SH_KIEL_TEST.pfx. "
 						+ "Additionally, the endpoint of the DEV-xta-server at li33-0005.dp.dsecurecloud.de must be reachable."
 )
+@Ignore
 class XtaClientRemoteITCase {
 
 	@RegisterExtension
 	static final XtaRemoteServerSetupExtension XTA_REMOTE_SERVER_SETUP_EXTENSION = new XtaRemoteServerSetupExtension();
 
-	private XtaClient authorClient;
-	private XtaClient readerClient;
+	private static final XdomeaXtaMessageCreator XDOMEA_XTA_MESSAGE_CREATOR = XdomeaXtaMessageCreator.createInstance();
+
+	private XtaClient testClient;
+	private XtaClient silentTestClient;
+	private XtaClient devClient;
+	private XtaClient silentDevClient;
+
+	private List<XtaMessageMetaData> supportCheckedMetadataItems;
+	private List<XtaMessage> processedMessages;
+	private Consumer<XtaMessage> processMessageDummy;
+	private Predicate<XtaMessageMetaData> isSupportedDummy;
 
 	@BeforeEach
 	@SneakyThrows
 	void setup() {
-		authorClient = XTA_REMOTE_SERVER_SETUP_EXTENSION.getAuthorClient();
-		readerClient = XTA_REMOTE_SERVER_SETUP_EXTENSION.getReaderClient();
+		processMessageDummy = (message) -> {
+		};
+		supportCheckedMetadataItems = new ArrayList<>();
+		isSupportedDummy = (metaData) -> true;
+		processedMessages = new ArrayList<>();
+
+		testClient = XTA_REMOTE_SERVER_SETUP_EXTENSION.getTestClient();
+		silentTestClient = XTA_REMOTE_SERVER_SETUP_EXTENSION.getSilentTestClient();
+		devClient = XTA_REMOTE_SERVER_SETUP_EXTENSION.getDevClient();
+		silentDevClient = XTA_REMOTE_SERVER_SETUP_EXTENSION.getSilentDevClient();
+
+		// Fail if any message pending, to ensure that we do not clear existing messages in the DEV environment
+		failIfAnyMessagePending(XTA_REMOTE_SERVER_SETUP_EXTENSION.getSilentDevClientConfig(), DEV_READER_CLIENT_IDENTIFIER);
+		failIfAnyMessagePending(XTA_REMOTE_SERVER_SETUP_EXTENSION.getSilentTestClientConfig(), TEST_READER_CLIENT_IDENTIFIER);
+
+		StaticStringListAppender.clearLogLines();
 	}
 
-	@DisplayName("get messages metadata")
-	@Nested
-	class TestGetMessagesMetadata {
+	@AfterEach
+	void cleanup() {
+		closeMessagesForAllReaders();
+	}
+
+	private void closeMessagesForAllReaders() {
+		closeAllMessages(XTA_REMOTE_SERVER_SETUP_EXTENSION.getSilentDevClientConfig(), DEV_READER_CLIENT_IDENTIFIER);
+		closeAllMessages(XTA_REMOTE_SERVER_SETUP_EXTENSION.getSilentTestClientConfig(), TEST_READER_CLIENT_IDENTIFIER);
+	}
 
-		@DisplayName("with no messages")
-		@Nested
-		class TestWithNoMessages {
+	@DisplayName("fetch messages")
+	@Nested
+	class TestFetchMessages {
 
-			@DisplayName("should return zero pending messages")
-			@Test
-			@SneakyThrows
-			void shouldReturnZeroPendingMessages() {
-				var result = readerClient.getMessagesMetadata(READER_CLIENT_IDENTIFIER1.value());
+		private List<XtaMessage> sendMessages;
+		private List<String> sendMessageIds;
 
-				assertThat(result.pendingMessageCount()).isZero();
-			}
+		@BeforeEach
+		void setup() {
+			sendMessages = List.of(
+					createMessage("dfoerdermittel", DEV_READER_CLIENT_IDENTIFIER, DEV_READER_CLIENT_IDENTIFIER),
+					createMessage("dfoerdermittel", DEV_READER_CLIENT_IDENTIFIER, TEST_READER_CLIENT_IDENTIFIER),
+					createMessage("abgabe0401-kleiner-waffenschein", TEST_AUTHOR_CLIENT_IDENTIFIER, TEST_READER_CLIENT_IDENTIFIER),
+					createMessage("dfoerdermittel", TEST_READER_CLIENT_IDENTIFIER, TEST_READER_CLIENT_IDENTIFIER)
+			);
+			sendMessageIds = sendMessages.stream()
+					.map(message -> sendTestMessage(
+							message.metaData().authorIdentifier().equals(DEV_READER_CLIENT_IDENTIFIER)
+									? silentDevClient
+									: silentTestClient,
+							message))
+					.toList();
 		}
 
-		@DisplayName("with one message")
-		@Nested
-		class TestWithOneMessage {
+		@DisplayName("should fetch no messages if no client identifier is configured")
+		@Test
+		void shouldFetchNoMessagesIfNoClientIdentifierIsConfigured() {
+			setupClientsWithIdentifiers(emptyList());
 
-			@BeforeEach
-			void setup() {
-				XTA_REMOTE_SERVER_SETUP_EXTENSION.sendTestMessage();
-			}
+			var messages = fetchMessages();
 
-			@DisplayName("should return one pending message for client")
-			@Test
-			@SneakyThrows
-			void shouldReturnOnePendingMessageClient() {
-				var result = readerClient.getMessagesMetadata(READER_CLIENT_IDENTIFIER1.value());
+			assertThat(supportCheckedMetadataItems).isEmpty();
+			assertThat(processedMessages).isEmpty();
+			assertThat(messages).isEmpty();
+		}
 
-				assertThat(result.pendingMessageCount()).isOne();
-			}
+		@DisplayName("should fetch no messages if client identifier has no messages pending")
+		@Test
+		void shouldFetchNoMessagesIfClientIdentifierHasNoMessagesPending() {
+			setupClientsWithIdentifiers(List.of(TEST_AUTHOR_CLIENT_IDENTIFIER));
 
-			@DisplayName("should return no pending message for another client")
-			@Test
-			@SneakyThrows
-			void shouldReturnNoPendingMessageForAnotherClient() {
-				var result = readerClient.getMessagesMetadata(READER_CLIENT_IDENTIFIER2.value());
+			var messages = fetchMessages();
 
-				assertThat(result.pendingMessageCount()).isZero();
-			}
+			assertThat(supportCheckedMetadataItems).isEmpty();
+			assertThat(processedMessages).isEmpty();
+			assertThat(messages).isEmpty();
 		}
 
-	}
-
-	@DisplayName("get message")
-	@Nested
-	class TestGetMessage {
+		@DisplayName("should fetch messages from first reader")
+		@Test
+		@SneakyThrows
+		void shouldFetchMessagesFromFirstReader() {
+			setupClientsWithIdentifiers(List.of(DEV_READER_CLIENT_IDENTIFIER));
 
-		private String messageId;
-		private XtaMessage message;
+			var transportReports = fetchMessages();
 
-		@BeforeEach
-		@SneakyThrows
-		void setup() {
-			var messageConfig = XtaMessageExampleLoader.MessageExampleConfig.builder()
-					.messageLabel("dfoerdermittel")
-					.reader(READER_CLIENT_IDENTIFIER1)
-					.author(AUTHOR_CLIENT_IDENTIFIER)
-					.build();
-			message = XtaMessageExampleLoader.load(messageConfig);
-			messageId = XTA_REMOTE_SERVER_SETUP_EXTENSION.sendTestMessage(message);
+			assertThat(supportCheckedMetadataItems).hasSize(1);
+			assertThatMessages(processedMessages).containExactlyInAnyOrder(sendMessages.getFirst());
+			assertThatTransportReports(transportReports)
+					.reportExactlyFor(processedMessages)
+					.haveExactlyClosedStatusFor(messageIdBySendIndex(0));
 		}
 
-		@DisplayName("should return message with green status")
+		@DisplayName("should fetch messages from second reader")
 		@Test
-		@SneakyThrows
-		void shouldReturnMessageWithGreenStatus() {
-			var result = readerClient.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId);
+		void shouldFetchMessagesFromSecondReader() {
+			setupClientsWithIdentifiers(List.of(TEST_READER_CLIENT_IDENTIFIER));
+
+			var transportReports = fetchMessages();
 
-			assertThat(result.message().metaData().messageId()).isEqualTo(messageId);
-			assertThat(result.transportReport().metaData().messageId()).isEqualTo(messageId);
-			assertThat(result.transportReport().status()).isEqualTo(XtaMessageStatus.GREEN);
+			assertThat(supportCheckedMetadataItems).hasSize(3);
+			assertThatMessages(processedMessages).containExactlyInAnyOrder(sendMessages.get(1), sendMessages.get(2), sendMessages.get(3));
+			assertThatTransportReports(transportReports)
+					.reportExactlyFor(processedMessages)
+					.haveExactlyClosedStatusFor(messageIdBySendIndex(1), messageIdBySendIndex(2), messageIdBySendIndex(3));
 		}
 
-		@DisplayName("should return message with correct message file content")
+		@DisplayName("should fetch messages from first and second reader")
 		@Test
+		void shouldFetchMessagesFromFirstAndSecondReader() {
+			setupClientsWithIdentifiers(List.of(DEV_READER_CLIENT_IDENTIFIER, TEST_READER_CLIENT_IDENTIFIER));
+
+			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)
+					.haveExactlyClosedStatusFor(
+							messageIdBySendIndex(0),
+							messageIdBySendIndex(1), messageIdBySendIndex(2), messageIdBySendIndex(3)
+					);
+		}
+
 		@SneakyThrows
-		void shouldReturnMessageWithCorrectMessageFileContent() {
-			var messageContent = extractMessageFileContent(message);
+		private void setupClientsWithIdentifiers(List<XtaIdentifier> identifiers) {
+			devClient = createClientWithIdentifiersAndClientCert(
+					identifiers,
+					XTA_REMOTE_SERVER_SETUP_EXTENSION.getDevClientConfig().getClientCertKeystore()
+			);
+			testClient = createClientWithIdentifiersAndClientCert(
+					identifiers,
+					XTA_REMOTE_SERVER_SETUP_EXTENSION.getTestClientConfig().getClientCertKeystore()
+			);
+		}
 
-			var result = readerClient.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId);
-			var resultContent = extractMessageFileContent(result.message());
+		@SneakyThrows
+		private XtaClient createClientWithIdentifiersAndClientCert(List<XtaIdentifier> identifiers, XtaClientConfig.KeyStore clientCertKeyStore) {
+			return XtaClient.from(
+					XTA_REMOTE_SERVER_SETUP_EXTENSION.createSpecificClientConfigBuilder()
+							.clientCertKeystore(clientCertKeyStore)
+							.clientIdentifiers(identifiers)
+							.maxListItems(TWO_MAX_LIST_ITEMS)
+							.isMessageSupported(this::isSupported)
+							.build()
+			);
+		}
 
-			assertThat(messageContent).isEqualTo(resultContent);
+		private boolean isSupported(XtaMessageMetaData metaData) {
+			supportCheckedMetadataItems.add(metaData);
+			return isSupportedDummy.test(metaData);
+		}
+
+		private String messageIdBySendIndex(int sendIndex) {
+			return sendMessageIds.get(sendIndex);
+		}
+
+		private List<XtaTransportReport> fetchMessages() {
+			return Stream.concat(
+					fetchDevMessages().stream(),
+					fetchTestMessages().stream()
+			).toList();
 		}
 
-		@DisplayName("should not show message id for a closed message in status list")
-		@Test
 		@SneakyThrows
-		void shouldNotShowMessageIdForClosedMessageInStatusList() {
-			assertThatNoException().isThrownBy(() -> readerClient.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId));
-			var metadataResult = readerClient.getMessagesMetadata(READER_CLIENT_IDENTIFIER1.value());
-			if (!metadataResult.messages().isEmpty()) {
-				assertThat(metadataResult.messages()).allMatch(metadata -> !messageId.equals(metadata.messageId()));
-			}
+		private List<XtaTransportReport> fetchDevMessages() {
+			return devClient.fetchMessages(this::processMessage);
 		}
 
-		@DisplayName("should throw invalid message id exception for modified message id")
-		@Test
-		void shouldThrowInvalidMessageIdExceptionForModifiedMessageId() {
-			assertThatThrownBy(() -> readerClient.getMessage(READER_CLIENT_IDENTIFIER1.value(), messageId + "1"))
-					.isInstanceOf(InvalidMessageIDException.class);
+		@SneakyThrows
+		private List<XtaTransportReport> fetchTestMessages() {
+			return testClient.fetchMessages(this::processMessage);
 		}
 
-		@DisplayName("should throw invalid message id exception for other client")
-		@Test
-		void shouldThrowInvalidMessageIdExceptionForOtherClient() {
-			assertThatThrownBy(() -> readerClient.getMessage(READER_CLIENT_IDENTIFIER2.value(), messageId))
-					.isInstanceOf(InvalidMessageIDException.class);
+		private void processMessage(XtaMessage message) {
+			processedMessages.add(message);
+			processMessageDummy.accept(message);
 		}
 
 	}
@@ -168,48 +251,39 @@ class XtaClientRemoteITCase {
 	@Nested
 	class TestSendMessage {
 
-		@AfterEach
-		@SneakyThrows
-		void afterEach() {
-			closeAllMessages(readerClient, READER_CLIENT_IDENTIFIER1);
-		}
-
 		@DisplayName("should return transport report with open status")
 		@SneakyThrows
 		@ParameterizedTest
 		@ValueSource(strings = { "dfoerdermittel", "abgabe0401-kleiner-waffenschein" })
 		void shouldReturn(String messageLabel) {
-			XtaMessage xtaMessage = createXdomeaMessage(messageLabel);
+			XtaMessage xtaMessage = createXdomeaMessage(loadMessage(messageLabel).messageFile());
 
-			try {
-				var result = authorClient.sendMessage(xtaMessage);
-
-				assertThat(result.status()).isEqualTo(XtaMessageStatus.OPEN);
-			} catch (MessageSchemaViolationException exception) {
-				log.error(exception.getFaultInfo().getErrorCode().toString(), exception);
-				fail(exception.getFaultInfo().getErrorCode().toString());
-			}
+			var result = testClient.sendMessage(xtaMessage);
 
+			assertThat(result.status()).isEqualTo(XtaMessageStatus.OPEN);
 		}
+	}
 
-		@SneakyThrows
-		private static XtaMessage createXdomeaMessage(
-				String messageLabel) {
-			var creator = XdomeaXtaMessageCreatorFactory
-					.createInstance()
-					.create();
+	private XtaMessage createMessage(String messageLabel, XtaIdentifier author, XtaIdentifier reader) {
+		return XtaMessageExampleLoader.load(
+				XtaMessageExampleLoader.MessageExampleConfig.builder()
+						.messageLabel(messageLabel)
+						.reader(reader)
+						.author(author)
+						.build());
+	}
 
-			return creator.createMessage(loadMessageFile(messageLabel));
-		}
+	@SneakyThrows
+	private static XtaMessage createXdomeaMessage(XtaFile messageFile) {
+		return XDOMEA_XTA_MESSAGE_CREATOR.createMessage(messageFile);
+	}
 
-		private static XtaFile loadMessageFile(String messageLabel) {
-			var message = XtaMessageExampleLoader.load(XtaMessageExampleLoader.MessageExampleConfig.builder()
-					.messageLabel(messageLabel)
-					.author(AUTHOR_CLIENT_IDENTIFIER)
-					.reader(READER_CLIENT_IDENTIFIER1)
-					.build());
-			return message.messageFile();
-		}
+	private static XtaMessage loadMessage(String messageLabel) {
+		return XtaMessageExampleLoader.load(XtaMessageExampleLoader.MessageExampleConfig.builder()
+				.messageLabel(messageLabel)
+				.author(AUTHOR_CLIENT_IDENTIFIER)
+				.reader(READER_CLIENT_IDENTIFIER1)
+				.build());
 	}
 
 }
diff --git a/src/test/java/de/ozgcloud/xta/client/XtaClientTest.java b/src/test/java/de/ozgcloud/xta/client/XtaClientTest.java
index 76f2e929c6a21c066c41cd1545fdc9036e5f696f..d5a5d38085709d72f8b82e36e7ee71dcfb495040 100644
--- a/src/test/java/de/ozgcloud/xta/client/XtaClientTest.java
+++ b/src/test/java/de/ozgcloud/xta/client/XtaClientTest.java
@@ -5,165 +5,656 @@ import static de.ozgcloud.xta.client.factory.XtaClientConfigTestFactory.*;
 import static org.assertj.core.api.Assertions.*;
 import static org.mockito.Mockito.*;
 
+import java.math.BigInteger;
 import java.util.List;
+import java.util.Optional;
+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.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Spy;
 
 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.core.XtaExceptionHandler;
+import de.ozgcloud.xta.client.exception.XtaClientRuntimeException;
+import de.ozgcloud.xta.client.factory.ClientRuntimeExceptionTestFactory;
+import de.ozgcloud.xta.client.factory.XtaClientExceptionTestFactory;
+import de.ozgcloud.xta.client.factory.XtaMessageMetaDataListingTestFactory;
+import de.ozgcloud.xta.client.factory.XtaMessageMetaDataTestFactory;
 import de.ozgcloud.xta.client.factory.XtaMessageTestFactory;
+import de.ozgcloud.xta.client.factory.XtaTransportReportTestFactory;
 import de.ozgcloud.xta.client.model.XtaMessage;
 import de.ozgcloud.xta.client.model.XtaMessageMetaData;
 import de.ozgcloud.xta.client.model.XtaMessageMetaDataListing;
+import de.ozgcloud.xta.client.model.XtaMessageStatus;
 import de.ozgcloud.xta.client.model.XtaTransportReport;
 import lombok.SneakyThrows;
 
 class XtaClientTest {
 
 	@Mock
-	private WrappedXtaService service;
+	private XtaClientService service;
 
 	@Mock
 	private XtaClientConfig config;
 
+	@Mock
+	private XtaExceptionHandler exceptionHandler;
+
 	@Spy
 	@InjectMocks
 	private XtaClient client;
 
-	@DisplayName("get messages metadata")
+	private XtaMessageMetaDataListing listing;
+
+	private List<XtaTransportReport> transportReports;
+
+	private XtaMessage message;
+
+	@BeforeEach
+	void setup() {
+		message = XtaMessageTestFactory.create();
+		listing = XtaMessageMetaDataListingTestFactory.create();
+		transportReports = listing.messages().stream()
+				.map(messageMetaData -> XtaTransportReportTestFactory.createBuilder()
+						.metaData(messageMetaData)
+						.build())
+				.toList();
+	}
+
+	@DisplayName("fetch messages")
 	@Nested
-	class TestGetMessagesMetadata {
+	class TestFetchMessages {
 
 		@Mock
-		XtaMessageMetaDataListing xtaMessageMetaDataListing;
+		private Consumer<XtaMessage> processMessage;
 
-		@BeforeEach
+		@Mock
+		private XtaTransportReport transportReport;
+
+		@DisplayName("should return")
+		@Test
 		@SneakyThrows
-		void mock() {
-			doReturn(SELF_IDENTIFIER).when(client).deriveIdentifier(SELF_IDENTIFIER_VALUE);
-			when(service.getStatusList(SELF_IDENTIFIER, MAX_LIST_ITEMS)).thenReturn(xtaMessageMetaDataListing);
-			when(config.getMaxListItems()).thenReturn(MAX_LIST_ITEMS);
+		void shouldReturn() {
+			doReturn(List.of(transportReport)).when(client).fetchMessagesRaw(processMessage);
+
+			var result = client.fetchMessages(processMessage);
+
+			assertThat(result).containsExactly(transportReport);
 		}
 
-		@DisplayName("should call checkAccountActive")
+		@DisplayName("should throw checked exception on runtime exception")
 		@Test
+		void shouldThrowCheckedExceptionOnRuntimeException() {
+			var clientException = XtaClientExceptionTestFactory.create();
+			var runTimeException = ClientRuntimeExceptionTestFactory.create();
+			doThrow(runTimeException).when(client).fetchMessagesRaw(processMessage);
+			when(exceptionHandler.deriveXtaClientException(runTimeException)).thenReturn(clientException);
+
+			assertThatThrownBy(() -> client.fetchMessages(processMessage))
+					.isEqualTo(clientException);
+		}
+
+	}
+
+	@DisplayName("fetch messages raw")
+	@Nested
+	class TestFetchMessagesRaw {
+
+		@Mock
+		private Consumer<XtaMessage> processMessage;
+
+		@Mock
+		private FetchMessageParameter parameter;
+
+		@Mock
+		private XtaTransportReport transportReport;
+
+		@BeforeEach
+		void mock() {
+			doReturn(List.of(SELF_IDENTIFIER)).when(config).getClientIdentifiers();
+		}
+
+		@DisplayName("with active account")
+		@Nested
+		class TestWithActiveAccount {
+
+			@DisplayName("should fetch messages for client identifier")
+			@Test
+			void shouldFetchMessagesForClientIdentifier() {
+				doReturn(true).when(service).checkAccountActive(SELF_IDENTIFIER);
+				doReturn(parameter).when(client).initializeFetchMessageParameter(SELF_IDENTIFIER, processMessage);
+				doReturn(Stream.of(transportReport)).when(client).fetchMessagesForClientIdentifier(parameter);
+
+				var result = fetchMessages();
+
+				assertThat(result).containsExactly(transportReport);
+			}
+		}
+
+		@DisplayName("with inactive account")
+		@Nested
+		class TestWithInactiveAccount {
+
+			@DisplayName("should fetch no messages")
+			@Test
+			void shouldFetchNoMessages() {
+				doReturn(false).when(service).checkAccountActive(SELF_IDENTIFIER);
+
+				var result = fetchMessages();
+
+				assertThat(result).isEmpty();
+			}
+		}
+
 		@SneakyThrows
-		void shouldCallCheckAccountActive() {
-			client.getMessagesMetadata(SELF_IDENTIFIER_VALUE);
+		private List<XtaTransportReport> fetchMessages() {
+			return client.fetchMessages(processMessage);
+		}
+	}
 
-			verify(service).checkAccountActive(SELF_IDENTIFIER);
+	@DisplayName("initialize fetch message parameter")
+	@Nested
+	class TestInitializeFetchMessageParameter {
+
+		@Mock
+		private Consumer<XtaMessage> processMessage;
+
+		@DisplayName("should init empty viewed message ids")
+		@Test
+		void shouldInitEmptyViewedMessageIds() {
+			var result = initializeFetchMessageParameter();
+
+			assertThat(result.viewedMessageIds()).isEmpty();
 		}
 
-		@DisplayName("should return get status list response")
+		@DisplayName("should set client identifier")
 		@Test
-		@SneakyThrows
-		void shouldReturnGetStatusListResponse() {
-			var result = client.getMessagesMetadata(SELF_IDENTIFIER_VALUE);
+		void shouldSetClientIdentifier() {
+			var result = initializeFetchMessageParameter();
+
+			assertThat(result.clientIdentifier()).isEqualTo(SELF_IDENTIFIER);
+		}
+
+		@DisplayName("should set process message")
+		@Test
+		void shouldSetProcessMessage() {
+			var result = initializeFetchMessageParameter();
 
-			assertThat(result).isEqualTo(xtaMessageMetaDataListing);
+			assertThat(result.processMessage()).isEqualTo(processMessage);
 		}
 
+		FetchMessageParameter initializeFetchMessageParameter() {
+			return client.initializeFetchMessageParameter(SELF_IDENTIFIER, processMessage);
+		}
 	}
 
-	@DisplayName("get next messages meta data")
+	@DisplayName("fetch messages for client identifier")
 	@Nested
-	class TestGetNextMessagesMetaData {
+	class TestFetchMessagesForClientIdentifier {
 
 		@Mock
-		XtaMessageMetaDataListing xtaMessageMetaDataListing;
+		private FetchMessageParameter parameter;
 
 		@BeforeEach
-		@SneakyThrows
 		void mock() {
-			doReturn(SELF_IDENTIFIER).when(client).deriveIdentifier(SELF_IDENTIFIER_VALUE);
-			when(service.getStatusList(SELF_IDENTIFIER, MAX_LIST_ITEMS)).thenReturn(xtaMessageMetaDataListing);
-			when(config.getMaxListItems()).thenReturn(MAX_LIST_ITEMS);
+			when(parameter.clientIdentifier()).thenReturn(SELF_IDENTIFIER);
+		}
+
+		@DisplayName("with successful listing")
+		@Nested
+		class TestWithSuccessfulListing {
+
+			@Mock
+			private FetchMessageParameter nextParameter;
+
+			@BeforeEach
+			void mock() {
+				when(service.getStatusList(SELF_IDENTIFIER)).thenReturn(Optional.of(listing));
+				doReturn(transportReports).when(client).fetchMessagesForListing(listing.messages(), parameter);
+			}
+
+			@DisplayName("should return transport reports if no more messages available\"")
+			@Test
+			void shouldReturnTransportReportsIfNoMoreMessagesAvailable() {
+				doReturn(false).when(client).checkMoreMessagesAvailable(listing, transportReports);
+
+				var result = client.fetchMessagesForClientIdentifier(parameter).toList();
+
+				assertThat(result)
+						.extracting(XtaTransportReport::metaData)
+						.extracting(XtaMessageMetaData::messageId)
+						.containsExactly(MESSAGE_ID, MESSAGE_ID2, MESSAGE_ID3);
+			}
+
+			@DisplayName("should return concatenated transport reports if more messages available")
+			@Test
+			void shouldReturnConcatenatedTransportReportsIfMoreMessagesAvailable() {
+				var messageId4 = "messageId4";
+				var additionalTransportReport = XtaTransportReportTestFactory.createBuilder()
+						.metaData(XtaMessageMetaDataTestFactory.createBuilder()
+								.messageId(messageId4)
+								.build())
+						.build();
+				doReturn(true).when(client).checkMoreMessagesAvailable(listing, transportReports);
+				doReturn(nextParameter).when(parameter).withViewedMessageIdsFrom(listing.messages());
+				doReturn(Stream.of(additionalTransportReport)).when(client).fetchMessagesForClientIdentifier(nextParameter);
+
+				var result = client.fetchMessagesForClientIdentifier(parameter).toList();
+
+				assertThat(result)
+						.extracting(XtaTransportReport::metaData)
+						.extracting(XtaMessageMetaData::messageId)
+						.containsExactly(MESSAGE_ID, MESSAGE_ID2, MESSAGE_ID3, messageId4);
+			}
+		}
+
+		@DisplayName("with missing listing")
+		@Nested
+		class TestWithMissingListing {
+
+			@BeforeEach
+			void mock() {
+				when(service.getStatusList(SELF_IDENTIFIER)).thenReturn(Optional.empty());
+			}
+
+			@DisplayName("should return empty stream")
+			@Test
+			void shouldReturnEmptyStream() {
+				var result = client.fetchMessagesForClientIdentifier(parameter).toList();
+
+				assertThat(result).isEmpty();
+			}
+		}
+	}
+
+	@DisplayName("check more messages available")
+	@Nested
+	class TestCheckMoreMessagesAvailable {
+
+		@DisplayName("with no extra pending messages")
+		@Nested
+		class TestWithNoExtraPendingMessages {
+			@DisplayName("should return false")
+			@Test
+			void shouldReturnFalse() {
+				doReturn(false).when(client).checkExtraPendingMessagesAvailable(listing);
+
+				var result = client.checkMoreMessagesAvailable(listing, transportReports);
+
+				assertThat(result).isFalse();
+			}
+		}
+
+		@DisplayName("with extra pending messages but no message closed")
+		@Nested
+		class TestWithExtraPendingMessagesButNoMessageClosed {
+			@DisplayName("should return false")
+			@Test
+			void shouldReturnFalse() {
+				doReturn(true).when(client).checkExtraPendingMessagesAvailable(listing);
+				doReturn(false).when(client).checkSomeMessageHasBeenClosed(transportReports);
+
+				var result = client.checkMoreMessagesAvailable(listing, transportReports);
+
+				assertThat(result).isFalse();
+			}
+		}
+	}
+
+	@DisplayName("check extra pending messages available")
+	@Nested
+	class TestCheckExtraPendingMessagesAvailable {
+		@DisplayName("should return true if more pending than received")
+		@Test
+		void shouldReturnTrueIfMorePendingThanReceived() {
+			var result = client.checkExtraPendingMessagesAvailable(listing);
+
+			assertThat(result).isTrue();
 		}
 
-		@DisplayName("should return get status list response")
+		@DisplayName("should return false if no more pending than received")
 		@Test
-		@SneakyThrows
-		void shouldReturnGetStatusListResponse() {
-			var result = client.getNextMessagesMetadata(SELF_IDENTIFIER_VALUE);
+		void shouldReturnFalseIfNoMorePendingThanReceived() {
+			var listingWithNoExtraPendingMessages = XtaMessageMetaDataListingTestFactory.createBuilder()
+					.pendingMessageCount(BigInteger.valueOf(listing.messages().size()))
+					.build();
+
+			var result = client.checkExtraPendingMessagesAvailable(listingWithNoExtraPendingMessages);
+
+			assertThat(result).isFalse();
+		}
+	}
+
+	@DisplayName("check some message has been closed")
+	@Nested
+	class TestCheckSomeMessageHasBeenClosed {
+		@DisplayName("should return false if no message has green status")
+		@Test
+		void shouldReturnFalseIfNoMessageHasGreenStatus() {
+			var result = client.checkSomeMessageHasBeenClosed(transportReports);
+
+			assertThat(result).isFalse();
+		}
+
+		@DisplayName("should log warning if no message has been closed")
+		@Test
+		void shouldLogWarningIfNoMessageHasBeenClosed() {
+			client.checkSomeMessageHasBeenClosed(transportReports);
+
+			verify(client).logWarnForNoMessageClosed();
+		}
+
+		@DisplayName("should return true if some message has green status")
+		@Test
+		void shouldReturnTrueIfSomeMessageHasGreenStatus() {
+			var transportReportsWithGreenStatus = Stream.concat(
+					transportReports.stream(),
+					Stream.of(XtaTransportReportTestFactory.createBuilder()
+							.status(XtaMessageStatus.GREEN)
+							.build())
+			).toList();
+
+			var result = client.checkSomeMessageHasBeenClosed(transportReportsWithGreenStatus);
+
+			assertThat(result).isTrue();
+		}
+	}
+
+	@DisplayName("fetch messages for listing")
+	@Nested
+	class TestFetchMessagesForListing {
+
+		@Mock
+		private FetchMessageParameter parameter;
+
+		@Captor
+		private ArgumentCaptor<XtaMessageMetaData> messageMetaDataArgumentCaptor;
+
+		@DisplayName("with messages which should not be fetched")
+		@Nested
+		class TestWithMessagesWhichShouldNotBeFetched {
+
+			@BeforeEach
+			void mock() {
+				doReturn(false).when(client).checkMessageShouldBeFetched(any(), any());
+			}
+
+			@DisplayName("should check if message has already been viewed")
+			@Test
+			void shouldCheckIfMessageHasAlreadyBeenViewed() {
+				client.fetchMessagesForListing(listing.messages(), parameter);
+
+				verify(client, times(3)).checkMessageShouldBeFetched(messageMetaDataArgumentCaptor.capture(), eq(parameter));
+				assertThat(messageMetaDataArgumentCaptor.getAllValues())
+						.containsExactlyElementsOf(listing.messages());
+			}
+		}
 
-			assertThat(result).isEqualTo(xtaMessageMetaDataListing);
+		@DisplayName("with messages which should be fetched")
+		@Nested
+		class TestWithMessagesWhichShouldBeFetched {
+
+			@BeforeEach
+			void mock() {
+				doReturn(true).when(client).checkMessageShouldBeFetched(any(), any());
+				doAnswer(args -> {
+					XtaMessageMetaData metaData = args.getArgument(0);
+					assertThat(metaData).isNotNull();
+					return getTestTransportReportByMessageId(metaData.messageId());
+				}).when(client).fetchMessage(any(), any());
+			}
+
+			private Optional<XtaTransportReport> getTestTransportReportByMessageId(String messageId) {
+				assertThat(messageId).isNotNull();
+				return transportReports.stream().filter(d -> {
+					assertThat(d.metaData().messageId()).isNotNull();
+					return d.metaData().messageId().equals(messageId);
+				}).findFirst();
+			}
+
+			@DisplayName("should return transport reports")
+			@Test
+			void shouldReturnTransportReports() {
+				var result = client.fetchMessagesForListing(listing.messages(), parameter);
+
+				assertThat(result).containsExactlyElementsOf(transportReports);
+			}
 		}
 	}
 
-	@DisplayName("derive identifier")
+	@DisplayName("check message should be fetched")
 	@Nested
-	class TestDeriveIdentifier {
+	class TestCheckMessageShouldBeFetched {
+		@Mock
+		private FetchMessageParameter parameter;
+
+		@Mock
+		private XtaMessageMetaData messageMetaData;
 
-		@DisplayName("should use value")
+		@DisplayName("should return false if message already viewed")
 		@Test
-		void shouldUseValue() {
-			when(config.getClientIdentifiers()).thenReturn(List.of(SELF_IDENTIFIER2, SELF_IDENTIFIER));
+		void shouldReturnFalseIfMessageAlreadyViewed() {
+			when(parameter.hasMessageAlreadyBeenViewed(messageMetaData)).thenReturn(true);
 
-			var result = client.deriveIdentifier(SELF_IDENTIFIER_VALUE);
+			var result = checkMessageShouldBeFetched();
 
-			assertThat(result.value()).isEqualTo(SELF_IDENTIFIER_VALUE);
+			assertThat(result).isFalse();
 		}
 
-		@DisplayName("should throw when unknown")
+		@DisplayName("with message not viewed")
+		@Nested
+		class TestWithMessageNotViewed {
+
+			@BeforeEach
+			void mock() {
+				when(parameter.hasMessageAlreadyBeenViewed(messageMetaData)).thenReturn(false);
+			}
+
+			@DisplayName("should return false if message is not supported")
+			@Test
+			void shouldReturnFalseIfMessageIsNotSupported() {
+				doReturn(false).when(client).isMessageSupported(messageMetaData);
+
+				var result = checkMessageShouldBeFetched();
+
+				assertThat(result).isFalse();
+			}
+
+			@DisplayName("should return true if message is supported")
+			@Test
+			void shouldReturnTrueIfMessageIsSupported() {
+				doReturn(true).when(client).isMessageSupported(messageMetaData);
+
+				var result = checkMessageShouldBeFetched();
+
+				assertThat(result).isTrue();
+			}
+		}
+
+		private boolean checkMessageShouldBeFetched() {
+			return client.checkMessageShouldBeFetched(messageMetaData, parameter);
+		}
+	}
+
+	@DisplayName("is message supported")
+	@Nested
+	class TestIsMessageSupported {
+
+		@Mock
+		private Predicate<XtaMessageMetaData> isSupportedPredicate;
+
+		@Mock
+		private XtaMessageMetaData messageMetaData;
+
+		@DisplayName("should return true if predicate is null")
 		@Test
-		void shouldThrowWhenUnknown() {
-			when(config.getClientIdentifiers()).thenReturn(List.of(SELF_IDENTIFIER2));
+		void shouldReturnTrueIfPredicateIsNull() {
+			when(config.getIsMessageSupported()).thenReturn(null);
+
+			var result = client.isMessageSupported(messageMetaData);
 
-			assertThatThrownBy(() -> client.deriveIdentifier(SELF_IDENTIFIER_VALUE))
-					.isInstanceOf(IllegalArgumentException.class)
-					.hasMessage("Unknown identifier: " + SELF_IDENTIFIER_VALUE);
+			assertThat(result).isTrue();
+		}
+
+		@DisplayName("should return predicate value")
+		@ParameterizedTest
+		@ValueSource(booleans = { true, false })
+		void shouldReturnPredicateValue(boolean value) {
+			when(config.getIsMessageSupported()).thenReturn(isSupportedPredicate);
+			when(isSupportedPredicate.test(messageMetaData)).thenReturn(value);
+
+			var result = client.isMessageSupported(messageMetaData);
+
+			assertThat(result).isEqualTo(value);
 		}
 	}
 
-	@DisplayName("get message")
+	@DisplayName("fetch message")
 	@Nested
-	class TestGetMessage {
+	class TestFetchMessage {
 
 		@Mock
-		XtaMessage xtaMessage;
+		private XtaMessageMetaData messageMetaData;
 
 		@Mock
-		XtaTransportReport xtaTransportReport;
+		private FetchMessageParameter parameter;
+
+		@Mock
+		private XtaMessage message;
+
+		@Mock
+		private XtaTransportReport transportReport;
+
+		@DisplayName("should return transport report")
+		@Test
+		void shouldReturnTransportReport() {
+			when(service.getMessage(messageMetaData)).thenReturn(Optional.of(message));
+			doReturn(Optional.of(transportReport)).when(client).processMesssageAndFetchTransportReport(message, parameter);
+
+			var result = client.fetchMessage(messageMetaData, parameter);
+
+			assertThat(result).contains(transportReport);
+		}
+	}
+
+	@DisplayName("process message and fetch transport report")
+	@Nested
+	class TestProcessMessageAndFetchTransportReport {
+
+		@Mock
+		private XtaMessageMetaData messageMetaData;
+
+		@Mock
+		private FetchMessageParameter parameter;
+
+		@Mock
+		private Consumer<XtaMessage> processMessageConsumer;
+
+		@Mock
+		private XtaMessage message;
+
+		@Mock
+		private XtaTransportReport transportReport;
 
 		@BeforeEach
-		@SneakyThrows
 		void mock() {
-			doReturn(SELF_IDENTIFIER).when(client).deriveIdentifier(SELF_IDENTIFIER_VALUE);
-			when(service.getMessage(MESSAGE_ID, SELF_IDENTIFIER)).thenReturn(xtaMessage);
-			when(service.getTransportReport(MESSAGE_ID, SELF_IDENTIFIER)).thenReturn(xtaTransportReport);
+			when(message.metaData()).thenReturn(messageMetaData);
+			when(parameter.processMessage()).thenReturn(processMessageConsumer);
+			doNothing().when(client).processMessageAndCloseIfNoException(any(), any());
+			when(service.getTransportReport(messageMetaData)).thenReturn(Optional.of(transportReport));
 		}
 
-		@DisplayName("should call close")
+		@DisplayName("should call process message and close if no exception")
 		@Test
-		@SneakyThrows
-		void shouldCallClose() {
-			client.getMessage(SELF_IDENTIFIER_VALUE, MESSAGE_ID);
+		void shouldCallProcessMessageAndCloseIfNoException() {
+			client.processMesssageAndFetchTransportReport(message, parameter);
 
-			verify(service).close(MESSAGE_ID, SELF_IDENTIFIER);
+			verify(client).processMessageAndCloseIfNoException(message, processMessageConsumer);
 		}
 
-		@DisplayName("should return with message")
+		@DisplayName("should return")
 		@Test
-		@SneakyThrows
 		void shouldReturn() {
-			var result = client.getMessage(SELF_IDENTIFIER_VALUE, MESSAGE_ID);
+			var result = client.processMesssageAndFetchTransportReport(message, parameter);
 
-			assertThat(result.message()).isEqualTo(xtaMessage);
+			assertThat(result).contains(transportReport);
 		}
+	}
+
+	@DisplayName("process message and close if no exception")
+	@Nested
+	class TestProcessMessageAndCloseIfNoException {
 
-		@DisplayName("should return with transport report")
+		@Mock
+		private Consumer<XtaMessage> processMessageConsumer;
+
+		@Mock
+		private XtaMessage message;
+
+		@BeforeEach
+		void mock() {
+			when(message.metaData()).thenReturn(listing.messages().getFirst());
+		}
+
+		@DisplayName("should consume message")
 		@Test
-		@SneakyThrows
-		void shouldReturnWithTransportReport() {
-			var result = client.getMessage(SELF_IDENTIFIER_VALUE, MESSAGE_ID);
+		void shouldConsumeMessage() {
+			processMessageAndCloseIfNoException();
+
+			verify(processMessageConsumer).accept(message);
+		}
+
+		@DisplayName("should close message")
+		@Test
+		void shouldCloseMessage() {
+			processMessageAndCloseIfNoException();
+
+			verify(service).closeMessage(message);
+		}
+
+		@DisplayName("with runtime exception")
+		@Nested
+		class TestWithRuntimeException {
+
+			@Mock
+			private RuntimeException exception;
 
-			assertThat(result.transportReport()).isEqualTo(xtaTransportReport);
+			@BeforeEach
+			void mock() {
+				doThrow(exception).when(processMessageConsumer).accept(message);
+			}
+
+			@DisplayName("should log error")
+			@Test
+			void shouldLogError() {
+				processMessageAndCloseIfNoException();
+
+				verify(client).logErrorForMessageProcessingFailure(MESSAGE_ID, exception);
+			}
+
+			@DisplayName("should not close message")
+			@Test
+			void shouldNotCloseMessage() {
+				processMessageAndCloseIfNoException();
+
+				verify(service, times(0)).closeMessage(any());
+			}
+		}
+
+		private void processMessageAndCloseIfNoException() {
+			client.processMessageAndCloseIfNoException(message, processMessageConsumer);
 		}
 	}
 
@@ -172,80 +663,125 @@ class XtaClientTest {
 	class TestSendMessage {
 
 		@Mock
-		XtaMessage xtaMessageWithoutMessageId;
+		private XtaTransportReport transportReport;
 
-		@Mock
-		XtaMessageMetaData xtaMessageMetaDataWithoutMessageId;
+		@DisplayName("should return")
+		@Test
+		@SneakyThrows
+		void shouldReturn() {
+			doReturn(transportReport).when(client).sendMessageRaw(message);
 
-		@Mock
-		XtaMessage xtaMessage;
+			var result = client.sendMessage(message);
+
+			assertThat(result).isEqualTo(transportReport);
+		}
+
+		@DisplayName("should throw checked exception on runtime exception")
+		@Test
+		@SneakyThrows
+		void shouldThrowCheckedExceptionOnRuntimeException() {
+			var clientException = XtaClientExceptionTestFactory.create();
+			var runtimeException = ClientRuntimeExceptionTestFactory.create();
+			doThrow(runtimeException).when(client).sendMessageRaw(message);
+			when(exceptionHandler.deriveXtaClientException(runtimeException)).thenReturn(clientException);
+
+			assertThatThrownBy(() -> client.sendMessage(message))
+					.isEqualTo(clientException);
+		}
+	}
+
+	@DisplayName("send message raw")
+	@Nested
+	class TestSendMessageRaw {
 
 		@Mock
-		XtaTransportReport xtaTransportReport;
+		private XtaTransportReport transportReport;
 
 		@BeforeEach
-		@SneakyThrows
 		void mock() {
-			when(xtaMessageWithoutMessageId.metaData()).thenReturn(xtaMessageMetaDataWithoutMessageId);
-			when(xtaMessageMetaDataWithoutMessageId.authorIdentifier()).thenReturn(AUTHOR_IDENTIFIER);
-			when(xtaMessageMetaDataWithoutMessageId.readerIdentifier()).thenReturn(READER_IDENTIFIER);
-			when(xtaMessageMetaDataWithoutMessageId.service()).thenReturn(MESSAGE_SERVICE);
-
-			when(service.createMessageId(AUTHOR_IDENTIFIER)).thenReturn(MESSAGE_ID);
-			doReturn(xtaMessage).when(client).createXtaMessageWithMessageId(xtaMessageWithoutMessageId, MESSAGE_ID);
-			when(service.getTransportReport(MESSAGE_ID, AUTHOR_IDENTIFIER)).thenReturn(xtaTransportReport);
+			doNothing().when(client).throwExceptionIfAccountInactive(any());
+			doNothing().when(client).throwExceptionIfServiceNotAvailable(any());
+			when(service.sendMessage(message)).thenReturn(transportReport);
 		}
 
 		@DisplayName("should call checkAccountActive")
 		@Test
 		@SneakyThrows
 		void shouldCallCheckAccountActive() {
-			client.sendMessage(xtaMessageWithoutMessageId);
+			sendMessage();
 
-			verify(service).checkAccountActive(AUTHOR_IDENTIFIER);
+			verify(client).throwExceptionIfAccountInactive(AUTHOR_IDENTIFIER);
 		}
 
 		@DisplayName("should call lookup service")
 		@Test
 		@SneakyThrows
 		void shouldCallLookupService() {
-			client.sendMessage(xtaMessageWithoutMessageId);
+			sendMessage();
 
-			verify(service).lookupService(MESSAGE_SERVICE, READER_IDENTIFIER, AUTHOR_IDENTIFIER);
+			verify(client).throwExceptionIfServiceNotAvailable(message.metaData());
 		}
 
-		@DisplayName("should call send message")
+		@DisplayName("should return")
 		@Test
+		void shouldReturn() {
+			var result = sendMessage();
+
+			assertThat(result).isEqualTo(transportReport);
+		}
+
 		@SneakyThrows
-		void shouldCallSendMessage() {
-			client.sendMessage(xtaMessageWithoutMessageId);
+		private XtaTransportReport sendMessage() {
+			return client.sendMessage(message);
+		}
+	}
 
-			verify(service).sendMessage(xtaMessage);
+	@DisplayName("throw exception if service not available")
+	@Nested
+	class TestThrowExceptionIfServiceNotAvailable {
+
+		@DisplayName("should call lookupService")
+		@Test
+		void shouldCallLookupService() {
+			doReturn(true).when(service).lookupService(any());
+
+			client.throwExceptionIfServiceNotAvailable(message.metaData());
+
+			verify(service).lookupService(message.metaData());
 		}
 
-		@DisplayName("should return with transport report")
+		@DisplayName("should throw exception if service not available")
 		@Test
-		@SneakyThrows
-		void shouldReturnWithTransportReport() {
-			var result = client.sendMessage(xtaMessageWithoutMessageId);
+		void shouldThrowExceptionIfServiceNotAvailable() {
+			var metaData = message.metaData();
+			doReturn(false).when(service).lookupService(message.metaData());
 
-			assertThat(result).isEqualTo(xtaTransportReport);
+			assertThatThrownBy(() -> client.throwExceptionIfServiceNotAvailable(metaData))
+					.isInstanceOf(XtaClientRuntimeException.class);
 		}
 	}
 
-	@DisplayName("create xta message with message id")
+	@DisplayName("throw exception if account inactive")
 	@Nested
-	class TestCreateXtaMessageWithMessageId {
-		private final XtaMessage xtaMessage = XtaMessageTestFactory.create();
+	class TestThrowExceptionIfAccountInactive {
 
-		@DisplayName("should set message id")
+		@DisplayName("should call checkAccountActive")
 		@Test
-		void shouldSetMessageId() {
-			var newMessageId = "newMessageId";
+		void shouldCallCheckAccountActive() {
+			doReturn(true).when(service).checkAccountActive(any());
 
-			var result = client.createXtaMessageWithMessageId(xtaMessage, newMessageId);
+			client.throwExceptionIfAccountInactive(SELF_IDENTIFIER);
+
+			verify(service).checkAccountActive(SELF_IDENTIFIER);
+		}
+
+		@DisplayName("should throw exception if account inactive")
+		@Test
+		void shouldThrowExceptionIfAccountInactive() {
+			doReturn(false).when(service).checkAccountActive(AUTHOR_IDENTIFIER);
 
-			assertThat(result.metaData().messageId()).isEqualTo(newMessageId);
+			assertThatThrownBy(() -> client.throwExceptionIfAccountInactive(AUTHOR_IDENTIFIER))
+					.isInstanceOf(XtaClientRuntimeException.class);
 		}
 	}
 
diff --git a/src/test/java/de/ozgcloud/xta/client/XtaTestServerContainer.java b/src/test/java/de/ozgcloud/xta/client/XtaTestServerContainer.java
index da3b366c818be7b6e7add8eeca14c574af488554..5642170c9da97c68b2ebd86f9374cefe2992d012 100644
--- a/src/test/java/de/ozgcloud/xta/client/XtaTestServerContainer.java
+++ b/src/test/java/de/ozgcloud/xta/client/XtaTestServerContainer.java
@@ -21,19 +21,19 @@ public class XtaTestServerContainer extends GenericContainer<XtaTestServerContai
 	}
 
 	public String getBaseUrl() {
-		return "https://%s:%d/services/XTAService/".formatted(getHost(), getMappedPort(PORT));
+		return "https://%s:%d/MB_XTA-WS/XTA210".formatted(getHost(), getMappedPort(PORT));
 	}
 
 	public String getMsgBoxPortUrl() {
-		return getBaseUrl() + "MsgBoxPort";
+		return getBaseUrl() + "msgBoxPort.svc";
 	}
 
 	public String getManagementPortUrl() {
-		return getBaseUrl() + "ManagementPort";
+		return getBaseUrl() + "managementPort.svc";
 	}
 
 	public String getSendPortUrl() {
-		return getBaseUrl() + "SendXtaPort";
+		return getBaseUrl() + "sendPort.svc";
 	}
 
 }
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..4e0a9583cf73810be1814cd72cf68c27acf665a8 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.exception.XtaClientInitializationException;
 import de.ozgcloud.xta.client.factory.XtaClientConfigTestFactory;
-import de.ozgcloud.xta.client.exception.ClientInitializationException;
+import de.ozgcloud.xta.client.model.XtaIdentifier;
 import lombok.SneakyThrows;
 
 class XtaConfigValidatorTest {
@@ -43,15 +44,15 @@ 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))
-					.isInstanceOf(ClientInitializationException.class)
+					.isInstanceOf(XtaClientInitializationException.class)
 					.hasMessageContaining("clientIdentifiers");
 		}
 
@@ -63,7 +64,7 @@ class XtaConfigValidatorTest {
 					.build();
 
 			assertThatThrownBy(() -> validator.validate(config))
-					.isInstanceOf(ClientInitializationException.class)
+					.isInstanceOf(XtaClientInitializationException.class)
 					.hasMessageContaining("managementServiceUrl");
 		}
 
@@ -75,7 +76,7 @@ class XtaConfigValidatorTest {
 					.build();
 
 			assertThatThrownBy(() -> validator.validate(config))
-					.isInstanceOf(ClientInitializationException.class)
+					.isInstanceOf(XtaClientInitializationException.class)
 					.hasMessageContaining("sendServiceUrl");
 		}
 
@@ -87,7 +88,7 @@ class XtaConfigValidatorTest {
 					.build();
 
 			assertThatThrownBy(() -> validator.validate(config))
-					.isInstanceOf(ClientInitializationException.class)
+					.isInstanceOf(XtaClientInitializationException.class)
 					.hasMessageContaining("msgBoxServiceUrl");
 		}
 
@@ -99,7 +100,7 @@ class XtaConfigValidatorTest {
 					.build();
 
 			assertThatThrownBy(() -> validator.validate(config))
-					.isInstanceOf(ClientInitializationException.class)
+					.isInstanceOf(XtaClientInitializationException.class)
 					.hasMessageContaining("maxListItems");
 		}
 
@@ -112,7 +113,7 @@ class XtaConfigValidatorTest {
 				var config = createKeystoreWithClientCertKeyStore(o -> o.password(null));
 
 				assertThatThrownBy(() -> validator.validate(config))
-						.isInstanceOf(ClientInitializationException.class)
+						.isInstanceOf(XtaClientInitializationException.class)
 						.hasMessageContaining("password");
 			}
 
@@ -122,7 +123,7 @@ class XtaConfigValidatorTest {
 				var config = createKeystoreWithClientCertKeyStore(o -> o.type(""));
 
 				assertThatThrownBy(() -> validator.validate(config))
-						.isInstanceOf(ClientInitializationException.class)
+						.isInstanceOf(XtaClientInitializationException.class)
 						.hasMessageContaining("type");
 			}
 
@@ -132,7 +133,7 @@ class XtaConfigValidatorTest {
 				var config = createKeystoreWithClientCertKeyStore(o -> o.content(null));
 
 				assertThatThrownBy(() -> validator.validate(config))
-						.isInstanceOf(ClientInitializationException.class)
+						.isInstanceOf(XtaClientInitializationException.class)
 						.hasMessageContaining("content");
 			}
 
diff --git a/src/test/java/de/ozgcloud/xta/client/core/XtaClientServiceTest.java b/src/test/java/de/ozgcloud/xta/client/core/XtaClientServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..44f5255784cdd6308a28dc908641fdefa0790d94
--- /dev/null
+++ b/src/test/java/de/ozgcloud/xta/client/core/XtaClientServiceTest.java
@@ -0,0 +1,431 @@
+package de.ozgcloud.xta.client.core;
+
+import static de.ozgcloud.xta.client.factory.MessageMetaDataTestFactory.*;
+import static de.ozgcloud.xta.client.factory.XtaClientConfigTestFactory.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.*;
+
+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.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import de.ozgcloud.xta.client.config.XtaClientConfig;
+import de.ozgcloud.xta.client.exception.XtaClientRuntimeException;
+import de.ozgcloud.xta.client.factory.XtaMessageMetaDataListingTestFactory;
+import de.ozgcloud.xta.client.factory.XtaMessageMetaDataTestFactory;
+import de.ozgcloud.xta.client.factory.XtaMessageTestFactory;
+import de.ozgcloud.xta.client.factory.XtaTransportReportTestFactory;
+import de.ozgcloud.xta.client.model.XtaMessage;
+import de.ozgcloud.xta.client.model.XtaMessageMetaData;
+import de.ozgcloud.xta.client.model.XtaMessageMetaDataListing;
+import de.ozgcloud.xta.client.model.XtaTransportReport;
+import genv3.de.xoev.transport.xta.x211.PermissionDeniedException;
+import genv3.de.xoev.transport.xta.x211.XTAWSTechnicalProblemException;
+import lombok.SneakyThrows;
+
+class XtaClientServiceTest {
+
+	@Mock
+	private WrappedXtaService wrapper;
+
+	@Mock
+	private XtaClientConfig config;
+
+	@Spy
+	@InjectMocks
+	private XtaClientService service;
+
+	private final XtaMessageMetaData messageMetaData = XtaMessageMetaDataTestFactory.create();
+	private final XtaMessage message = XtaMessageTestFactory.create();
+	private final XtaTransportReport transportReport = XtaTransportReportTestFactory.create();
+
+	@DisplayName("get transport report")
+	@Nested
+	class TestGetTransportReport {
+
+		@Mock
+		private XtaClientRuntimeException exception;
+
+		@DisplayName("should return")
+		@Test
+		void shouldReturn() {
+			doReturn(transportReport).when(service).getTransportReportOrThrowException(messageMetaData);
+
+			var result = service.getTransportReport(messageMetaData);
+
+			assertThat(result).contains(transportReport);
+		}
+
+		@DisplayName("should return empty if exception")
+		@Test
+		void shouldReturnEmptyIfException() {
+			doThrow(exception).when(service).getTransportReportOrThrowException(messageMetaData);
+
+			var result = service.getTransportReport(messageMetaData);
+
+			assertThat(result).isEmpty();
+		}
+
+		@DisplayName("should log error if exception")
+		@Test
+		void shouldLogErrorIfException() {
+			doThrow(exception).when(service).getTransportReportOrThrowException(messageMetaData);
+
+			service.getTransportReport(messageMetaData);
+
+			verify(service).logError(anyString(), eq(exception));
+		}
+	}
+
+	@DisplayName("close message")
+	@Nested
+	class TestCloseMessage {
+		@Mock
+		private XTAWSTechnicalProblemException xtaWSTechnicalProblemException;
+
+		@DisplayName("should call close")
+		@Test
+		@SneakyThrows
+		void shouldCallClose() {
+			service.closeMessage(message);
+
+			verify(wrapper).close(MESSAGE_ID, READER_IDENTIFIER);
+		}
+
+		@DisplayName("should log error if exception")
+		@Test
+		@SneakyThrows
+		void shouldLogErrorIfException() {
+			doThrow(xtaWSTechnicalProblemException).when(wrapper).close(MESSAGE_ID, READER_IDENTIFIER);
+
+			service.closeMessage(message);
+
+			verify(service).logError(anyString(), eq(xtaWSTechnicalProblemException));
+		}
+
+	}
+
+	@DisplayName("get message")
+	@Nested
+	class TestGetMessage {
+
+		@Mock
+		private XTAWSTechnicalProblemException xtaWSTechnicalProblemException;
+
+		@DisplayName("should return")
+		@Test
+		@SneakyThrows
+		void shouldReturn() {
+			doReturn(message).when(wrapper).getMessage(MESSAGE_ID, READER_IDENTIFIER);
+
+			var result = service.getMessage(messageMetaData);
+
+			assertThat(result).contains(message);
+		}
+
+		@DisplayName("should return empty if exception")
+		@Test
+		@SneakyThrows
+		void shouldReturnEmptyIfException() {
+			doThrow(xtaWSTechnicalProblemException).when(wrapper).getMessage(MESSAGE_ID, READER_IDENTIFIER);
+
+			var result = service.getMessage(messageMetaData);
+
+			assertThat(result).isEmpty();
+		}
+
+		@DisplayName("should log error if exception")
+		@Test
+		@SneakyThrows
+		void shouldLogErrorIfException() {
+			doThrow(xtaWSTechnicalProblemException).when(wrapper).getMessage(MESSAGE_ID, READER_IDENTIFIER);
+
+			service.getMessage(messageMetaData);
+
+			verify(service).logError(anyString(), eq(xtaWSTechnicalProblemException));
+		}
+	}
+
+	@DisplayName("get status list")
+	@Nested
+	class TestGetStatusList {
+
+		@Mock
+		private XTAWSTechnicalProblemException xtaWSTechnicalProblemException;
+
+		private final XtaMessageMetaDataListing listing = XtaMessageMetaDataListingTestFactory.create();
+
+		@BeforeEach
+		void mock() {
+			when(config.getMaxListItems()).thenReturn(MAX_LIST_ITEMS);
+		}
+
+		@DisplayName("should return")
+		@Test
+		@SneakyThrows
+		void shouldReturn() {
+			doReturn(listing).when(wrapper).getStatusList(SELF_IDENTIFIER, MAX_LIST_ITEMS);
+
+			var result = service.getStatusList(SELF_IDENTIFIER);
+
+			assertThat(result).contains(listing);
+		}
+
+		@DisplayName("should return empty if exception")
+		@Test
+		@SneakyThrows
+		void shouldReturnEmptyIfException() {
+			doThrow(xtaWSTechnicalProblemException).when(wrapper).getStatusList(SELF_IDENTIFIER, MAX_LIST_ITEMS);
+
+			var result = service.getStatusList(SELF_IDENTIFIER);
+
+			assertThat(result).isEmpty();
+		}
+
+		@DisplayName("should log error if exception")
+		@Test
+		@SneakyThrows
+		void shouldLogErrorIfException() {
+			doThrow(xtaWSTechnicalProblemException).when(wrapper).getStatusList(SELF_IDENTIFIER, MAX_LIST_ITEMS);
+
+			service.getStatusList(SELF_IDENTIFIER);
+
+			verify(service).logError(anyString(), eq(xtaWSTechnicalProblemException));
+		}
+	}
+
+	@DisplayName("check account active")
+	@Nested
+	class TestCheckAccountActive {
+
+		@DisplayName("should call check account active")
+		@Test
+		@SneakyThrows
+		void shouldCallCheckAccountActive() {
+			checkAccountActive();
+
+			verify(wrapper).checkAccountActive(SELF_IDENTIFIER);
+		}
+
+		@DisplayName("should return true if no exception")
+		@Test
+		void shouldReturnTrueIfNoException() {
+			var result = checkAccountActive();
+
+			assertThat(result).isTrue();
+		}
+
+		@DisplayName("should return false if permission denied exception")
+		@Test
+		@SneakyThrows
+		void shouldReturnFalseIfPermissionDeniedException() {
+			doThrow(new PermissionDeniedException()).when(wrapper).checkAccountActive(any());
+
+			var result = checkAccountActive();
+
+			assertThat(result).isFalse();
+		}
+
+		@DisplayName("should throw on technical problem exception")
+		@Test
+		@SneakyThrows
+		void shouldThrowOnTechnicalProblemException() {
+			var technicalException = new XTAWSTechnicalProblemException();
+			doThrow(technicalException).when(wrapper).checkAccountActive(any());
+
+			assertThatThrownBy(this::checkAccountActive)
+					.isInstanceOf(XtaClientRuntimeException.class)
+					.hasCause(technicalException);
+		}
+
+		private boolean checkAccountActive() {
+			return service.checkAccountActive(SELF_IDENTIFIER);
+		}
+	}
+
+	@DisplayName("send message")
+	@Nested
+	class TestSendMessage {
+
+		@Mock
+		private XtaMessage messageWithoutMessageId;
+
+		@Mock
+		private XTAWSTechnicalProblemException exception;
+
+		@BeforeEach
+		@SneakyThrows
+		void mock() {
+			doReturn(message).when(service).getXtaMessageWithMessageId(messageWithoutMessageId);
+		}
+
+		@DisplayName("should call send message")
+		@Test
+		@SneakyThrows
+		void shouldCallSendMessage() {
+			sendMessage();
+
+			verify(wrapper).sendMessage(message);
+		}
+
+		@DisplayName("should return with transport report")
+		@Test
+		@SneakyThrows
+		void shouldReturnWithTransportReport() {
+			doReturn(transportReport).when(service).getTransportReportOrThrowException(message.metaData());
+
+			var result = sendMessage();
+
+			assertThat(result).isEqualTo(transportReport);
+		}
+
+		@DisplayName("should throw client runtime exception if send message fails")
+		@Test
+		@SneakyThrows
+		void shouldThrowClientRuntimeExceptionIfSendMessageFails() {
+			doThrow(exception).when(wrapper).sendMessage(message);
+
+			assertThatThrownBy(this::sendMessage)
+					.isInstanceOf(XtaClientRuntimeException.class)
+					.hasCause(exception);
+		}
+
+		private XtaTransportReport sendMessage() {
+			return service.sendMessage(messageWithoutMessageId);
+		}
+	}
+
+	@DisplayName("get xta message with message id")
+	@Nested
+	class TestGetXtaMessageWithMessageId {
+
+		@Mock
+		private XtaMessage messageWithMessageId;
+
+		@Mock
+		private XTAWSTechnicalProblemException exception;
+
+		@DisplayName("should return message")
+		@Test
+		@SneakyThrows
+		void shouldReturnMessage() {
+			doReturn(MESSAGE_ID2).when(wrapper).createMessageId(AUTHOR_IDENTIFIER);
+			doReturn(messageWithMessageId).when(service).createXtaMessageWithMessageId(message, MESSAGE_ID2);
+
+			var result = service.getXtaMessageWithMessageId(message);
+
+			assertThat(result).isEqualTo(messageWithMessageId);
+		}
+
+		@DisplayName("should throw client runtime exception if create message fails")
+		@Test
+		@SneakyThrows
+		void shouldThrowClientRuntimeExceptionIfCreateMessageFails() {
+			doThrow(exception).when(wrapper).createMessageId(AUTHOR_IDENTIFIER);
+
+			assertThatThrownBy(() -> service.getXtaMessageWithMessageId(message))
+					.isInstanceOf(XtaClientRuntimeException.class)
+					.hasCause(exception);
+		}
+	}
+
+	@DisplayName("get transport report or throw exception")
+	@Nested
+	class TestGetTransportReportOrThrowException {
+		@Mock
+		private XTAWSTechnicalProblemException exception;
+
+		@DisplayName("should return")
+		@Test
+		@SneakyThrows
+		void shouldReturn() {
+			when(wrapper.getTransportReport(MESSAGE_ID, AUTHOR_IDENTIFIER)).thenReturn(transportReport);
+
+			var result = service.getTransportReportOrThrowException(messageMetaData);
+
+			assertThat(result).isEqualTo(transportReport);
+		}
+
+		@DisplayName("should throw client exception if exception")
+		@Test
+		@SneakyThrows
+		void shouldThrowClientExceptionIfException() {
+			when(wrapper.getTransportReport(MESSAGE_ID, AUTHOR_IDENTIFIER)).thenThrow(exception);
+
+			assertThatThrownBy(() -> service.getTransportReportOrThrowException(messageMetaData))
+					.isInstanceOf(XtaClientRuntimeException.class)
+					.hasCause(exception);
+		}
+	}
+
+	@DisplayName("create xta message with message id")
+	@Nested
+	class TestCreateXtaMessageWithMessageId {
+		private final XtaMessage xtaMessage = XtaMessageTestFactory.create();
+
+		@DisplayName("should set message id")
+		@Test
+		void shouldSetMessageId() {
+			var newMessageId = "newMessageId";
+
+			var result = service.createXtaMessageWithMessageId(xtaMessage, newMessageId);
+
+			assertThat(result.metaData().messageId()).isEqualTo(newMessageId);
+		}
+	}
+
+	@DisplayName("lookup service")
+	@Nested
+	class TestLookupService {
+		@Mock
+		private XTAWSTechnicalProblemException exception;
+
+		@DisplayName("should return lookup result")
+		@ParameterizedTest
+		@ValueSource(booleans = { true, false })
+		@SneakyThrows
+		void shouldReturnLookupResult(boolean lookupResult) {
+			when(wrapper.lookupService(MESSAGE_SERVICE, READER_IDENTIFIER, AUTHOR_IDENTIFIER)).thenReturn(lookupResult);
+
+			var result = lookupService();
+
+			assertThat(result).isEqualTo(lookupResult);
+		}
+
+		@DisplayName("should return false if exception")
+		@Test
+		@SneakyThrows
+		void shouldReturnFalseIfException() {
+			when(wrapper.lookupService(MESSAGE_SERVICE, READER_IDENTIFIER, AUTHOR_IDENTIFIER)).thenThrow(exception);
+
+			var result = lookupService();
+
+			assertThat(result).isFalse();
+		}
+
+		@DisplayName("should log warning if exception")
+		@Test
+		@SneakyThrows
+		void shouldLogWarningIfException() {
+			when(wrapper.lookupService(MESSAGE_SERVICE, READER_IDENTIFIER, AUTHOR_IDENTIFIER)).thenThrow(exception);
+
+			lookupService();
+
+			verify(service).logWarning(anyString(), eq(exception));
+		}
+
+		private boolean lookupService() {
+			return service.lookupService(messageMetaData);
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/src/test/java/de/ozgcloud/xta/client/core/XtaExceptionHandlerFactoryTest.java b/src/test/java/de/ozgcloud/xta/client/core/XtaExceptionHandlerFactoryTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..985c7aa712c5a886c8716e1db445771f83314d8a
--- /dev/null
+++ b/src/test/java/de/ozgcloud/xta/client/core/XtaExceptionHandlerFactoryTest.java
@@ -0,0 +1,29 @@
+package de.ozgcloud.xta.client.core;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Spy;
+
+class XtaExceptionHandlerFactoryTest {
+
+	@InjectMocks
+	@Spy
+	private XtaExceptionHandlerFactory exceptionHandlerFactory;
+
+	@DisplayName("create")
+	@Nested
+	class TestCreate {
+		@DisplayName("should return")
+		@Test
+		void shouldReturn() {
+			var result = exceptionHandlerFactory.create();
+
+			assertThat(result).isNotNull();
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/src/test/java/de/ozgcloud/xta/client/core/XtaExceptionHandlerTest.java b/src/test/java/de/ozgcloud/xta/client/core/XtaExceptionHandlerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..be02999134d1f2003672d7e256a04fe35be9af8b
--- /dev/null
+++ b/src/test/java/de/ozgcloud/xta/client/core/XtaExceptionHandlerTest.java
@@ -0,0 +1,176 @@
+package de.ozgcloud.xta.client.core;
+
+import static de.ozgcloud.xta.client.core.XtaExceptionHandler.*;
+import static de.ozgcloud.xta.client.factory.ClientRuntimeExceptionTestFactory.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+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.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import de.ozgcloud.xta.client.exception.XtaClientRuntimeException;
+import de.ozgcloud.xta.client.exception.XtaClientException;
+import de.ozgcloud.xta.client.factory.ClientRuntimeExceptionTestFactory;
+import genv3.de.xoev.transport.xta.x211.PermissionDeniedException;
+
+class XtaExceptionHandlerTest {
+
+	@InjectMocks
+	@Spy
+	XtaExceptionHandler exceptionHandler;
+
+	private XtaClientRuntimeException exception;
+
+	@BeforeEach
+	void setUp() {
+		exception = ClientRuntimeExceptionTestFactory.create();
+	}
+
+	@DisplayName("derive xta client exception")
+	@Nested
+	class TestDeriveXtaClientException {
+
+		@Mock
+		private XtaClientException derivedClientException;
+
+		@DisplayName("should derive xta client exception from xta client runtime exception")
+		@Test
+		void shouldDeriveXtaClientExceptionFromXtaClientRuntimeException() {
+			doReturn(derivedClientException).when(exceptionHandler).deriveXtaClientExceptionFromClientRuntimeException(exception);
+
+			var result = exceptionHandler.deriveXtaClientException(exception);
+
+			assertThat(result).isEqualTo(derivedClientException);
+		}
+
+		@DisplayName("without xta client runtime exception instance")
+		@Nested
+		class TestWithoutXtaClientRuntimeExceptionInstance {
+
+			@Mock
+			private RuntimeException runtimeException;
+
+			@DisplayName("should return with unknown error message")
+			@Test
+			void shouldReturnWithUnknownErrorMessage() {
+				var result = exceptionHandler.deriveXtaClientException(runtimeException);
+
+				assertThat(result.getMessage()).isEqualTo(UNEXPECTED_ERROR_MESSAGE);
+			}
+
+			@DisplayName("should have cause")
+			@Test
+			void shouldHaveCause() {
+				var result = exceptionHandler.deriveXtaClientException(runtimeException);
+
+				assertThat(result.getCause()).isEqualTo(runtimeException);
+			}
+		}
+	}
+
+	@DisplayName("derive xta client exception from xta client runtime exception")
+	@Nested
+	class TestDeriveXtaClientExceptionFromXtaClientRuntimeException {
+
+		@DisplayName("should keep message if no cause")
+		@Test
+		void shouldKeepMessageIfNoCause() {
+			var exceptionWithoutCause = new XtaClientRuntimeException(MESSAGE);
+
+			var result = exceptionHandler.deriveXtaClientExceptionFromClientRuntimeException(exceptionWithoutCause);
+
+			assertThat(result.getMessage()).isEqualTo(MESSAGE);
+		}
+
+		@DisplayName("should keep message if cause is not an xta exception")
+		@Test
+		void shouldKeepMessageIfCauseIsNotAnXtaException() {
+			var cause = new Exception();
+			var exceptionWithNoXtaCause = new XtaClientRuntimeException(MESSAGE, cause);
+
+			var result = exceptionHandler.deriveXtaClientExceptionFromClientRuntimeException(exceptionWithNoXtaCause);
+
+			assertThat(result.getMessage()).isEqualTo(MESSAGE);
+
+		}
+
+		@DisplayName("should extend detail message if cause is an xta exception")
+		@Test
+		void shouldExtendDetailMessageIfCauseIsAnXtaException() {
+			var result = exceptionHandler.deriveXtaClientExceptionFromClientRuntimeException(exception);
+
+			assertThat(result.getMessage()).contains(MESSAGE, CAUSE_MESSAGE, CAUSE_CODE, CAUSE_NAME);
+		}
+	}
+
+	@DisplayName("get detail lines")
+	@Nested
+	class TestGetDetailLines {
+
+		@DisplayName("should return empty stream if cause is null")
+		@Test
+		void shouldReturnEmptyStreamIfCauseIsNull() {
+			var result = exceptionHandler.getDetailLines(null).toList();
+
+			assertThat(result).isEmpty();
+		}
+
+		@DisplayName("should return empty stream if cause is not an exception")
+		@Test
+		void shouldReturnEmptyStreamIfCauseIsNotAnException() {
+			var result = exceptionHandler.getDetailLines(new Throwable()).toList();
+
+			assertThat(result).isEmpty();
+		}
+
+		@DisplayName("should return empty stream if cause is not an xta exception")
+		@Test
+		void shouldReturnEmptyStreamIfCauseIsNotAnXtaException() {
+			var result = exceptionHandler.getDetailLines(new Exception()).toList();
+
+			assertThat(result).isEmpty();
+		}
+
+		@DisplayName("should return detail lines if cause is an xta exception")
+		@Test
+		void shouldReturnDetailLinesIfCauseIsAnXtaException() {
+			var causeException = (PermissionDeniedException) exception.getCause();
+			doReturn(Stream.of(CAUSE_MESSAGE)).when(exceptionHandler).deriveDetailLinesFromException(causeException);
+
+			var result = exceptionHandler.getDetailLines(causeException).toList();
+
+			assertThat(result).containsExactly(CAUSE_MESSAGE);
+		}
+	}
+
+	@DisplayName("derive detail lines from exception")
+	@Nested
+	class TestDeriveDetailLinesFromException {
+
+		@DisplayName("should return empty stream if exception type is not present")
+		@Test
+		void shouldReturnEmptyStreamIfExceptionTypeIsNotPresent() {
+			var result = exceptionHandler.deriveDetailLinesFromException(new Exception()).toList();
+
+			assertThat(result).isEmpty();
+		}
+
+		@DisplayName("should return stream with error code and name if exception type is present")
+		@Test
+		void shouldReturnStreamWithErrorCodeAndNameIfExceptionTypeIsPresent() {
+			var causeException = (PermissionDeniedException) exception.getCause();
+
+			var result = exceptionHandler.deriveDetailLinesFromException(causeException).toList();
+
+			assertThat(result).containsExactly(CAUSE_MESSAGE, "[%s] %s".formatted(CAUSE_CODE, CAUSE_NAME));
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/src/test/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactoryTest.java b/src/test/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactoryTest.java
index f9077cf24e4ac1ce0218d3784a1f501b349f2b32..8047c2dddd27d5918427a4c5f687b8f8da4bcd7e 100644
--- a/src/test/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactoryTest.java
+++ b/src/test/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactoryTest.java
@@ -22,7 +22,7 @@ import org.mockito.Spy;
 
 import de.ozgcloud.xta.client.factory.XtaClientConfigTestFactory;
 import de.ozgcloud.xta.client.config.XtaClientConfig;
-import de.ozgcloud.xta.client.exception.ClientInitializationException;
+import de.ozgcloud.xta.client.exception.XtaClientInitializationException;
 import lombok.SneakyThrows;
 
 class XtaTLSClientParametersFactoryTest {
@@ -123,7 +123,7 @@ class XtaTLSClientParametersFactoryTest {
 				doThrow(new KeyStoreException("something")).when(sslContextBuilder).loadKeyMaterial(any(), any());
 
 				assertThatThrownBy(() -> factory.createXtaSslContext())
-						.isInstanceOf(ClientInitializationException.class);
+						.isInstanceOf(XtaClientInitializationException.class);
 			}
 		}
 
diff --git a/src/test/java/de/ozgcloud/xta/client/extension/StaticStringListAppender.java b/src/test/java/de/ozgcloud/xta/client/extension/StaticStringListAppender.java
new file mode 100644
index 0000000000000000000000000000000000000000..5ea33622fcb4131b0e9722df63cc13b9346c0056
--- /dev/null
+++ b/src/test/java/de/ozgcloud/xta/client/extension/StaticStringListAppender.java
@@ -0,0 +1,46 @@
+package de.ozgcloud.xta.client.extension;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.apache.logging.log4j.core.Filter;
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Node;
+import org.apache.logging.log4j.core.config.Property;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+@Plugin(name = "StaticStringList", category = Node.CATEGORY)
+public class StaticStringListAppender extends AbstractAppender {
+
+	private static final ConcurrentLinkedQueue<String> LOG_LINES = new ConcurrentLinkedQueue<>();
+
+	protected StaticStringListAppender(String name, Filter filter,
+			Layout<? extends Serializable> layout, boolean ignoreExceptions,
+			Property[] properties) {
+		super(name, filter, layout, ignoreExceptions, properties);
+	}
+
+	@PluginFactory
+	public static StaticStringListAppender createAppender(@PluginAttribute("name") String name) {
+		return new StaticStringListAppender(name, null, null, true, Property.EMPTY_ARRAY);
+	}
+
+	public static List<String> getLogLines() {
+		return new ArrayList<>(LOG_LINES);
+	}
+
+	public static void clearLogLines() {
+		LOG_LINES.clear();
+	}
+
+	@Override
+	public void append(LogEvent event) {
+		LOG_LINES.add(event.getMessage().getFormattedMessage());
+	}
+}
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/XtaRemoteServerSetupExtension.java b/src/test/java/de/ozgcloud/xta/client/extension/XtaRemoteServerSetupExtension.java
index d59023bc848ab56920d9a2eed43d36f6a3b3d893..cf59130cf1751c7160074766f8741f3055d4c8a7 100644
--- a/src/test/java/de/ozgcloud/xta/client/extension/XtaRemoteServerSetupExtension.java
+++ b/src/test/java/de/ozgcloud/xta/client/extension/XtaRemoteServerSetupExtension.java
@@ -6,7 +6,6 @@ import java.io.File;
 import java.util.Objects;
 
 import org.junit.jupiter.api.extension.BeforeAllCallback;
-import org.junit.jupiter.api.extension.BeforeEachCallback;
 import org.junit.jupiter.api.extension.ExtensionContext;
 
 import com.google.common.io.Files;
@@ -15,8 +14,6 @@ import de.ozgcloud.xta.client.XtaClient;
 import de.ozgcloud.xta.client.XtaClientFactory;
 import de.ozgcloud.xta.client.config.XtaClientConfig;
 import de.ozgcloud.xta.client.core.WrappedXtaService;
-import de.ozgcloud.xta.client.model.XtaMessage;
-
 import lombok.Getter;
 import lombok.Setter;
 import lombok.SneakyThrows;
@@ -25,12 +22,18 @@ import lombok.extern.slf4j.Slf4j;
 @Getter
 @Setter
 @Slf4j
-public class XtaRemoteServerSetupExtension implements BeforeAllCallback, BeforeEachCallback {
+public class XtaRemoteServerSetupExtension implements BeforeAllCallback {
+
+	private XtaClient testClient;
+	private XtaClientConfig testClientConfig;
+	private XtaClient silentTestClient;
+	private XtaClientConfig silentTestClientConfig;
+	private XtaClient devClient;
+	private XtaClientConfig devClientConfig;
+	private XtaClient silentDevClient;
+	private XtaClientConfig silentDevClientConfig;
 
-	private XtaClient authorClient;
-	private XtaClient readerClient;
 	private WrappedXtaService service;
-	private XtaClientConfig config;
 	private XtaClientFactory clientFactory;
 
 	static final String BASE_URL = "https://li33-0005.dp.dsecurecloud.de/MB_XTA-WS/XTA210";
@@ -38,54 +41,57 @@ public class XtaRemoteServerSetupExtension implements BeforeAllCallback, BeforeE
 	@Override
 	@SneakyThrows
 	public void beforeAll(ExtensionContext context) {
-		authorClient = setupClient(getEnvVar("KOP_SH_KIEL_TEST_PATH"), getEnvVar("KOP_SH_KIEL_TEST_PASSWORD"));
-		readerClient = setupClient(getEnvVar("KOP_SH_KIEL_DEV_PATH"), getEnvVar("KOP_SH_KIEL_DEV_PASSWORD"));
+		testClientConfig = createClientConfig(
+				createClientCertKeyStore(getEnvVar("KOP_SH_KIEL_TEST_PATH"), getEnvVar("KOP_SH_KIEL_TEST_PASSWORD")),
+				true);
+		testClient = XtaClient.from(testClientConfig);
+		silentTestClientConfig = createClientConfig(
+				testClientConfig.getClientCertKeystore(),
+				false);
+		silentTestClient = XtaClient.from(silentTestClientConfig);
+
+		devClientConfig = createClientConfig(
+				createClientCertKeyStore(getEnvVar("KOP_SH_KIEL_DEV_PATH"), getEnvVar("KOP_SH_KIEL_DEV_PASSWORD")),
+				true);
+		silentDevClientConfig = createClientConfig(
+				devClientConfig.getClientCertKeystore(),
+				false);
+		devClient = XtaClient.from(devClientConfig);
+		silentDevClient = XtaClient.from(silentDevClientConfig);
 	}
 
-	@SneakyThrows
-	XtaClient setupClient(String clientCertKeystorePath, String clientCertKeystorePassword) {
-		var clientCertKeyStore = XtaClientConfig.KeyStore.builder()
+	private XtaClientConfig.KeyStore createClientCertKeyStore(String clientCertKeystorePath, String clientCertKeystorePassword) {
+		return XtaClientConfig.KeyStore.builder()
 				.content(readBytesFromFile(clientCertKeystorePath))
 				.type("PKCS12")
 				.password(clientCertKeystorePassword.toCharArray())
 				.build();
-		config = createClientConfigBuilder()
-				.managementServiceUrl(BASE_URL + "managementPort.svc")
-				.sendServiceUrl(BASE_URL + "sendPort.svc")
-				.msgBoxServiceUrl(BASE_URL + "msgBoxPort.svc")
+	}
+
+	@SneakyThrows
+	XtaClientConfig createClientConfig(XtaClientConfig.KeyStore clientCertKeyStore, boolean verbose) {
+		return createSpecificClientConfigBuilder()
+				.logSoapRequests(verbose)
+				.logSoapResponses(verbose)
 				.clientCertKeystore(clientCertKeyStore)
 				.build();
-		clientFactory = XtaClientFactory.from(config);
-		return clientFactory.create();
 	}
 
 	private String getEnvVar(String name) {
 		return Objects.requireNonNull(System.getenv(name), "Environment variable " + name + " is required!");
 	}
 
-	@Override
 	@SneakyThrows
-	public void beforeEach(ExtensionContext context) {
-		XtaServerSetupExtensionTestUtil.closeAllMessages(readerClient, READER_CLIENT_IDENTIFIER1);
-	}
-
-	@SneakyThrows
-	public String sendTestMessage() {
-		return XtaServerSetupExtensionTestUtil.sendTestMessage(authorClient, XtaMessageExampleLoader.MessageExampleConfig.builder()
-				.messageLabel("dfoerdermittel")
-				.reader(READER_CLIENT_IDENTIFIER1)
-				.author(AUTHOR_CLIENT_IDENTIFIER)
-				.build());
-	}
-
-	@SneakyThrows
-	public String sendTestMessage(XtaMessage message) {
-		return XtaServerSetupExtensionTestUtil.sendTestMessage(authorClient, message);
+	private static byte[] readBytesFromFile(String path) {
+		return Files.toByteArray(new File(path));
 	}
 
 	@SneakyThrows
-	private static byte[] readBytesFromFile(String path) {
-		return Files.toByteArray(new File(path));
+	public XtaClientConfig.XtaClientConfigBuilder createSpecificClientConfigBuilder() {
+		return createClientConfigBuilder()
+				.managementServiceUrl(BASE_URL + "managementPort.svc")
+				.sendServiceUrl(BASE_URL + "sendPort.svc")
+				.msgBoxServiceUrl(BASE_URL + "msgBoxPort.svc");
 	}
 
 }
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 45888f62bca53e16bcd8d6c2f29ab48c405d6144..0432d3445846171a7908c47377e0d1b32baa48be 100644
--- a/src/test/java/de/ozgcloud/xta/client/extension/XtaServerSetupExtensionTestUtil.java
+++ b/src/test/java/de/ozgcloud/xta/client/extension/XtaServerSetupExtensionTestUtil.java
@@ -1,13 +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.Set;
+import java.util.function.Function;
+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.WrappedXtaServiceFactory;
+import de.ozgcloud.xta.client.exception.XtaClientRuntimeException;
+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;
@@ -19,52 +32,67 @@ import lombok.extern.log4j.Log4j2;
 @Log4j2
 public class XtaServerSetupExtensionTestUtil {
 
-	public static final XtaIdentifier AUTHOR_CLIENT_IDENTIFIER = XtaIdentifier.builder()
+	public static final XtaIdentifier DEV_READER_CLIENT_IDENTIFIER = XtaIdentifier.builder()
+			.value("gae:dev-environment@ozg-cloud.de")
+			.category("Generischer Antragsempfänger")
+			.name("OZG-Cloud Leser Dev")
+			.build();
+	public static final XtaIdentifier TEST_READER_CLIENT_IDENTIFIER = XtaIdentifier.builder()
+			.value("gae:test-environment@ozg-cloud.de")
+			.category("Generischer Antragsempfänger")
+			.name("OZG-Cloud Leser Test")
+			.build();
+
+	public static final XtaIdentifier TEST_AUTHOR_CLIENT_IDENTIFIER = XtaIdentifier.builder()
 			.value("gad:010103000000")
 			.category("DMS Schleswig-Holstein")
 			.name("Generischer Antragsdienst")
 			.build();
-	public static final XtaIdentifier READER_CLIENT_IDENTIFIER2 = XtaIdentifier.builder()
-			.value("gae:test-environment@ozg-cloud.de")
-			.category("Generischer Antragsempfänger")
-			.name("OZG-Cloud Test")
+
+	public static final XtaIdentifier AUTHOR_CLIENT_IDENTIFIER = TEST_AUTHOR_CLIENT_IDENTIFIER;
+	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 READER_CLIENT_IDENTIFIER1 = XtaIdentifier.builder()
-			.value("gae:dev-environment@ozg-cloud.de")
-			.category("Generischer Antragsempfänger")
-			.name("OZG-Cloud Dev")
+	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_IDENTIFIER1 = DEV_READER_CLIENT_IDENTIFIER;
+	public static final XtaIdentifier READER_CLIENT_IDENTIFIER2 = TEST_READER_CLIENT_IDENTIFIER;
+	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);
 	}
 
-	@SneakyThrows
-	public static String sendTestMessage(XtaClient client, XtaMessageExampleLoader.MessageExampleConfig messageExampleConfig) {
-		var message = XtaMessageExampleLoader.load(messageExampleConfig);
-		return sendTestMessage(client, message);
-	}
-
 	@SneakyThrows
 	public static String sendTestMessage(XtaClient client, XtaMessage message) {
 		try {
 			log.info("Sending from author {} to reader {}.", message.metaData().authorIdentifier(), message.metaData().readerIdentifier());
 			var transportReport = client.sendMessage(message);
 			return transportReport.metaData().messageId();
-		} catch (ParameterIsNotValidException e) {
-			logCodeFehlerNummer(e.getFaultInfo().getErrorCode());
-			throw e;
-		} catch (PermissionDeniedException e) {
-			logCodeFehlerNummer(e.getFaultInfo().getErrorCode());
-			throw e;
-		} catch (MessageSchemaViolationException e) {
-			logCodeFehlerNummer(e.getFaultInfo().getErrorCode());
-			throw e;
-		} catch (XTAWSTechnicalProblemException e) {
-			logCodeFehlerNummer(e.getFaultInfo().getErrorCode());
+		} catch (XtaClientRuntimeException e) {
+			var cause = e.getCause();
+			if (cause instanceof ParameterIsNotValidException) {
+				logCodeFehlerNummer(((ParameterIsNotValidException) cause).getFaultInfo().getErrorCode());
+			} else if (cause instanceof PermissionDeniedException) {
+				logCodeFehlerNummer(((PermissionDeniedException) cause).getFaultInfo().getErrorCode());
+			} else if (cause instanceof MessageSchemaViolationException) {
+				logCodeFehlerNummer(((MessageSchemaViolationException) cause).getFaultInfo().getErrorCode());
+			} else if (cause instanceof XTAWSTechnicalProblemException) {
+				logCodeFehlerNummer(((XTAWSTechnicalProblemException) cause).getFaultInfo().getErrorCode());
+			}
 			throw e;
 		}
 	}
@@ -74,23 +102,268 @@ public class XtaServerSetupExtensionTestUtil {
 	}
 
 	@SneakyThrows
-	public static void closeAllMessages(XtaClient client, XtaIdentifier clientId) {
-		var field = XtaClient.class.getDeclaredField("service");
-		field.setAccessible(true);
-		var service = (WrappedXtaService) field.get(client);
+	public static void failIfAnyMessagePending(XtaClientConfig config, XtaIdentifier clientId) {
+		var wrappedService = createWrappedService(config);
+		var result = wrappedService.getStatusList(clientId, 1);
+		if (!result.messages().isEmpty()) {
+			fail("Expect no pending xta-messages for reader %s! Ensure that the mailbox is empty before running this test.".formatted(
+					clientId.value()));
+		}
+	}
 
-		var result = service.getStatusList(clientId, 100);
+	@SneakyThrows
+	public static void closeAllMessages(XtaClientConfig config, XtaIdentifier clientId) {
+		var wrappedService = createWrappedService(config);
+
+		var result = wrappedService.getStatusList(clientId, 100);
 		var messageIds = result.messages().stream()
 				.map(XtaMessageMetaData::messageId)
 				.toList();
 		for (var messageId : messageIds) {
-			service.close(messageId, clientId);
+			wrappedService.close(messageId, clientId);
+		}
+	}
+
+	@SneakyThrows
+	private static WrappedXtaService createWrappedService(XtaClientConfig config) {
+		return WrappedXtaServiceFactory.from(config).create();
+	}
+
+	public static XtaMessageMetaData withoutMessageIdAndSize(XtaMessageMetaData metaData) {
+		return metaData.toBuilder()
+				.messageId(null)
+				.messageSize(null)
+				.build();
+	}
+
+	public static XtaMessageMetaData withoutMessageSize(XtaMessageMetaData metaData) {
+		return metaData.toBuilder()
+				.messageSize(null)
+				.build();
+	}
+
+	public record MessagesAssert(List<XtaMessage> processedMessages) {
+
+		public MessagesAssert containExactlyInAnyOrder(XtaMessage... messages) {
+			try {
+				containExactlyInAnyOrderRaw(messages);
+			} catch (AssertionError | RuntimeException e) {
+				log.error("Messages do not exactly contain excepted messages!");
+				throw e;
+			}
+			return this;
+		}
+
+		private void containExactlyInAnyOrderRaw(XtaMessage... messages) {
+			var messageMetaData = Arrays.stream(messages)
+					.map(XtaMessage::metaData)
+					.toArray(XtaMessageMetaData[]::new);
+			containMetaDataExactlyInAnyOrder(messageMetaData);
+
+			assertEqualMessageFileWithoutContent(messages);
+			assertEqualAttachmentFilesWithoutContent(messages);
+
+			assertEqualContentOfMessageFile(messages);
+			assertEqualContentOfAttachmentFiles(messages);
+		}
+
+		private void assertEqualMessageFileWithoutContent(XtaMessage... messages) {
+			// ignoring size since it may be null before sending
+			assertThat(processedMessages)
+					.extracting(XtaMessage::messageFile)
+					.extracting(this::withoutContentAndSize)
+					.containsExactlyInAnyOrderElementsOf(Arrays.stream(messages)
+							.map(XtaMessage::messageFile)
+							.map(this::withoutContentAndSize)
+							.toList());
+		}
+
+		private void assertEqualAttachmentFilesWithoutContent(XtaMessage... messages) {
+			// ignoring size since it may be null before sending
+			assertThat(processedMessages)
+					.extracting(XtaMessage::attachmentFiles)
+					.extracting(this::filesWithoutContentAndSize)
+					.containsExactlyInAnyOrderElementsOf(Arrays.stream(messages)
+							.map(XtaMessage::attachmentFiles)
+							.map(this::filesWithoutContentAndSize)
+							.toList());
+		}
+
+		private List<XtaFile> filesWithoutContentAndSize(List<XtaFile> fileList) {
+			return fileList.stream().map(this::withoutContentAndSize).toList();
+		}
+
+		private XtaFile withoutContentAndSize(XtaFile xtaFile) {
+			return xtaFile.toBuilder()
+					.content(null)
+					.size(null)
+					.build();
+		}
+
+		private void assertEqualContentOfMessageFile(XtaMessage... messages) {
+			var listOfMessageFileContents = processedMessages.stream()
+					.map(XtaMessage::messageFile)
+					.map(XtaServerSetupExtensionTestUtil::readBytesOfXtaFile)
+					.toList();
+			var exceptedListOfMessageFileContents = Arrays.stream(messages)
+					.map(XtaMessage::messageFile)
+					.map(XtaServerSetupExtensionTestUtil::readBytesOfXtaFile)
+					.toList();
+			assertThat(listOfMessageFileContents)
+					.extracting(b -> b.length)
+					.containsExactlyInAnyOrderElementsOf(exceptedListOfMessageFileContents.stream().map(b -> b.length).toList());
+			assertThat(listOfMessageFileContents).containsExactlyInAnyOrderElementsOf(exceptedListOfMessageFileContents);
+		}
+
+		private void assertEqualContentOfAttachmentFiles(XtaMessage... messages) {
+			var attachmentFileContents = processedMessages.stream()
+					.map(XtaMessage::attachmentFiles)
+					.map(XtaServerSetupExtensionTestUtil::readBytesOfXtaFiles)
+					.toList();
+			var exceptedAttachmentFileContents = Arrays.stream(messages)
+					.map(XtaMessage::attachmentFiles)
+					.map(XtaServerSetupExtensionTestUtil::readBytesOfXtaFiles)
+					.toList();
+			assertEqualLengthOfAttachmentFiles(attachmentFileContents, exceptedAttachmentFileContents);
+			assertEqualRawContentOfAttachmentFiles(attachmentFileContents, exceptedAttachmentFileContents);
+		}
+
+		private void assertEqualLengthOfAttachmentFiles(List<List<byte[]>> attachmentFileContents,
+				List<List<byte[]>> exceptedAttachmentFileContents) {
+			Function<List<byte[]>, List<Integer>> contentLengths = contentList -> contentList.stream().map(b -> b.length).toList();
+			assertThat(attachmentFileContents)
+					.extracting(contentLengths)
+					.containsExactlyInAnyOrderElementsOf(exceptedAttachmentFileContents.stream().map(contentLengths).toList());
 		}
+
+		private void assertEqualRawContentOfAttachmentFiles(List<List<byte[]>> attachmentFileContents,
+				List<List<byte[]>> exceptedAttachmentFileContents) {
+			assertThat(attachmentFileContents)
+					.usingElementComparator(this::compareAttachmentFiles)
+					.containsExactlyInAnyOrderElementsOf(exceptedAttachmentFileContents);
+		}
+
+		private int compareAttachmentFiles(List<byte[]> a, List<byte[]> 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);
+		}
+
+		public void containMetaDataExactlyInAnyOrder(XtaMessageMetaData... messageMetaDataItems) {
+			try {
+				// Assert equal message counts
+				assertThat(processedMessages).hasSize(messageMetaDataItems.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(messageMetaDataItems)
+								.map(XtaServerSetupExtensionTestUtil::withoutMessageIdAndSize)
+								.toList());
+			} catch (AssertionError | RuntimeException e) {
+				log.error("Messages do not exactly contain excepted metadata!");
+				throw e;
+			}
+		}
+	}
+
+	public static MessagesAssert assertThatMessages(List<XtaMessage> processedMessages) {
+		return new MessagesAssert(processedMessages);
+	}
+
+	public record TransportReportsAssert(List<XtaTransportReport> transportReports) {
+		public TransportReportsAssert reportExactlyFor(List<XtaMessage> processedMessages) {
+			try {
+				reportExactlyForRaw(processedMessages);
+			} catch (AssertionError | RuntimeException e) {
+				log.error("TransportReports do not exactly match messages metadata!");
+				throw e;
+			}
+			return this;
+		}
+
+		private void reportExactlyForRaw(List<XtaMessage> processedMessages) {
+			assertThat(transportReports).hasSize(processedMessages.size());
+			assertEqualMessageId(processedMessages);
+			assertEqualMessageMetadata(processedMessages);
+		}
+
+		private void assertEqualMessageId(List<XtaMessage> processedMessages) {
+			assertThat(transportReports)
+					.extracting(XtaTransportReport::metaData)
+					.extracting(XtaMessageMetaData::messageId)
+					.containsExactlyElementsOf(processedMessages.stream()
+							.map(XtaMessage::metaData)
+							.map(XtaMessageMetaData::messageId)
+							.toList());
+		}
+
+		private void assertEqualMessageMetadata(List<XtaMessage> processedMessages) {
+			// ignoring size since it may be null for transport report (due to message closed?)
+			assertThat(transportReports)
+					.extracting(XtaTransportReport::metaData)
+					.extracting(XtaServerSetupExtensionTestUtil::withoutMessageSize)
+					.containsExactlyElementsOf(processedMessages.stream()
+							.map(XtaMessage::metaData)
+							.map(XtaServerSetupExtensionTestUtil::withoutMessageSize)
+							.toList());
+		}
+
+		public TransportReportsAssert haveExactlyClosedStatusFor(String... messageIds) {
+			try {
+				haveExactlyClosedStatusForRaw(messageIds);
+			} catch (AssertionError | RuntimeException e) {
+				log.error("TransportReports do not have excepted closed status for messageIds!");
+				throw e;
+			}
+			return this;
+		}
+
+		private void haveExactlyClosedStatusForRaw(String... messageIds) {
+			var setOfMessageIds = Arrays.stream(messageIds).collect(Collectors.toSet());
+
+			assertThat(transportReports)
+					.allMatch(transportReport ->
+							isClosed(transportReport) == expectIsClosed(transportReport, setOfMessageIds)
+
+					);
+		}
+
+		private boolean expectIsClosed(XtaTransportReport transportReport, Set<String> messageIds) {
+			return messageIds.contains(transportReport.metaData().messageId());
+		}
+
+		private boolean isClosed(XtaTransportReport transportReport) {
+			return !transportReport.status().equals(XtaMessageStatus.OPEN);
+		}
+	}
+
+	public static TransportReportsAssert assertThatTransportReports(List<XtaTransportReport> transportReports) {
+		return new TransportReportsAssert(transportReports);
+	}
+
+	public static boolean hasLogLineContaining(String logLine) {
+		return StaticStringListAppender.getLogLines().stream()
+				.anyMatch(line -> line.contains(logLine));
+	}
+
+	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 c66e62a9a48f220288bddc86a476daa4ebf34991..9001ba631dc7387f7f1d2af6fe76399307c8650d 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,10 +21,10 @@ 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("latest");
+			.withTag("1.6.0");
 
 	private static final String JOHN_SMITH_KEYSTORE_PATH = "store/john-smith-client-cert-keystore.p12";
 	private static final String JOHN_SMITH_KEYSTORE_PASSWORD = "password";
@@ -33,10 +32,12 @@ 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 silentTestClient;
+	private XtaClientConfig silentTestClientConfig;
 	private XtaClientFactory clientFactory;
 	private XtaTestServerContainer xtaServerContainer;
+	private XtaClientConfig.KeyStore clientCertKeyStore;
+	private XtaClientConfig.KeyStore trustStore;
 
 	@Override
 	@SneakyThrows
@@ -44,9 +45,27 @@ 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();
+		silentTestClientConfig = createSpecificClientConfigBuilder()
+				.logSoapRequests(false)
+				.logSoapResponses(false)
+				.build();
+		silentTestClient = XtaClient.from(silentTestClientConfig);
 	}
 
 	private void setupServer() {
@@ -66,43 +85,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
diff --git a/src/test/java/de/ozgcloud/xta/client/factory/ClientRuntimeExceptionTestFactory.java b/src/test/java/de/ozgcloud/xta/client/factory/ClientRuntimeExceptionTestFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..7b2d14bd1bad08067e1b6a1212d05717c08ab754
--- /dev/null
+++ b/src/test/java/de/ozgcloud/xta/client/factory/ClientRuntimeExceptionTestFactory.java
@@ -0,0 +1,33 @@
+package de.ozgcloud.xta.client.factory;
+
+import de.ozgcloud.xta.client.exception.XtaClientRuntimeException;
+import genv3.de.xoev.transport.xta.x211.CodeFehlernummer;
+import genv3.de.xoev.transport.xta.x211.PermissionDeniedException;
+import genv3.de.xoev.transport.xta.x211.PermissionDeniedExceptionType;
+
+public class ClientRuntimeExceptionTestFactory {
+
+	public static final String MESSAGE = "message";
+	public static final String CAUSE_MESSAGE = "cause message";
+	public static final String CAUSE_CODE = "cause code";
+	public static final String CAUSE_NAME = "cause name";
+
+	public static XtaClientRuntimeException create() {
+		return new XtaClientRuntimeException(MESSAGE, createPermissionDeniedException());
+	}
+
+	private static PermissionDeniedException createPermissionDeniedException() {
+		return new PermissionDeniedException(CAUSE_MESSAGE, createPermissionDeniedExceptionType());
+	}
+
+	private static PermissionDeniedExceptionType createPermissionDeniedExceptionType() {
+		var code = new CodeFehlernummer();
+		code.setCode(CAUSE_CODE);
+		code.setName(CAUSE_NAME);
+
+		var exceptionType = new PermissionDeniedExceptionType();
+		exceptionType.setErrorCode(code);
+		return exceptionType;
+	}
+
+}
diff --git a/src/test/java/de/ozgcloud/xta/client/factory/XtaClientExceptionTestFactory.java b/src/test/java/de/ozgcloud/xta/client/factory/XtaClientExceptionTestFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..ac6767647f556c06078c2ac90025f966ba5207a4
--- /dev/null
+++ b/src/test/java/de/ozgcloud/xta/client/factory/XtaClientExceptionTestFactory.java
@@ -0,0 +1,12 @@
+package de.ozgcloud.xta.client.factory;
+
+import de.ozgcloud.xta.client.exception.XtaClientException;
+
+public class XtaClientExceptionTestFactory {
+
+	public static final String CLIENT_EXCEPTION_MESSAGE = "CLIENT_EXCEPTION_MESSAGE";
+
+	public static XtaClientException create() {
+		return new XtaClientException(CLIENT_EXCEPTION_MESSAGE, ClientRuntimeExceptionTestFactory.create().getCause());
+	}
+}
diff --git a/src/test/java/de/ozgcloud/xta/client/factory/XtaMessageMetaDataListingTestFactory.java b/src/test/java/de/ozgcloud/xta/client/factory/XtaMessageMetaDataListingTestFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..3da8ca5d25feb8ad894c7cb5ab43f1f955da93b5
--- /dev/null
+++ b/src/test/java/de/ozgcloud/xta/client/factory/XtaMessageMetaDataListingTestFactory.java
@@ -0,0 +1,31 @@
+package de.ozgcloud.xta.client.factory;
+
+import static de.ozgcloud.xta.client.factory.MessageMetaDataTestFactory.*;
+
+import java.math.BigInteger;
+import java.util.List;
+
+import de.ozgcloud.xta.client.model.XtaMessageMetaDataListing;
+
+public class XtaMessageMetaDataListingTestFactory {
+
+	public final static BigInteger PENDING_MESSAGES_COUNT = BigInteger.valueOf(7);
+
+	public static XtaMessageMetaDataListing create() {
+		return createBuilder().build();
+	}
+
+	public static XtaMessageMetaDataListing.XtaMessageMetaDataListingBuilder createBuilder() {
+		return XtaMessageMetaDataListing.builder()
+				.messages(List.of(XtaMessageMetaDataTestFactory.create(),
+						XtaMessageMetaDataTestFactory.createBuilder()
+								.messageId(MESSAGE_ID2)
+								.messageTypeCode(MESSAGE_TYPE_CODE2)
+								.build(),
+						XtaMessageMetaDataTestFactory.createBuilder()
+								.messageId(MESSAGE_ID3)
+								.messageTypeCode(MESSAGE_TYPE_CODE3)
+								.build()))
+				.pendingMessageCount(PENDING_MESSAGES_COUNT);
+	}
+}
diff --git a/src/test/java/de/ozgcloud/xta/client/factory/XtaTransportReportTestFactory.java b/src/test/java/de/ozgcloud/xta/client/factory/XtaTransportReportTestFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..33d54a78cce10cf194d8c9b9a99f37057a68f580
--- /dev/null
+++ b/src/test/java/de/ozgcloud/xta/client/factory/XtaTransportReportTestFactory.java
@@ -0,0 +1,21 @@
+package de.ozgcloud.xta.client.factory;
+
+import static de.ozgcloud.xta.client.factory.TransportReportTestFactory.*;
+
+
+import de.ozgcloud.xta.client.model.XtaMessageStatus;
+import de.ozgcloud.xta.client.model.XtaTransportReport;
+
+public class XtaTransportReportTestFactory {
+
+	public static XtaTransportReport create() {
+		return createBuilder().build();
+	}
+
+	public static XtaTransportReport.XtaTransportReportBuilder createBuilder() {
+		return XtaTransportReport.builder()
+				.reportTime(REPORT_TIME)
+				.metaData(XtaMessageMetaDataTestFactory.create())
+				.status(XtaMessageStatus.OPEN);
+	}
+}
diff --git a/src/test/java/de/ozgcloud/xta/client/xdomea/XdomeaMetaDataValidatorTest.java b/src/test/java/de/ozgcloud/xta/client/xdomea/XdomeaMetaDataValidatorTest.java
index b3251bedc7fb829bd22a35a6d6c6f648c678626c..97dd894d8b6c47a01863919b007736e7363bb99a 100644
--- a/src/test/java/de/ozgcloud/xta/client/xdomea/XdomeaMetaDataValidatorTest.java
+++ b/src/test/java/de/ozgcloud/xta/client/xdomea/XdomeaMetaDataValidatorTest.java
@@ -21,7 +21,7 @@ import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Spy;
 
-import de.ozgcloud.xta.client.exception.ClientException;
+import de.ozgcloud.xta.client.exception.XtaClientException;
 import de.ozgcloud.xta.client.factory.XdomeaXmlValuesTestFactory;
 import de.ozgcloud.xta.client.factory.XtaFileTestFactory;
 import de.ozgcloud.xta.client.factory.XtaMessageMetaDataTestFactory;
@@ -170,7 +170,7 @@ class XdomeaMetaDataValidatorTest {
 		})
 		void shouldThrowClientExceptionForWrongPrefix(String prefix) {
 			assertThatThrownBy(() -> validator.validateIdentifierPrefix(prefix, EXPECTED_PREFIX, PREFIX_NAME))
-					.isInstanceOf(ClientException.class)
+					.isInstanceOf(XtaClientException.class)
 					.hasMessageContaining(PREFIX_NAME);
 		}
 
@@ -200,7 +200,7 @@ class XdomeaMetaDataValidatorTest {
 		})
 		void shouldThrowClientExceptionForWrongZipFileName(String wrongZipFileName) {
 			assertThatThrownBy(() -> validator.validateZipFileName(wrongZipFileName, PROCESS_ID, MESSAGE_TYPE_CODE))
-					.isInstanceOf(ClientException.class);
+					.isInstanceOf(XtaClientException.class);
 		}
 
 	}
@@ -241,7 +241,7 @@ class XdomeaMetaDataValidatorTest {
 			when(zipFileEntryReader.getEntryNames(contentDataHandler)).thenReturn(INVALID_ZIP_ENTRY_NAMES);
 
 			assertThatThrownBy(() -> validator.validatePrimaryDocumentReferences(xdomeaZipFile, PRIMARY_DOCUMENT_NAMES))
-					.isInstanceOf(ClientException.class);
+					.isInstanceOf(XtaClientException.class);
 		}
 	}
 
diff --git a/src/test/java/de/ozgcloud/xta/client/xdomea/XdomeaXtaMessageCreatorITCase.java b/src/test/java/de/ozgcloud/xta/client/xdomea/XdomeaXtaMessageCreatorITCase.java
index 6256c854dbd400a3fabd26ed0f5520ac5dd89f13..7ff3ffd6754a4682db3cf3c660134f77127a6ee0 100644
--- a/src/test/java/de/ozgcloud/xta/client/xdomea/XdomeaXtaMessageCreatorITCase.java
+++ b/src/test/java/de/ozgcloud/xta/client/xdomea/XdomeaXtaMessageCreatorITCase.java
@@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 
-import de.ozgcloud.xta.client.exception.ClientException;
+import de.ozgcloud.xta.client.exception.XtaClientException;
 import de.ozgcloud.xta.client.extension.XtaMessageExampleLoader;
 import de.ozgcloud.xta.client.model.XtaFile;
 import lombok.SneakyThrows;
@@ -77,7 +77,7 @@ class XdomeaXtaMessageCreatorITCase {
 			);
 
 			assertThatThrownBy(() -> creator.createMessage(invalidMessageZipFile))
-					.isInstanceOf(ClientException.class);
+					.isInstanceOf(XtaClientException.class);
 		}
 
 		@DisplayName("should not throw with valid message 0201")
@@ -118,7 +118,7 @@ class XdomeaXtaMessageCreatorITCase {
 			);
 
 			assertThatThrownBy(() -> creator.createMessage(invalidMessageZipFile))
-					.isInstanceOf(ClientException.class);
+					.isInstanceOf(XtaClientException.class);
 		}
 
 		@DisplayName("should not throw with valid message 0401")
diff --git a/src/test/java/de/ozgcloud/xta/client/xdomea/XdomeaXtaMessageCreatorTest.java b/src/test/java/de/ozgcloud/xta/client/xdomea/XdomeaXtaMessageCreatorTest.java
index 6ffa37b6177f2022c068f847707a02c5d8e36d3f..a183007f589a4083b82296c8c9331825a434c727 100644
--- a/src/test/java/de/ozgcloud/xta/client/xdomea/XdomeaXtaMessageCreatorTest.java
+++ b/src/test/java/de/ozgcloud/xta/client/xdomea/XdomeaXtaMessageCreatorTest.java
@@ -20,7 +20,7 @@ import org.mockito.Spy;
 import org.w3c.dom.Document;
 
 import de.ozgcloud.common.errorhandling.TechnicalException;
-import de.ozgcloud.xta.client.exception.ClientException;
+import de.ozgcloud.xta.client.exception.XtaClientException;
 import de.ozgcloud.xta.client.factory.XdomeaXmlValuesTestFactory;
 import de.ozgcloud.xta.client.factory.XtaFileTestFactory;
 import de.ozgcloud.xta.client.factory.XtaMessageMetaDataTestFactory;
@@ -156,7 +156,7 @@ class XdomeaXtaMessageCreatorTest {
 			doThrow(technicalException).when(metadataMapper).mapXtaMessageMetadata(xdomeaXmlValues);
 
 			assertThatThrownBy(() -> creator.deriveValidMetaData(xdomeaZipFile))
-					.isInstanceOf(ClientException.class);
+					.isInstanceOf(XtaClientException.class);
 		}
 
 	}
diff --git a/src/test/java/de/ozgcloud/xta/client/xdomea/reader/XdomeaValueReaderTest.java b/src/test/java/de/ozgcloud/xta/client/xdomea/reader/XdomeaValueReaderTest.java
index 9c72e7ab2ab52438c42b923abbc78b4b519d335e..5b8fd71b95ed7d76f2527f3cbce0442311f636f2 100644
--- a/src/test/java/de/ozgcloud/xta/client/xdomea/reader/XdomeaValueReaderTest.java
+++ b/src/test/java/de/ozgcloud/xta/client/xdomea/reader/XdomeaValueReaderTest.java
@@ -16,7 +16,7 @@ import org.mockito.Mock;
 import org.mockito.Spy;
 import org.w3c.dom.Document;
 
-import de.ozgcloud.xta.client.exception.ClientException;
+import de.ozgcloud.xta.client.exception.XtaClientException;
 import lombok.SneakyThrows;
 
 class XdomeaValueReaderTest {
@@ -183,7 +183,7 @@ class XdomeaValueReaderTest {
 			when(xmlValueReader.readNonEmptyTexts(xdomeaXmlDocument)).thenReturn(Stream.empty());
 
 			assertThatThrownBy(() -> xdomeaValueReader.readRequiredValue(xdomeaXmlDocument, XPATH_STRING))
-					.isInstanceOf(ClientException.class);
+					.isInstanceOf(XtaClientException.class);
 		}
 	}
 
diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml
index 71f815985778d395c5f74d051be77a7e04cbf83b..b2ca53a3d66fedb98bddcef50e5e133fcb135777 100644
--- a/src/test/resources/log4j2.xml
+++ b/src/test/resources/log4j2.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<Configuration name="log4j2Config" status="WARN">
+<Configuration name="log4j2Config" status="WARN" packages="de.ozgcloud.xta.client.extension">
 
     <Appenders>
         <Console name="Console" target="SYSTEM_OUT">
@@ -9,11 +9,13 @@
                     onMismatch="DENY" />
             </filters>
         </Console>
+        <StaticStringList name="StaticStringList" />
     </Appenders>
 
     <Loggers>
         <Logger name="de.ozgcloud.xta" level="debug" additivity="false">
             <AppenderRef ref="Console" level="${env:LOG_LEVEL_STDOUT:-debug}" />
+            <AppenderRef ref="StaticStringList" level="debug" />
         </Logger>
 
         <Root level="info">
diff --git a/src/test/resources/messages/abgabe0401-kleiner-waffenschein/xta-message.yaml b/src/test/resources/messages/abgabe0401-kleiner-waffenschein/xta-message.yaml
index 1b1adb5a6c724aa8b16aefbb1f79956713cdac01..4be291a5f51c45b7c77fbda95b9c7bc9bf70abb7 100644
--- a/src/test/resources/messages/abgabe0401-kleiner-waffenschein/xta-message.yaml
+++ b/src/test/resources/messages/abgabe0401-kleiner-waffenschein/xta-message.yaml
@@ -1,5 +1,5 @@
 metaData:
-  service: urn:xoev-de:xdomea:schema:3.0.0/xdomea300Antrag.wsdl
+  service: urn:xoev-de:xdomea:schema:3.0.0/xdomea300Abgabe.wsdl
   businessScenarioCode: XDOMEAGAD_DATA
   businessScenarioListUri: urn:de:dataport:codeliste:business.scenario
   businessScenarioListVersionId: 1.0