diff --git a/README.md b/README.md
index b8c33838f7256b25ddd376b944f2f5bf14377e6e..e298c5a4dd59203795ed5521d05baeb3656d6467 100644
--- a/README.md
+++ b/README.md
@@ -95,7 +95,7 @@ _The following steps show how to get the SDK running_
    ```sh
    ./mvnw clean install -DskipTests
    ```
-5. Configure your  `config.yml`
+5. Configure your `config.yml` (see [template](/config.yml))
     - add Sender OAuth credentials from the self-service portal to *SENDER* section 
     - add Subscriber OAuth credentials from the self-service portal to *SUBSCRIBER* section 
     - add reference to private decryption key (JWK) to *SUBSCRIBER* section
@@ -110,6 +110,59 @@ _The following steps show how to get the SDK running_
 
 <p align="right">(<a href="#top">back to top</a>)</p>
 
+## API Usage for Routing
+The Routing-Client allows to retrieve data about areas and services as well as their service destination.
+A typical workflow using the `RoutingClient` and `SenderClient` would be:
+
+1) Find the `areaId` for an area via routing
+2) Find the destination for a `leikaKey` and an `areaId` via routing
+3) Submit a new submission to the destination using the `SenderClient`
+
+### Finding Areas
+Areas can be searched with one or more search criteria:
+
+```java
+final RoutingClient routingClient = ClientFactory.routingClient(config);
+
+final var citySearchCriterion = "Leip*";
+final var zipCodeSearchCriterion = "04229";
+
+// get first 5 area results
+final List<Area> areas = routingClient.findAreas(List.of(citySearchCriterion, zipCodeSearchCriterion), 0, 5);
+
+LOGGER.info("Found {} areas", areas.size());
+for (final Area area : areas){
+    LOGGER.info("Area {} with id {} found", area.getName(), area.getId());
+}
+```
+
+### Finding Destinations by service identifier and region
+
+For searching a destination the `DestinationSearch.Builder` is used pass a search request to the routing client. 
+The leikaKey is mandatory, as well as (max.) one other search criterion for the area/region, like one of:
+- ars amtlicher regionalschlüssel
+- ags amtlicher gemeindeschlüssel
+- areaId identifier of an area that can be retrieved via [finding areas](#finding-areas)
+
+```java
+final RoutingClient routingClient = ClientFactory.routingClient(config);
+
+final DestinationSearch search = DestinationSearch.Builder()
+        .withLeikaKey("99123456760610")
+        .withArs("064350014014")
+        .withLimit(5)
+        .build();
+
+// get first 5 route results
+final List<Route> routes = routingClient.findDestinations(search);
+
+LOGGER.info("Found {} routes for service identifier {}", routes.size(), leikaKey);
+for (final Route route : routes){
+    LOGGER.info("Route {} with destinationId {} found", route.getName(), route.getDestinationId());
+}
+```
+
+
 ## API Usage for Sender
 
 For sending submission and already encrypted submissions builder are provided to construct the necessary payload to be sent. 
@@ -374,8 +427,7 @@ if(validationResult.hasError()){
 ```
 
 ## Roadmap
-- [ ] Add Routing features
-- [ ] Add Callback validation
+
 - [ ] Add auto-reject on technical errors
 - [ ] Maven central release of 1.0.0
 
diff --git a/api/README.md b/api/README.md
index 9981df66fdc0e09d84f8ee3e8748b8a51fdab368..18ff35050c164b4c952526ca5e31906974d21e35 100644
--- a/api/README.md
+++ b/api/README.md
@@ -1,7 +1,6 @@
 ## API Module
 
-The API-module contains interfaces and domain model value classes that provide the basic functionality to build an
-sdk-client.
+The API-module contains interfaces and domain model value classes that provide the basic functionality to build a sdk-client.
 
 ### Structure
 
@@ -10,7 +9,7 @@ sdk-client.
 - **api.exceptions** - all use case specific the services throw
 - **api.services** - all services to authenticate, encrypt, validate and perform REST-requests
 
-There are two service facade interfaces that provide a client centric wrapper around the underlying services,
+There are two service facade interfaces that provide a client centric wrapper around the underlying services:
 
 - **Sender** - create a submission, announce attachments, encrypt and send the submission including metadata
 - **Subscriber** - poll, receive and decrypt submissions and confirm their valid transmission
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/config/ApplicationConfig.java b/api/src/main/java/dev/fitko/fitconnect/api/config/ApplicationConfig.java
index deeb94b053a444797f00886b5f4ac58b7cd72c02..5c57e4aa7fa47ba9f2d40b0697ab5c34537e91d9 100644
--- a/api/src/main/java/dev/fitko/fitconnect/api/config/ApplicationConfig.java
+++ b/api/src/main/java/dev/fitko/fitconnect/api/config/ApplicationConfig.java
@@ -30,6 +30,9 @@ public class ApplicationConfig {
     @Builder.Default
     private URI metadataSchemaWriteVersion = SchemaConfig.METADATA_V_1_0_0.getSchemaUri();
 
+    @Builder.Default
+    private URI destinationSchemaVersion = SchemaConfig.XZUFI_DESTINATION_SCHEMA.getSchemaUri();
+
     private SenderConfig senderConfig;
 
     private SubscriberConfig subscriberConfig;
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/config/ResourcePaths.java b/api/src/main/java/dev/fitko/fitconnect/api/config/ResourcePaths.java
index 96b2c3d78fa139b782dcf369ac1059e932aec3ae..0f429301b2ddad6434f366c1a383746a7edf4352 100644
--- a/api/src/main/java/dev/fitko/fitconnect/api/config/ResourcePaths.java
+++ b/api/src/main/java/dev/fitko/fitconnect/api/config/ResourcePaths.java
@@ -6,14 +6,19 @@ final class ResourcePaths {
     }
 
     static final String AUTH_TOKEN_PATH = "/token";
+
     static final String DESTINATIONS_PATH = "/v1/destinations/{destinationId}";
     static final String DESTINATIONS_KEY_PATH = "/v1/destinations/{destinationId}/keys/{kid}";
+
     static final String EVENTS_PATH = "/v1/cases/{caseId}/events";
+
     static final String SUBMISSION_PATH = "/v1/submissions/{submissionId}";
     static final String SUBMISSIONS_PATH = "/v1/submissions";
     static final String SUBMISSION_ATTACHMENT_PATH = "/v1/submissions/{submissionId}/attachments/{attachmentId}";
+
     static final String ROUTING_AREA_PATH = "/v1/areas";
     static final String ROUTING_ROUTE_PATH = "/v1/routes";
+
     static final String WELL_KNOWN_KEYS_PATH = "/.well-known/jwks.json";
 }
 
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/config/SchemaConfig.java b/api/src/main/java/dev/fitko/fitconnect/api/config/SchemaConfig.java
index ca64a9591950639ca6bc6481fd1e9d62c281bc5c..eeffbfb7dce1506b1c6af3e8229df2183f644955 100644
--- a/api/src/main/java/dev/fitko/fitconnect/api/config/SchemaConfig.java
+++ b/api/src/main/java/dev/fitko/fitconnect/api/config/SchemaConfig.java
@@ -14,7 +14,9 @@ public enum SchemaConfig {
     EVENTS_SCHEMA_PATH(SCHEMA_BASE_URL.schemaUri.resolve("events/")),
     SET_V_1_0_1(SCHEMA_BASE_URL.schemaUri.resolve("set-payload/1.0.1/set-payload.schema.json"), "set_schema_1.0.1.json"),
     SET_V_1_0_0(SCHEMA_BASE_URL.schemaUri.resolve("set-payload/1.0.0/set-payload.schema.json"), "set_schema_1.0.0.json"),
-    METADATA_V_1_0_0(SCHEMA_BASE_URL.schemaUri.resolve("metadata/1.0.0/metadata.schema.json"), "metadata_schema_1.0.0.json");
+    METADATA_V_1_0_0(SCHEMA_BASE_URL.schemaUri.resolve("metadata/1.0.0/metadata.schema.json"), "metadata_schema_1.0.0.json"),
+
+    XZUFI_DESTINATION_SCHEMA(SCHEMA_BASE_URL.schemaUri.resolve("xzufi/destination.schema.json"), "destination_schema.json");
 
     private final URI schemaUri;
 
@@ -42,6 +44,12 @@ public enum SchemaConfig {
                 .collect(Collectors.toList());
     }
 
+    public static List<String> getDestinationSchemaPaths(final String destinationSchemaBaseDir) {
+        return Stream.of(XZUFI_DESTINATION_SCHEMA.fileName)
+                .map(fileName -> destinationSchemaBaseDir + "/" + fileName)
+                .collect(Collectors.toList());
+    }
+
     @Override
     public String toString() {
         return schemaUri.toString();
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/destination/ReplyChannelEMail.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/destination/ReplyChannelEMail.java
index ed32bc9179829fd836574367d471c55b3681b335..7f9fec808762c485d58af6375dca265e66ba41d1 100644
--- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/destination/ReplyChannelEMail.java
+++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/destination/ReplyChannelEMail.java
@@ -1,14 +1,12 @@
 package dev.fitko.fitconnect.api.domain.model.destination;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonTypeName;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
 @Data
-@JsonTypeName("ReplyChannel_eMail")
 @NoArgsConstructor
-public class ReplyChannelEMail {
+class ReplyChannelEMail {
 
     @JsonProperty("usePgp")
     private Boolean usePgp;
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/attachment/Purpose.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/attachment/Purpose.java
index 5e12c15d01b2218be1dc1f99ac9bdacd9e6eb25e..553fd977469a2af835580bc8cd346c6850f0f1eb 100644
--- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/attachment/Purpose.java
+++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/attachment/Purpose.java
@@ -3,34 +3,45 @@ package dev.fitko.fitconnect.api.domain.model.metadata.attachment;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonValue;
 
-import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
 
 public enum Purpose {
 
     FORM("form"),
     ATTACHMENT("attachment"),
     REPORT("report");
+
     private final String value;
 
+    private static final Map<String, Purpose> CONSTANTS = new HashMap<>();
+
+    static {
+        for (final Purpose p : values()) {
+            CONSTANTS.put(p.value, p);
+        }
+    }
+
     Purpose(final String value) {
         this.value = value;
     }
 
     @Override
     public String toString() {
-        return this.value;
+        return value;
     }
 
     @JsonValue
     public String value() {
-        return this.value;
+        return value;
     }
 
     @JsonCreator
     public static Purpose fromValue(final String value) {
-        return Arrays.stream(Purpose.values())
-                .filter(enumValue -> enumValue.value.equals(value))
-                .findFirst()
-                .orElseThrow(() -> new IllegalArgumentException("Unexpected value '" + value + "'"));
+        final Purpose constant = CONSTANTS.get(value);
+        if (constant == null) {
+            throw new IllegalArgumentException("Unexpected value '" + value + "'");
+        }
+        return constant;
     }
 }
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/Area.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/Area.java
new file mode 100644
index 0000000000000000000000000000000000000000..5bd59c70e123822d623ac37dd0dc5cbff7bb8fe6
--- /dev/null
+++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/Area.java
@@ -0,0 +1,20 @@
+package dev.fitko.fitconnect.api.domain.model.route;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class Area {
+
+  @JsonProperty("id")
+  private String id;
+
+  @JsonProperty("name")
+  private String name;
+
+  @JsonProperty("type")
+  private String type;
+
+}
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/AreaResult.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/AreaResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..7943a6ce33adfb03e735ba73f360f79531aae18d
--- /dev/null
+++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/AreaResult.java
@@ -0,0 +1,27 @@
+package dev.fitko.fitconnect.api.domain.model.route;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class AreaResult {
+
+  @JsonProperty("count")
+  private Integer count;
+
+  @JsonProperty("offset")
+  private Integer offset;
+
+  @JsonProperty("totalCount")
+  private Integer totalCount;
+
+  @JsonProperty("areas")
+  private List<Area> areas = new ArrayList<>();
+}
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/Route.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/Route.java
new file mode 100644
index 0000000000000000000000000000000000000000..0bd6fd3741e2214fdddc681cee8b912fdc4a1632
--- /dev/null
+++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/Route.java
@@ -0,0 +1,31 @@
+package dev.fitko.fitconnect.api.domain.model.route;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.UUID;
+
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class Route {
+
+    @JsonProperty("destinationId")
+    private UUID destinationId;
+
+    @JsonProperty("destinationSignature")
+    private String destinationSignature;
+
+    @JsonProperty("destinationParameters")
+    private RouteDestination destinationParameters;
+
+    @JsonProperty("destinationParametersSignature")
+    private String destinationParametersSignature;
+
+    @JsonProperty("destinationName")
+    private String destinationName;
+
+    @JsonProperty("destinationLogo")
+    private String destinationLogo;
+
+}
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/RouteDestination.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/RouteDestination.java
new file mode 100644
index 0000000000000000000000000000000000000000..8db7011a36135a10fa6d3d1ce169bc4b2142d67c
--- /dev/null
+++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/RouteDestination.java
@@ -0,0 +1,42 @@
+package dev.fitko.fitconnect.api.domain.model.route;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import dev.fitko.fitconnect.api.domain.model.destination.ReplyChannel;
+import dev.fitko.fitconnect.api.domain.model.destination.StatusEnum;
+import dev.fitko.fitconnect.api.domain.model.jwk.ApiJwkSet;
+import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class RouteDestination {
+
+  @JsonProperty("encryptionKid")
+  private String encryptionKid = null;
+
+  @JsonProperty("metadataVersions")
+  private List<String> metadataVersions = new ArrayList<>();
+
+  @JsonProperty("publicKeys")
+  private ApiJwkSet publicKeys = null;
+
+  @JsonProperty("replyChannels")
+  private ReplyChannel replyChannels = null;
+
+  @JsonProperty("status")
+  private StatusEnum status = null;
+
+  @JsonProperty("submissionSchemas")
+  private List<SubmissionSchema> submissionSchemas = new ArrayList<>();
+
+  @JsonProperty("submissionUrl")
+  private String submissionUrl = null;
+
+
+
+
+}
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/RouteResult.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/RouteResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..0fafa2fb6d90c7eeec586ba2d4d56d39e3231393
--- /dev/null
+++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/RouteResult.java
@@ -0,0 +1,28 @@
+package dev.fitko.fitconnect.api.domain.model.route;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class RouteResult {
+
+  @JsonProperty("count")
+  private Integer count;
+
+  @JsonProperty("offset")
+  private Integer offset;
+
+  @JsonProperty("totalCount")
+  private Integer totalCount;
+
+  @JsonProperty("routes")
+  private List<Route> routes = new ArrayList<>();
+
+}
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/schema/SchemaResources.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/schema/SchemaResources.java
new file mode 100644
index 0000000000000000000000000000000000000000..022b164a45561ce5305734532cc14a518787625f
--- /dev/null
+++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/schema/SchemaResources.java
@@ -0,0 +1,13 @@
+package dev.fitko.fitconnect.api.domain.schema;
+
+import lombok.Value;
+
+import java.util.List;
+
+@Value
+public class SchemaResources {
+
+    List<String> setSchemaPaths;
+    List<String> metadataSchemaPaths;
+    List<String> destinationSchemaPaths;
+}
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/exceptions/RoutingException.java b/api/src/main/java/dev/fitko/fitconnect/api/exceptions/RoutingException.java
new file mode 100644
index 0000000000000000000000000000000000000000..ad334471f971fc361883cc03781fdd37b6b21f36
--- /dev/null
+++ b/api/src/main/java/dev/fitko/fitconnect/api/exceptions/RoutingException.java
@@ -0,0 +1,12 @@
+package dev.fitko.fitconnect.api.exceptions;
+
+public class RoutingException extends RuntimeException {
+
+    public RoutingException(final String errorMessage) {
+        super(errorMessage);
+    }
+
+    public RoutingException(final String errorMessage, final Throwable error) {
+        super(errorMessage, error);
+    }
+}
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/services/keys/KeyService.java b/api/src/main/java/dev/fitko/fitconnect/api/services/keys/KeyService.java
index 153b0614cef0d932e9e3ff991810060529beeb5a..040dc4166d8c5b001ff46a63f5ac566f432e11c6 100644
--- a/api/src/main/java/dev/fitko/fitconnect/api/services/keys/KeyService.java
+++ b/api/src/main/java/dev/fitko/fitconnect/api/services/keys/KeyService.java
@@ -38,7 +38,7 @@ public interface KeyService {
      * @param keyId unique identifier of the {@link RSAKey}
      * @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)})
      */
-    RSAKey getPortalSignatureKey(String keyId);
+    RSAKey getPortalPublicKey(String keyId);
 
     /**
      * Get a public signature key for a given key-id from the submission service well-known keys.
@@ -46,6 +46,15 @@ public interface KeyService {
      * @param keyId unique identifier of the {@link RSAKey}
      * @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)})
      */
-    RSAKey getSubmissionServiceSignatureKey(String keyId);
+    RSAKey getSubmissionServicePublicKey(String keyId);
 
+    /**
+     * Get a public signature key for a given key-id from a submission service instance url well known keys.
+     * The <b>'/well-known/jkws.json'</b> is added to the given url.
+     *
+     * @param url custom url to load the well known keys from
+     * @param keyId unique identifier of the {@link RSAKey} the well known keys are filtered by
+     * @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)})
+     */
+    RSAKey getWellKnownKeysForSubmissionUrl(String url, String keyId);
 }
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/services/routing/RoutingService.java b/api/src/main/java/dev/fitko/fitconnect/api/services/routing/RoutingService.java
new file mode 100644
index 0000000000000000000000000000000000000000..d398df4511dffc54be443b6da13ab7608169fa11
--- /dev/null
+++ b/api/src/main/java/dev/fitko/fitconnect/api/services/routing/RoutingService.java
@@ -0,0 +1,42 @@
+package dev.fitko.fitconnect.api.services.routing;
+
+import dev.fitko.fitconnect.api.domain.model.route.Area;
+import dev.fitko.fitconnect.api.domain.model.route.AreaResult;
+import dev.fitko.fitconnect.api.domain.model.route.Route;
+import dev.fitko.fitconnect.api.domain.model.route.RouteResult;
+import dev.fitko.fitconnect.api.exceptions.RestApiException;
+
+import java.util.List;
+
+/**
+ * Routing API Service that retrieves data form FIT-Connect Routing API REST-Endpoints
+ */
+public interface RoutingService {
+
+    /**
+     * Finds an {@link AreaResult} based on a list of multiple filter criteria include a zip code or e.g. city as in List.of("04229", "Leip*").
+     *
+     * @param searchExpressions list of string filters
+     * @param offset  offset to start from
+     * @param limit   max entries
+     *
+     * @return list of {@link Area}
+     * @throws RestApiException if a technical error occurred
+     */
+    AreaResult getAreas(List<String> searchExpressions, int offset, int limit) throws RestApiException;
+
+    /**
+     * Finds a {@link RouteResult} by a given  service identifier and AT LEAST ONE OTHER search criterion (ars | ags | areaId).
+     *
+     * @param leikaKey leikaKey
+     * @param ars      amtlicher regionalschlüssel
+     * @param ags      amtlicher gemeindeschlüssel
+     * @param areaId   areaId
+     * @param offset   offset to start from
+     * @param limit    max entries
+     *
+     * @return list of found {@link Route}s matching the search criteria
+     * @throws RestApiException if a technical error occurred
+     */
+    RouteResult getRoutes(String leikaKey, String ars, String ags, String areaId, int offset, int limit) throws RestApiException;
+}
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/services/routing/RoutingVerificationService.java b/api/src/main/java/dev/fitko/fitconnect/api/services/routing/RoutingVerificationService.java
new file mode 100644
index 0000000000000000000000000000000000000000..7f7ca0af06d071ea59573e40a2aaaaac9586ec2e
--- /dev/null
+++ b/api/src/main/java/dev/fitko/fitconnect/api/services/routing/RoutingVerificationService.java
@@ -0,0 +1,22 @@
+package dev.fitko.fitconnect.api.services.routing;
+
+import dev.fitko.fitconnect.api.domain.model.route.Route;
+import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
+
+import java.util.List;
+
+/**
+ * Service to verify the validity of routing-related data.
+ */
+public interface RoutingVerificationService {
+
+    /**
+     * Verifies host url, self-service portal signature and DVDV signature of the provided {@link Route}.
+     *
+     * @param routes                     list of routes to be verified
+     * @param requestedServiceIdentifier requested service identifier to be checked if the route supports it
+     * @param requestedRegion            requested service region to be checked if the routes supports it
+     * @return {@link ValidationResult}, is ok if validation passes, has an error including an exception if the validation failed
+     */
+    ValidationResult validateRouteDestinations(List<Route> routes, String requestedServiceIdentifier, String requestedRegion);
+}
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/services/schema/SchemaProvider.java b/api/src/main/java/dev/fitko/fitconnect/api/services/schema/SchemaProvider.java
index 70cc8bdd31f2274078a731ed73ce10c4e3c6285c..95bf3bedc5aef606bc12b404d3d83e031949ccc3 100644
--- a/api/src/main/java/dev/fitko/fitconnect/api/services/schema/SchemaProvider.java
+++ b/api/src/main/java/dev/fitko/fitconnect/api/services/schema/SchemaProvider.java
@@ -50,4 +50,14 @@ public interface SchemaProvider {
      * @throws SchemaNotFoundException if the schema is not existing
      */
     String loadMetadataSchema(URI schemaUri) throws SchemaNotFoundException;
+
+    /**
+     * Load the schema for payload of signed destination.
+     *
+     * @param schemaUri uri of the destination schema that should be loaded
+     * @return the schema as string
+     *
+     * @throws SchemaNotFoundException if the schema is not existing
+     */
+    String loadDestinationSchema(URI schemaUri) throws SchemaNotFoundException;
 }
\ No newline at end of file
diff --git a/api/src/main/java/dev/fitko/fitconnect/api/services/validation/ValidationService.java b/api/src/main/java/dev/fitko/fitconnect/api/services/validation/ValidationService.java
index 34c97b6660919e65a9281fbed6d7a9895df49dd0..c5d747b3f9e54e225c6f9ef0d55aad86628c4f4d 100644
--- a/api/src/main/java/dev/fitko/fitconnect/api/services/validation/ValidationService.java
+++ b/api/src/main/java/dev/fitko/fitconnect/api/services/validation/ValidationService.java
@@ -4,6 +4,8 @@ import com.nimbusds.jose.jwk.RSAKey;
 import dev.fitko.fitconnect.api.domain.model.metadata.Metadata;
 import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
 
+import java.util.Map;
+
 /**
  * Validator for publicKeys and metadata.
  *
@@ -53,6 +55,15 @@ public interface ValidationService {
      */
     ValidationResult validateSetEventSchema(String setEventPayload);
 
+    /**
+     * Validates signature payload claims against a given schema.
+     *
+     * @param destinationPayload the payload to be validated
+     *
+     * @return a {@link ValidationResult} with an optional error
+     */
+    ValidationResult validateDestinationSchema(Map<String, Object> destinationPayload);
+
     /**
      * Compares a given byte[] to its original hash value.
      *
diff --git a/client/README.md b/client/README.md
index 34acfc52addd637b84b765b6ea9d91753522e9a7..9d9ca485e698761807456e1560309695298408cf 100644
--- a/client/README.md
+++ b/client/README.md
@@ -1,11 +1,11 @@
 ## Client Module
 
 ### Commandline Client
-The sdk comes with a commandline client to be able to use the sdk without any coding.
+The sdk provides a commandline-client as a runnable .jar, to be able to use the sdk without any coding.
 
 #### Setup & Build
 1. Build project root wih ``./mvnw clean package``
-2. Go to client/target and find a runnable jar ``client-VERSION.jar``
+2. Go to client/target and find a runnable jar ``fit-connect-client.jar``
 3. Provide [config yaml](../config.yml):
    1. set environment variable ``FIT_CONNECT_CONFIG``:
       1. Linux/MacOS: ``export FIT_CONNECT_CONFIG=path/to/config.yml``
@@ -16,13 +16,13 @@ The sdk comes with a commandline client to be able to use the sdk without any co
       var senderClient = ClientFactory.senderClient(config);
       ````
    3. put ``config.yml`` in same path as the .jar-file 
-5. run client with ``java -jar client-VERSION.jar [COMMAND] [OPTIONS]``
+5. run client with ``java -jarfit-connect-client.jar [COMMAND] [OPTIONS]``
 
-#### SEND Example
+#### SEND Single Submission Example
 The send command submits a new submission to a destination. Apart from optional attachments, all options are mandatory.
 
 ````sh
-java -jar client-1.0-SNAPSHOT.jar send 
+java -jar fit-connect-client.jar send 
     --destinationId=1b7d1a24-a6c8-4050-bb71-ae3749ec432f 
     --serviceName=Test 
     --leikaKey=urn:de:fim:leika:leistung:99400048079000 
@@ -35,7 +35,7 @@ java -jar client-1.0-SNAPSHOT.jar send
 #### LIST Submissions Example
 The list command lists all submissionIds for a given destinationID.
 ````sh
-java -jar client-1.0-SNAPSHOT.jar list --destinationID=1b7d1a24-a6c8-4050-bb71-ae3749ec432f 
+java -jar fit-connect-client.jar list --destinationID=1b7d1a24-a6c8-4050-bb71-ae3749ec432f 
 ````
 #### GET Single Submission Example
 The get command loads a submission by `submissionId` and stores data and attachments in the given target location. If no target is
@@ -43,13 +43,13 @@ set, the cmd-client saves the data into in a folder named by the `submissionId`
 jar.
 
 ````sh
-java -jar client-1.0-SNAPSHOT.jar get --submissionID=cc9b9b3c-d4b1-4ac7-a70b-e7ca76e88608 --target=Users/submissions/data
+java -jar fit-connect-client.jar get --submissionID=cc9b9b3c-d4b1-4ac7-a70b-e7ca76e88608 --target=Users/submissions/data
 ````
 #### Batch Mode
 To send multiple submission with a single command, the client can be used in batch mode:
 
 ````sh
-java -jar client-1.0-SNAPSHOT.jar batch --data=batch_data.csv
+java -jar fit-connect-client.jar batch --data=batch_data.csv
 ````
 
 Currently, the import of CSV is supported. Follow the schema below, setting up your data:
@@ -112,95 +112,3 @@ Usage: <main class> [command] [command options]
         * --data
             Path to submission data as csv
 ````
-
-### API Flow
-
-The ClientFactory provides fluent API clients for both **Sender** and **Subscriber**.
-
-As the flow chart below shows, the fluent client guides through all essential calls in order to hand in a correct 
-**submission** as well as receive submissions on the subscriber side.
-
-#### Api client flow for sending a submission
-
-For the actual sender client those calls look like this:
-
-
-<table>
-<tr>
-<th>Workflow</th>
-<th>Java sample calls</th>
-</tr>
-<tr>
-<td>
-
-```mermaid
-
-flowchart TD
-
-A[Create Client] --> B(Add Attachments)
-A[Create Client] --> C(Add Data)
-B -->|next| C[Add Data]
-C -->|next| D[Add Destination]
-D -->|next| E[Add ServiceType]
-E -->|send| F[SubmissionForPickup]
-```
-Add Service Type
-</td>
-<td>
-
-```java
-
-ClientFactory.senderClient()
-        .withAttachments(attachments)
-        .withJsonData("{ caseSpecific: 'Data' }")
-        .withDestination(UUID.randomUUID())
-        .withServiceType("ServiceName", "leika:key:service")
-        .submit();
-```
-
-</td>
-</tr>
-</table>
-
-#### Api client flow for subscribing to a submission
-
-<table>
-<tr>
-<th>Workflow</th>
-<th>Java sample calls</th>
-</tr>
-<tr>
-<td>
-
-```mermaid
-flowchart TD
-
-A[Create Client] --> B(Poll List ofAvailable Submissions)
-A[Create Client] --> C(Request Submission by ID)
-
-C -->|get| D[Attachments]
-C -->|get| E[Metadata]
-C -->|get| F[Data]
-
-```
-
-</td>
-<td>
-
-```java
-
-    var client = ClientFactory.subscriberClient();
-
-    var submissions = client.getAvailableSubmissions(destinationId);
-    // filter submission list for requested one ...
-            
-    var submission = client.requestSubmission(submissionId)
-    submission.getAttachments();
-    submission.getMetadata();
-    submission.getData();
-```
-
-</td>
-</tr>
-</table>
-
diff --git a/client/src/main/java/dev/fitko/fitconnect/client/RoutingClient.java b/client/src/main/java/dev/fitko/fitconnect/client/RoutingClient.java
new file mode 100644
index 0000000000000000000000000000000000000000..f91e6e2956ab541239f57752b93247855fcb27f1
--- /dev/null
+++ b/client/src/main/java/dev/fitko/fitconnect/client/RoutingClient.java
@@ -0,0 +1,68 @@
+package dev.fitko.fitconnect.client;
+
+import dev.fitko.fitconnect.api.domain.model.route.Area;
+import dev.fitko.fitconnect.api.domain.model.route.Route;
+import dev.fitko.fitconnect.api.domain.model.route.RouteResult;
+import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
+import dev.fitko.fitconnect.api.exceptions.RestApiException;
+import dev.fitko.fitconnect.api.exceptions.RoutingException;
+import dev.fitko.fitconnect.api.services.routing.RoutingService;
+import dev.fitko.fitconnect.client.router.DestinationSearch;
+import dev.fitko.fitconnect.core.routing.RouteVerifier;
+
+import java.util.List;
+
+public final class RoutingClient {
+
+    private final RoutingService routingService;
+    private final RouteVerifier routeVerifier;
+
+    public RoutingClient(final RoutingService routingService, final RouteVerifier routeVerifier) {
+        this.routingService = routingService;
+        this.routeVerifier = routeVerifier;
+    }
+
+    /**
+     * Finds a list of {@link Route}s by a given service identifier and an area search criterion.
+     *
+     * @param search search parameters that contain leikaKey and one other area search criterion see {@link DestinationSearch}
+     * @return list of found {@link Route}s matching the search criteria
+     * @throws RoutingException if the signature validation failed
+     * @throws RestApiException if a technical error occurred during the query
+     */
+
+    public List<Route> findDestinations(final DestinationSearch search) throws RoutingException, RestApiException {
+        final RouteResult routeResult = routingService.getRoutes(search.getLeikaKey(), search.getArs(), search.getAgs(), search.getAreaId(), search.getOffset(), search.getLimit());
+        final ValidationResult result = routeVerifier.validateRouteDestinations(routeResult.getRoutes(), search.getLeikaKey(), search.getArs());
+        if (result.hasError()) {
+            throw new RoutingException(result.getError().getMessage(), result.getError());
+        }
+        return routeResult.getRoutes();
+    }
+
+    /**
+     * Finds a list of {@link Area}s based on a filter criterion, e.g. a zip code or city.
+     *
+     * @param filter filter criterion as string
+     * @param offset offset to start from
+     * @param limit  max entries
+     * @return list of {@link Area}
+     * @throws RestApiException if a technical error occurred during the query
+     */
+    public List<Area> findAreas(final String filter, final int offset, final int limit) throws RestApiException {
+        return findAreas(List.of(filter), offset, limit);
+    }
+
+    /**
+     * Find a list {@link Area}s based on a list of multiple filter criteria, e.g. a zip code or a city as in List.of("04229", "Leip*").
+     *
+     * @param filters list of string filters
+     * @param offset  offset to start from
+     * @param limit   max entries
+     * @return list of {@link Area}
+     * @throws RestApiException if a technical error occurred during the query
+     */
+    public List<Area> findAreas(final List<String> filters, final int offset, final int limit) throws RestApiException {
+        return routingService.getAreas(filters, offset, limit).getAreas();
+    }
+}
diff --git a/client/src/main/java/dev/fitko/fitconnect/client/SenderClient.java b/client/src/main/java/dev/fitko/fitconnect/client/SenderClient.java
index 72d563b702b3780a9f90d356f616d8ca1d2f22c2..523193e4379bd075df2c4a7e5dd5a517df80fe79 100644
--- a/client/src/main/java/dev/fitko/fitconnect/client/SenderClient.java
+++ b/client/src/main/java/dev/fitko/fitconnect/client/SenderClient.java
@@ -21,7 +21,7 @@ import java.util.Optional;
 import java.util.UUID;
 
 /**
- * A fluent client for announcing and handing in a {@link SubmitSubmission}
+ * A sender-side client for announcing and handing in a {@link SubmitSubmission}
  */
 public class SenderClient {
 
@@ -77,7 +77,7 @@ public class SenderClient {
      * @param timestamp      timestamp provided by the callback
      * @param httpBody       HTTP body provided by the callback
      * @param callbackSecret secret owned by the client, which is used to calculate the hmac
-     * @return {@code true} if hmac and timestamp provided by the callback meet the required conditions
+     * @return {@code ValidationResult.ok()} if hmac and timestamp provided by the callback meet the required conditions
      */
     public ValidationResult validateCallback(final String hmac, final Long timestamp, final String httpBody, final String callbackSecret) {
         return sender.validateCallback(hmac, timestamp, httpBody, callbackSecret);
diff --git a/client/src/main/java/dev/fitko/fitconnect/client/SubscriberClient.java b/client/src/main/java/dev/fitko/fitconnect/client/SubscriberClient.java
index a2caf86626e654eb3ae04eaadd8bf4d19ddec140..4c75814f737c024c901915f335fd93289a7d6b27 100644
--- a/client/src/main/java/dev/fitko/fitconnect/client/SubscriberClient.java
+++ b/client/src/main/java/dev/fitko/fitconnect/client/SubscriberClient.java
@@ -31,11 +31,10 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import java.util.UUID;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
-import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildDecryptedAttachmentPayload;
-import static dev.fitko.fitconnect.client.util.SubmissionUtil.getDataHashFromMetadata;
-import static dev.fitko.fitconnect.client.util.SubmissionUtil.mapToReceivedAttachments;
 
 /**
  * A subscriber-side client for retrieving submissions.
@@ -144,17 +143,27 @@ public class SubscriberClient {
         return null;
     }
 
-    public ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret) {
-        return this.subscriber.validateCallback(hmac, timestamp, httpBody, callbackSecret);
+    public ValidationResult validateCallback(final String hmac, final Long timestamp, final String httpBody, final String callbackSecret) {
+        return subscriber.validateCallback(hmac, timestamp, httpBody, callbackSecret);
     }
 
     private ReceivedSubmission buildReceivedSubmission(final Submission submission, final Metadata metadata, final byte[] decryptedData, final List<DecryptedAttachmentPayload> attachments) {
         final MimeType mimeType = metadata.getContentStructure().getData().getSubmissionSchema().getMimeType();
         final ReceivedData receivedData = new ReceivedData(new String(decryptedData, StandardCharsets.UTF_8), mimeType);
-        final List<ReceivedAttachment> receivedAttachments = mapToReceivedAttachments(attachments);
+        final List<ReceivedAttachment> receivedAttachments = attachments.stream().map(mapToReceivedAttachment()).collect(Collectors.toList());
         return new ReceivedSubmission(subscriber, submission, metadata, receivedData, receivedAttachments);
     }
 
+    private static Function<DecryptedAttachmentPayload, ReceivedAttachment> mapToReceivedAttachment() {
+        return payload -> ReceivedAttachment.builder()
+                .attachmentId(payload.getAttachmentMetadata().getAttachmentId())
+                .filename(payload.getAttachmentMetadata().getFilename())
+                .mimeType(payload.getAttachmentMetadata().getMimeType())
+                .description(payload.getAttachmentMetadata().getDescription())
+                .data(payload.getDecryptedContent())
+                .build();
+    }
+
     private List<DecryptedAttachmentPayload> loadAttachments(final UUID submissionId, final List<Attachment> attachmentMetadata) {
         if (attachmentMetadata == null || attachmentMetadata.isEmpty()) {
             LOGGER.info("Submission contains no attachments");
@@ -164,7 +173,11 @@ public class SubscriberClient {
         for (final Attachment metadata : attachmentMetadata) {
             final String encryptedAttachment = downloadAttachment(submissionId, metadata);
             final byte[] decryptedAttachment = decryptAttachment(metadata, encryptedAttachment);
-            receivedAttachments.add(buildDecryptedAttachmentPayload(metadata, decryptedAttachment));
+            final DecryptedAttachmentPayload decryptedAttachmentPayload = DecryptedAttachmentPayload.builder()
+                    .decryptedContent(decryptedAttachment)
+                    .attachmentMetadata(metadata)
+                    .build();
+            receivedAttachments.add(decryptedAttachmentPayload);
         }
         return receivedAttachments;
     }
@@ -207,4 +220,10 @@ public class SubscriberClient {
         return ValidationResult.ok();
     }
 
+    private String getDataHashFromMetadata(final Metadata metadata) {
+        return metadata.getContentStructure()
+                .getData()
+                .getHash()
+                .getContent();
+    }
 }
diff --git a/client/src/main/java/dev/fitko/fitconnect/client/factory/ApplicationConfigLoader.java b/client/src/main/java/dev/fitko/fitconnect/client/factory/ApplicationConfigLoader.java
index 706c23a45306ffa6b5f33512813dbf8606cfd89a..d6c67ce20d9cca49a4dbde93192007344e6c5b8c 100644
--- a/client/src/main/java/dev/fitko/fitconnect/client/factory/ApplicationConfigLoader.java
+++ b/client/src/main/java/dev/fitko/fitconnect/client/factory/ApplicationConfigLoader.java
@@ -52,11 +52,16 @@ public final class ApplicationConfigLoader {
     public static ApplicationConfig loadConfigFromYaml(final String configYaml) {
 
         final Yaml applicationPropertiesYaml = new Yaml(new Constructor(ApplicationConfig.class));
-        ApplicationConfig applicationConfig = applicationPropertiesYaml.load(configYaml);
+        final ApplicationConfig applicationConfig = applicationPropertiesYaml.load(configYaml);
 
         return applicationConfig;
     }
 
+    /**
+     * Load BuildInfo properties.
+     *
+     * @return {@link BuildInfo}
+     */
     public static BuildInfo loadBuildInfo() {
 
         final Yaml buildInfoYaml = new Yaml(new Constructor(BuildInfo.class));
diff --git a/client/src/main/java/dev/fitko/fitconnect/client/factory/ClientFactory.java b/client/src/main/java/dev/fitko/fitconnect/client/factory/ClientFactory.java
index 8877ce9bda3cc0e051e82f1432be5f8eafa7df36..cfdd4ee6d739976c8ef678ef0ca8c09470575669 100644
--- a/client/src/main/java/dev/fitko/fitconnect/client/factory/ClientFactory.java
+++ b/client/src/main/java/dev/fitko/fitconnect/client/factory/ClientFactory.java
@@ -6,6 +6,7 @@ import dev.fitko.fitconnect.api.config.ApplicationConfig;
 import dev.fitko.fitconnect.api.config.BuildInfo;
 import dev.fitko.fitconnect.api.config.SchemaConfig;
 import dev.fitko.fitconnect.api.config.SubscriberConfig;
+import dev.fitko.fitconnect.api.domain.schema.SchemaResources;
 import dev.fitko.fitconnect.api.exceptions.InitializationException;
 import dev.fitko.fitconnect.api.exceptions.InvalidKeyException;
 import dev.fitko.fitconnect.api.services.Sender;
@@ -13,13 +14,15 @@ import dev.fitko.fitconnect.api.services.Subscriber;
 import dev.fitko.fitconnect.api.services.auth.OAuthService;
 import dev.fitko.fitconnect.api.services.crypto.CryptoService;
 import dev.fitko.fitconnect.api.services.crypto.MessageDigestService;
-import dev.fitko.fitconnect.api.services.keys.KeyService;
 import dev.fitko.fitconnect.api.services.events.EventLogService;
 import dev.fitko.fitconnect.api.services.events.EventLogVerificationService;
 import dev.fitko.fitconnect.api.services.events.SecurityEventService;
+import dev.fitko.fitconnect.api.services.keys.KeyService;
+import dev.fitko.fitconnect.api.services.routing.RoutingService;
 import dev.fitko.fitconnect.api.services.schema.SchemaProvider;
 import dev.fitko.fitconnect.api.services.submission.SubmissionService;
 import dev.fitko.fitconnect.api.services.validation.ValidationService;
+import dev.fitko.fitconnect.client.RoutingClient;
 import dev.fitko.fitconnect.client.SenderClient;
 import dev.fitko.fitconnect.client.SubscriberClient;
 import dev.fitko.fitconnect.core.SubmissionSender;
@@ -27,11 +30,13 @@ import dev.fitko.fitconnect.core.SubmissionSubscriber;
 import dev.fitko.fitconnect.core.auth.DefaultOAuthService;
 import dev.fitko.fitconnect.core.crypto.HashService;
 import dev.fitko.fitconnect.core.crypto.JWECryptoService;
-import dev.fitko.fitconnect.core.keys.PublicKeyService;
 import dev.fitko.fitconnect.core.events.EventLogApiService;
 import dev.fitko.fitconnect.core.events.EventLogVerifier;
 import dev.fitko.fitconnect.core.events.SecurityEventTokenService;
-import dev.fitko.fitconnect.core.http.ProxyConfig;
+import dev.fitko.fitconnect.core.http.RestService;
+import dev.fitko.fitconnect.core.keys.PublicKeyService;
+import dev.fitko.fitconnect.core.routing.RouteVerifier;
+import dev.fitko.fitconnect.core.routing.RoutingApiService;
 import dev.fitko.fitconnect.core.schema.SchemaResourceProvider;
 import dev.fitko.fitconnect.core.submission.SubmissionApiService;
 import dev.fitko.fitconnect.core.validation.DefaultValidationService;
@@ -46,7 +51,7 @@ import java.text.ParseException;
 import java.util.List;
 
 /**
- * Factory that constructs clients for {@link Sender} and {@link Subscriber}.
+ * Factory that constructs {@link SenderClient}, {@link SubscriberClient} and {@link RoutingClient}.
  */
 public final class ClientFactory {
 
@@ -54,13 +59,15 @@ public final class ClientFactory {
 
     private static final String CONFIG_ENV_KEY_NAME = "FIT_CONNECT_CONFIG";
     private static final String SET_SCHEMA_DIR = "/set-schema";
+
+    private static final String DESTINATION_SCHEMA_DIR = "/destination-schema";
     private static final String METADATA_SCHEMA_DIR = "/metadata-schema";
 
     private ClientFactory() {
     }
 
     /**
-     * Create a new {@link SenderClient} that is automatically configured via the config.yml file.
+     * Create a new {@link SenderClient} to send submissions that is automatically configured via the config.yml file.
      *
      * @return the sender client
      */
@@ -69,7 +76,7 @@ public final class ClientFactory {
     }
 
     /**
-     * Create a new {@link SenderClient} that is automatically configured via a provided {@link ApplicationConfig}.
+     * Create a new {@link SenderClient} to send submissions that is automatically configured via a provided {@link ApplicationConfig}.
      *
      * @return the sender client
      */
@@ -80,7 +87,7 @@ public final class ClientFactory {
 
 
     /**
-     * Create a new {@link SubscriberClient} that is automatically configured via config.yml file.
+     * Create a new {@link SubscriberClient} to receive submissions that is automatically configured via config.yml file.
      *
      * @return the subscriber client
      */
@@ -89,7 +96,7 @@ public final class ClientFactory {
     }
 
     /**
-     * Create a new {@link SubscriberClient} that is automatically configured via a provided {@link ApplicationConfig}.
+     * Create a new {@link SubscriberClient} to receive submissions that is automatically configured via a provided {@link ApplicationConfig}.
      *
      * @return the subscriber client
      */
@@ -105,41 +112,73 @@ public final class ClientFactory {
         return new SubscriberClient(subscriber, privateKey);
     }
 
-    private static Subscriber getSubscriber(final ApplicationConfig config, final BuildInfo buildInfo) {
+    /**
+     * Create a new {@link RoutingClient} to find destinations and services that is automatically configured via a provided {@link ApplicationConfig}.
+     *
+     * @return the routing client
+     */
+    public static RoutingClient routingClient() {
+        return routingClient(loadConfig());
+    }
+
+    /**
+     * Create a new {@link RoutingClient} to find destinations and services that is automatically configured via a provided {@link ApplicationConfig}.
+     *
+     * @return the routing client
+     */
+    public static RoutingClient routingClient(final ApplicationConfig config) {
+        LOGGER.info("Initializing routing client ...");
+        final RestTemplate restTemplate = getRestTemplate(config, ApplicationConfigLoader.loadBuildInfo());
+        final SchemaProvider schemaProvider = getSchemaProvider();
+        final OAuthService authService = getSenderConfiguredAuthService(config, restTemplate);
+
+        final MessageDigestService messageDigestService = getMessageDigestService();
+        final ValidationService validator = getValidationService(config, schemaProvider, messageDigestService);
+        final SubmissionService submissionService = getSubmissionService(config, restTemplate, authService);
+        final KeyService keyService = getKeyService(config, restTemplate, authService, submissionService, validator);
+
+        final RouteVerifier routeVerifier = getRouteVerifier(keyService, validator);
+        final RoutingService routingService = getRoutingService(config, restTemplate);
+
+        return new RoutingClient(routingService, routeVerifier);
+    }
+
+    private static Sender getSender(final ApplicationConfig config, final BuildInfo buildInfo) {
         final RestTemplate restTemplate = getRestTemplate(config, buildInfo);
         final SchemaProvider schemaProvider = getSchemaProvider();
         final MessageDigestService messageDigestService = getMessageDigestService();
 
         final CryptoService cryptoService = getCryptoService(messageDigestService);
         final ValidationService validator = getValidationService(config, schemaProvider, messageDigestService);
-        final OAuthService authService = getSubscriberConfiguredAuthService(config, restTemplate);
+        final OAuthService authService = getSenderConfiguredAuthService(config, restTemplate);
 
         final SubmissionService submissionService = getSubmissionService(config, restTemplate, authService);
         final KeyService keyService = getKeyService(config, restTemplate, authService, submissionService, validator);
         final EventLogVerificationService eventLogVerifier = getEventLogVerifier(keyService, validator);
         final EventLogService eventLogService = getEventLogService(config, restTemplate, eventLogVerifier, authService);
-        final SecurityEventService setService = getSecurityEventTokenService(config, validator);
 
-        return new SubmissionSubscriber(submissionService, eventLogService, cryptoService, validator, setService);
+        return new SubmissionSender(submissionService, eventLogService, cryptoService, validator, keyService);
     }
 
-    private static Sender getSender(final ApplicationConfig config, final BuildInfo buildInfo) {
+    private static Subscriber getSubscriber(final ApplicationConfig config, final BuildInfo buildInfo) {
         final RestTemplate restTemplate = getRestTemplate(config, buildInfo);
         final SchemaProvider schemaProvider = getSchemaProvider();
         final MessageDigestService messageDigestService = getMessageDigestService();
 
         final CryptoService cryptoService = getCryptoService(messageDigestService);
         final ValidationService validator = getValidationService(config, schemaProvider, messageDigestService);
-        final OAuthService authService = getSenderConfiguredAuthService(config, restTemplate);
+        final OAuthService authService = getSubscriberConfiguredAuthService(config, restTemplate);
 
         final SubmissionService submissionService = getSubmissionService(config, restTemplate, authService);
         final KeyService keyService = getKeyService(config, restTemplate, authService, submissionService, validator);
         final EventLogVerificationService eventLogVerifier = getEventLogVerifier(keyService, validator);
         final EventLogService eventLogService = getEventLogService(config, restTemplate, eventLogVerifier, authService);
+        final SecurityEventService setService = getSecurityEventTokenService(config, validator);
 
-        return new SubmissionSender(submissionService, eventLogService, cryptoService, validator, keyService);
+        return new SubmissionSubscriber(submissionService, eventLogService, cryptoService, validator, setService);
     }
 
+
     private static OAuthService getSenderConfiguredAuthService(final ApplicationConfig config, final RestTemplate restTemplate) {
         final String clientId = config.getSenderConfig().getClientId();
         final String clientSecret = config.getSenderConfig().getClientSecret();
@@ -169,8 +208,8 @@ public final class ClientFactory {
     }
 
     private static RestTemplate getRestTemplate(final ApplicationConfig config, final BuildInfo buildInfo) {
-        final ProxyConfig proxyConfig = new ProxyConfig(config.getHttpProxyHost(), config.getHttpProxyPort(), buildInfo);
-        return proxyConfig.proxyRestTemplate();
+        final RestService restService = new RestService(config.getHttpProxyHost(), config.getHttpProxyPort(), buildInfo);
+        return restService.getRestTemplate();
     }
 
     private static MessageDigestService getMessageDigestService() {
@@ -193,9 +232,19 @@ public final class ClientFactory {
     }
 
     private static SchemaProvider getSchemaProvider() {
-        final List<String> metadataSchemaFiles = SchemaConfig.getMetadataSchemaFileNames(METADATA_SCHEMA_DIR);
         final List<String> setSchemaFiles = SchemaConfig.getSetSchemaFilePaths(SET_SCHEMA_DIR);
-        return new SchemaResourceProvider(setSchemaFiles, metadataSchemaFiles);
+        final List<String> metadataSchemaFiles = SchemaConfig.getMetadataSchemaFileNames(METADATA_SCHEMA_DIR);
+        final List<String> destinationSchemaFiles = SchemaConfig.getDestinationSchemaPaths(DESTINATION_SCHEMA_DIR);
+        final SchemaResources schemaResources = new SchemaResources(setSchemaFiles, metadataSchemaFiles, destinationSchemaFiles);
+        return new SchemaResourceProvider(schemaResources);
+    }
+
+    private static RoutingService getRoutingService(final ApplicationConfig config, final RestTemplate restTemplate) {
+        return new RoutingApiService(config, restTemplate);
+    }
+
+    private static RouteVerifier getRouteVerifier(final KeyService keyService, final ValidationService validationService) {
+        return new RouteVerifier(keyService, validationService);
     }
 
     private static RSAKey readRSAKeyFromString(final String key) {
diff --git a/client/src/main/java/dev/fitko/fitconnect/client/router/DestinationSearch.java b/client/src/main/java/dev/fitko/fitconnect/client/router/DestinationSearch.java
new file mode 100644
index 0000000000000000000000000000000000000000..fc9e37521e8a93ea0d7653291792988cb1b36818
--- /dev/null
+++ b/client/src/main/java/dev/fitko/fitconnect/client/router/DestinationSearch.java
@@ -0,0 +1,146 @@
+package dev.fitko.fitconnect.client.router;
+
+import lombok.AllArgsConstructor;
+import lombok.Value;
+
+import java.util.regex.Pattern;
+
+
+/**
+ * Builds a new search request for a service identifier and AT MAX. ONE OTHER search criterion (ars | ags | areaId).
+ */
+@Value
+@AllArgsConstructor
+public class DestinationSearch {
+
+    String leikaKey;
+    String ars;
+    String ags;
+    String areaId;
+
+    int offset;
+    int limit;
+
+    public static Builder Builder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+
+        private static final Pattern AGS_PATTERN = Pattern.compile("^(\\d{2}|\\d{3}|\\d{5}|\\d{8})$");
+        private static final Pattern ARS_PATTERN = Pattern.compile("^(\\d{2}|\\d{3}|\\d{5}|\\d{9}|\\d{12})$");
+        private static final Pattern AREA_ID_PATTERN = Pattern.compile("^\\d{1,}");
+        private static final Pattern LEIKA_KEY_PATTERN = Pattern.compile("^99\\d{12}$");
+
+        private String leikaKey;
+        private String ars;
+        private String ags;
+        private String areaId;
+        private int offset = 0;
+        private int limit = 100;
+
+        /**
+         * Leika Key of the requested service.
+         *
+         * @param leikaKey service identifier
+         * @return Builder
+         * @throws IllegalArgumentException if the leika key pattern is not matching
+         */
+        public Builder withLeikaKey(final String leikaKey) throws IllegalArgumentException {
+            if (!LEIKA_KEY_PATTERN.matcher(leikaKey).matches()) {
+                throw new IllegalArgumentException("Leika key does not match allowed pattern " + LEIKA_KEY_PATTERN);
+            }
+            this.leikaKey = leikaKey;
+            return this;
+        }
+
+        /**
+         * Official municipal code of the place.
+         *
+         * @param ars amtlicher regionalschlüssel
+         * @return Builder
+         * @throws IllegalArgumentException if the ars key pattern is not matching
+         */
+        public Builder withArs(final String ars) throws IllegalArgumentException{
+            if (!ARS_PATTERN.matcher(ars).matches()) {
+                throw new IllegalArgumentException("ARS key does not match allowed pattern " + ARS_PATTERN);
+            }
+            this.ars = ars;
+
+            return this;
+        }
+
+        /**
+         * Official regional key of the area.
+         *
+         * @param ags amtlicher gemeindeschlüssel
+         * @return Builder
+         * @throws IllegalArgumentException if the ags key pattern is not matching
+         */
+        public Builder withAgs(final String ags) throws IllegalArgumentException {
+            if (!AGS_PATTERN.matcher(ags).matches()) {
+                throw new IllegalArgumentException("AGS key does not match allowed pattern " + AGS_PATTERN);
+            }
+            this.ags = ags;
+            return this;
+        }
+
+        /**
+         * ID of the area. This ID can be determined via the routing clients <code>findAreas</code> search.
+         *
+         * @param areaId id of the area
+         * @return Builder
+         * @see dev.fitko.fitconnect.client.RoutingClient#findAreas(String, int, int)
+         * @throws IllegalArgumentException if the area id pattern is not matching
+         */
+        public Builder withAreaId(final String areaId) throws IllegalArgumentException {
+            if (!AREA_ID_PATTERN.matcher(areaId).matches()) {
+                throw new IllegalArgumentException("AreaId key does not match allowed pattern " + AREA_ID_PATTERN);
+            }
+            this.areaId = areaId;
+            return this;
+        }
+
+        /**
+         * Start position of the subset of the result set. Default is 0.
+         *
+         * @param offset start of the subset
+         * @return Builder
+         * @throws IllegalArgumentException if the offset is a negative number
+         */
+        public Builder withOffset(final int offset) throws IllegalArgumentException{
+            if (limit < 0) {
+                throw new IllegalArgumentException("offset must be positive");
+            }
+            this.offset = offset;
+            return this;
+        }
+
+        /**
+         * Max. size of the subset of the result set. Maximum is 500. Default is 100.
+         *
+         * @param limit max. entries in the subset
+         * @return Builder
+         * @throws IllegalArgumentException if the limit is > 500
+         */
+        public Builder withLimit(final int limit) throws IllegalArgumentException {
+            if (limit > 500) {
+                throw new IllegalArgumentException("limit must no be > 500");
+            }
+            this.limit = limit;
+            return this;
+        }
+
+        /**
+         * Construct the search request.
+         *
+         * @return DestinationSearch
+         */
+        public DestinationSearch build() throws IllegalArgumentException {
+            if(leikaKey == null){
+                throw new IllegalArgumentException("leikaKey is mandatory");
+            }
+            return new DestinationSearch(leikaKey, ars, ags, areaId, offset, limit);
+        }
+    }
+}
diff --git a/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendEncryptedSubmissionStrategy.java b/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendEncryptedSubmissionStrategy.java
index 00d872b95886b477b8ea98c063821e2eba4c7d66..b1dec39424d25c1c48a554f60c66a3c7df176caa 100644
--- a/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendEncryptedSubmissionStrategy.java
+++ b/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendEncryptedSubmissionStrategy.java
@@ -2,6 +2,7 @@ package dev.fitko.fitconnect.client.sender.strategies;
 
 import dev.fitko.fitconnect.api.domain.model.submission.CreateSubmission;
 import dev.fitko.fitconnect.api.domain.model.submission.SentSubmission;
+import dev.fitko.fitconnect.api.domain.model.submission.ServiceType;
 import dev.fitko.fitconnect.api.domain.model.submission.Submission;
 import dev.fitko.fitconnect.api.domain.model.submission.SubmitSubmission;
 import dev.fitko.fitconnect.api.exceptions.RestApiException;
@@ -14,12 +15,13 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
+import java.util.stream.Collectors;
 
+import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildCreateSubmission;
 import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSentSubmission;
-import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSubmissionToAnnounce;
 import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSubmitSubmission;
-import static dev.fitko.fitconnect.client.util.SubmissionUtil.toAttachmentPayloads;
 
 public class SendEncryptedSubmissionStrategy {
 
@@ -34,9 +36,9 @@ public class SendEncryptedSubmissionStrategy {
     public SentSubmission send(final EncryptedSubmissionPayload submissionPayload) {
 
         try {
-            final UUID submissionId = announceNewSubmission(submissionPayload);
-            final SubmitSubmission submitSubmission = buildSubmitSubmission(submissionId, submissionPayload);
             final List<AttachmentPayload> attachmentPayloads = toAttachmentPayloads(submissionPayload.getEncryptedAttachments());
+            final UUID submissionId = announceNewSubmission(submissionPayload, attachmentPayloads);
+            final SubmitSubmission submitSubmission = buildSubmitSubmission(submissionId, submissionPayload.getEncryptedData(), submissionPayload.getEncryptedMetadata());
 
             final var startTimeAttachmentUpload = StopWatch.start();
             uploadAttachments(attachmentPayloads, submissionId);
@@ -47,7 +49,7 @@ public class SendEncryptedSubmissionStrategy {
             LOGGER.info("Uploading submission took {}", StopWatch.stopWithFormattedTime(startTimeSubmissionUpload));
 
             LOGGER.info("SUCCESSFULLY HANDED IN SUBMISSION !");
-            return buildSentSubmission(submissionPayload, attachmentPayloads, submission);
+            return buildSentSubmission(attachmentPayloads, submissionPayload.getEncryptedData(), submissionPayload.getEncryptedData(), submission);
 
         } catch (final RestApiException e) {
             LOGGER.error("Sending submission failed", e);
@@ -58,8 +60,10 @@ public class SendEncryptedSubmissionStrategy {
         return null;
     }
 
-    private UUID announceNewSubmission(final EncryptedSubmissionPayload submissionPayload) {
-        final CreateSubmission submissionToAnnounce = buildSubmissionToAnnounce(submissionPayload);
+    private UUID announceNewSubmission(final EncryptedSubmissionPayload submissionPayload, final List<AttachmentPayload> attachmentPayloads) {
+        final UUID destinationId = submissionPayload.getDestinationId();
+        final ServiceType serviceType = submissionPayload.getServiceType();
+        final CreateSubmission submissionToAnnounce = buildCreateSubmission(destinationId, attachmentPayloads, serviceType);
         return sender.createSubmission(submissionToAnnounce).getSubmissionId();
     }
 
@@ -71,4 +75,14 @@ public class SendEncryptedSubmissionStrategy {
             attachmentPayloads.forEach(a -> sender.uploadAttachment(submissionId, a.getAttachmentId(), a.getEncryptedData()));
         }
     }
+
+    private List<AttachmentPayload> toAttachmentPayloads(final Map<UUID, String> encryptedAttachments) {
+        return encryptedAttachments.entrySet()
+                .stream()
+                .map(attachment -> AttachmentPayload.builder()
+                        .encryptedData(attachment.getValue())
+                        .attachmentId(attachment.getKey())
+                        .build())
+                .collect(Collectors.toList());
+    }
 }
diff --git a/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendNewSubmissionStrategy.java b/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendNewSubmissionStrategy.java
index 9df636fb65fd6f633d57ab953170ac0ff3cd56ae..162fef4271a78cb7cd522aa03fb5a26b655a245d 100644
--- a/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendNewSubmissionStrategy.java
+++ b/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendNewSubmissionStrategy.java
@@ -2,7 +2,14 @@ package dev.fitko.fitconnect.client.sender.strategies;
 
 import com.nimbusds.jose.jwk.RSAKey;
 import dev.fitko.fitconnect.api.domain.model.destination.Destination;
+import dev.fitko.fitconnect.api.domain.model.destination.DestinationService;
+import dev.fitko.fitconnect.api.domain.model.metadata.ContentStructure;
+import dev.fitko.fitconnect.api.domain.model.metadata.Hash;
 import dev.fitko.fitconnect.api.domain.model.metadata.Metadata;
+import dev.fitko.fitconnect.api.domain.model.metadata.PublicServiceType;
+import dev.fitko.fitconnect.api.domain.model.metadata.SignatureType;
+import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment;
+import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Purpose;
 import dev.fitko.fitconnect.api.domain.model.metadata.data.Data;
 import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
 import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema;
@@ -34,20 +41,18 @@ import java.net.URLConnection;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Paths;
+import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
-import java.util.Optional;
 import java.util.UUID;
+
 import java.util.stream.Collectors;
 
-import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildMetadataToSend;
+import static dev.fitko.fitconnect.api.config.SchemaConfig.METADATA_V_1_0_0;
+import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildCreateSubmission;
 import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSentSubmission;
-import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSubmissionToAnnounce;
 import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSubmitSubmission;
-import static dev.fitko.fitconnect.client.util.SubmissionUtil.createData;
-import static dev.fitko.fitconnect.client.util.SubmissionUtil.getFileAttachmentPayload;
-import static dev.fitko.fitconnect.client.util.SubmissionUtil.getSchemaUriForMimeType;
-import static dev.fitko.fitconnect.client.util.SubmissionUtil.getSubmissionSchemasFromDestination;
+
 
 public class SendNewSubmissionStrategy {
 
@@ -70,7 +75,7 @@ public class SendNewSubmissionStrategy {
             final Destination destination = sender.getDestination(destinationId);
 
             final List<AttachmentPayload> encryptedAttachments = encryptAndHashAttachments(encryptionKey, submissionPayload.getAttachments());
-            final CreateSubmission newSubmission = buildSubmissionToAnnounce(destinationId, serviceType, encryptedAttachments);
+            final CreateSubmission newSubmission = buildCreateSubmission(destinationId, encryptedAttachments, serviceType);
 
             final SubmissionForPickup announcedSubmission = sender.createSubmission(newSubmission);
             final UUID announcedSubmissionId = announcedSubmission.getSubmissionId();
@@ -119,20 +124,74 @@ public class SendNewSubmissionStrategy {
 
     private Metadata buildMetadata(final SubmissionPayload submissionPayload, final Destination destination, final List<AttachmentPayload> encryptedAttachments) {
         final String hashedData = sender.createHash(submissionPayload.getData().getBytes(StandardCharsets.UTF_8));
-        final URI schemaUri = getSchemaUri(submissionPayload.getDataMimeType(), destination);
-        final Data data = createData(submissionPayload, hashedData, schemaUri);
-        return buildMetadataToSend(data, submissionPayload.getServiceType(), encryptedAttachments);
+
+        final MimeType dataMimeType = submissionPayload.getDataMimeType();
+        final URI schemaUri = getSubmissionSchemaFromDestination(destination, dataMimeType);
+        final Data data = createData(dataMimeType, hashedData, schemaUri);
+        final PublicServiceType publicServiceType = buildPublicServiceType(submissionPayload.getServiceType());
+        final List<Attachment> attachmentMetadata = encryptedAttachments.stream().map(this::toHashedAttachment).collect(Collectors.toList());
+
+        final var contentStructure = new ContentStructure();
+        contentStructure.setAttachments(attachmentMetadata);
+        contentStructure.setData(data);
+
+        final var metadata = new Metadata();
+        metadata.setSchema(METADATA_V_1_0_0.toString());
+        metadata.setContentStructure(contentStructure);
+        metadata.setPublicServiceType(publicServiceType);
+
+        return metadata;
+    }
+
+    private Data createData(final MimeType mimeType, final String hashedData, final URI schemaUri) {
+        final var hash = new Hash();
+        hash.setContent(hashedData);
+        hash.setSignatureType(SignatureType.SHA_512);
+
+        final var submissionSchema = new SubmissionSchema();
+        submissionSchema.setMimeType(mimeType);
+        submissionSchema.setSchemaUri(schemaUri);
+
+        final var data = new Data();
+        data.setSubmissionSchema(submissionSchema);
+        data.setHash(hash);
+        return data;
     }
 
-    private URI getSchemaUri(final MimeType mimeType, final Destination destination) {
-        final List<SubmissionSchema> submissionSchemas = getSubmissionSchemasFromDestination(destination);
-        final String supportedSchemas = submissionSchemas.stream().map(s -> s.getMimeType().toString()).collect(Collectors.joining(" | "));
-        final Optional<URI> schemaUriForMimeType = getSchemaUriForMimeType(submissionSchemas, mimeType);
-        if (schemaUriForMimeType.isEmpty()) {
-            LOGGER.error("Destination supports the mime-type '{}'. Mime-type {} is not allowed, please check the destination", supportedSchemas, mimeType);
-            throw new SchemaNotFoundException("Schema for mime-type " + mimeType + " not found");
+    private PublicServiceType buildPublicServiceType(final ServiceType serviceType) {
+        final var publicServiceType = new PublicServiceType();
+        publicServiceType.setIdentifier(serviceType.getIdentifier());
+        if (serviceType.getName() != null) {
+            publicServiceType.setName(serviceType.getName());
         }
-        return schemaUriForMimeType.get();
+        if (serviceType.getDescription() != null) {
+            publicServiceType.setDescription(serviceType.getDescription());
+        }
+        return publicServiceType;
+    }
+
+    private Attachment toHashedAttachment(final AttachmentPayload attachmentPayload) {
+        final var attachment = new Attachment();
+        attachment.setAttachmentId(attachmentPayload.getAttachmentId());
+        attachment.setPurpose(Purpose.ATTACHMENT);
+        attachment.setFilename(attachmentPayload.getFile().getName());
+        attachment.setMimeType(attachmentPayload.getMimeType());
+
+        final var hash = new Hash();
+        hash.setContent(attachmentPayload.getHashedData());
+        hash.setSignatureType(SignatureType.SHA_512);
+        attachment.setHash(hash);
+        return attachment;
+    }
+
+    private URI getSubmissionSchemaFromDestination(final Destination destination, final MimeType mimeType) {
+        return destination.getServices().stream()
+                .map(DestinationService::getSubmissionSchemas)
+                .flatMap(Collection::stream)
+                .filter(schema -> schema.getMimeType().equals(mimeType))
+                .map(SubmissionSchema::getSchemaUri)
+                .findFirst()
+                .orElseThrow(() -> new SchemaNotFoundException("Destination does not support data with mime-type " + mimeType));
     }
 
     private void uploadAttachments(final List<AttachmentPayload> attachmentPayloads, final UUID submissionId) {
@@ -146,7 +205,7 @@ public class SendNewSubmissionStrategy {
 
     private List<AttachmentPayload> encryptAndHashAttachments(final RSAKey encryptionKey, final List<File> attachments) {
         return attachments.stream()
-                .map(getFileAttachmentPayload())
+                .map(file -> AttachmentPayload.builder().file(file).attachmentId(UUID.randomUUID()).build())
                 .filter(Objects::nonNull)
                 .map(payload -> encryptAndHashAttachment(encryptionKey, payload))
                 .collect(Collectors.toList());
@@ -160,12 +219,13 @@ public class SendNewSubmissionStrategy {
             final String hashedBytes = sender.createHash(rawData);
             return attachmentPayload.withEncryptedData(encryptedAttachment)
                     .withHashedData(hashedBytes)
-                    .withMimeType(setMimeType(attachmentPayload));
+                    .withMimeType(detectMimeTypeFromFile(attachmentPayload));
         } catch (final Exception e) {
             throw new AttachmentCreationException("Attachment '" + file.getAbsolutePath() + "' could not be created ", e);
         }
     }
-    private String setMimeType(final AttachmentPayload attachmentPayload) {
+
+    private String detectMimeTypeFromFile(final AttachmentPayload attachmentPayload) {
         final File file = attachmentPayload.getFile();
         String mimeType;
         try {
diff --git a/client/src/main/java/dev/fitko/fitconnect/client/util/SubmissionUtil.java b/client/src/main/java/dev/fitko/fitconnect/client/util/SubmissionUtil.java
index 45acaa70fa6dd44207bb419e18a67169d5a4212b..da952802fbdf83007896c337b2f52538fe4e2036 100644
--- a/client/src/main/java/dev/fitko/fitconnect/client/util/SubmissionUtil.java
+++ b/client/src/main/java/dev/fitko/fitconnect/client/util/SubmissionUtil.java
@@ -1,88 +1,36 @@
 package dev.fitko.fitconnect.client.util;
 
 import com.nimbusds.jose.JWEObject;
-import dev.fitko.fitconnect.api.domain.model.destination.Destination;
-import dev.fitko.fitconnect.api.domain.model.destination.DestinationService;
 import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags;
-import dev.fitko.fitconnect.api.domain.model.metadata.*;
-import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment;
-import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Purpose;
-import dev.fitko.fitconnect.api.domain.model.metadata.data.Data;
-import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
-import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema;
-import dev.fitko.fitconnect.api.domain.model.submission.*;
+import dev.fitko.fitconnect.api.domain.model.submission.CreateSubmission;
+import dev.fitko.fitconnect.api.domain.model.submission.SentSubmission;
+import dev.fitko.fitconnect.api.domain.model.submission.ServiceType;
+import dev.fitko.fitconnect.api.domain.model.submission.Submission;
+import dev.fitko.fitconnect.api.domain.model.submission.SubmitSubmission;
 import dev.fitko.fitconnect.client.sender.model.AttachmentPayload;
-import dev.fitko.fitconnect.client.sender.model.EncryptedSubmissionPayload;
-import dev.fitko.fitconnect.client.sender.model.SubmissionPayload;
-import dev.fitko.fitconnect.client.subscriber.model.DecryptedAttachmentPayload;
-import dev.fitko.fitconnect.client.subscriber.model.ReceivedAttachment;
 
-import java.io.File;
-import java.net.URI;
 import java.text.ParseException;
-import java.util.*;
-import java.util.function.Function;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
 import java.util.stream.Collectors;
 
-import static dev.fitko.fitconnect.api.config.SchemaConfig.METADATA_V_1_0_0;
-
+/**
+ * Helper to create types for announced, sent and submit submissions.
+ */
 public final class SubmissionUtil {
 
     private SubmissionUtil() {
     }
 
-    private static CreateSubmission createSubmission(final UUID destinationId, final ServiceType serviceType, final List<UUID> attachmentIdsToAnnounce) {
+    public static CreateSubmission buildCreateSubmission(final UUID destinationId, final List<AttachmentPayload> attachments, final ServiceType serviceType) {
         return CreateSubmission.builder()
                 .destinationId(destinationId)
-                .announcedAttachments(attachmentIdsToAnnounce)
+                .announcedAttachments(attachments.stream().map(AttachmentPayload::getAttachmentId).collect(Collectors.toList()))
                 .serviceType(serviceType)
                 .build();
     }
 
-    public static PublicServiceType buildPublicServiceType(final ServiceType serviceType) {
-        final var publicServiceType = new PublicServiceType();
-        publicServiceType.setIdentifier(serviceType.getIdentifier());
-        if (serviceType.getName() != null) {
-            publicServiceType.setName(serviceType.getName());
-        }
-        if (serviceType.getDescription() != null) {
-            publicServiceType.setDescription(serviceType.getDescription());
-        }
-        return publicServiceType;
-    }
-
-    public static Metadata buildMetadataToSend(final Data data, final ServiceType serviceType, final List<AttachmentPayload> encryptedAttachments) {
-        final PublicServiceType publicServiceType = buildPublicServiceType(serviceType);
-        final List<Attachment> attachmentMetadata = toAttachmentMetadata(encryptedAttachments);
-        return buildMetadata(attachmentMetadata, data, publicServiceType);
-    }
-
-    public static Metadata buildMetadata(final List<Attachment> attachments, final Data data, final PublicServiceType publicServiceType) {
-        final var contentStructure = new ContentStructure();
-        contentStructure.setAttachments(attachments);
-        contentStructure.setData(data);
-
-        final var metadata = new Metadata();
-        metadata.setSchema(METADATA_V_1_0_0.toString());
-        metadata.setContentStructure(contentStructure);
-        metadata.setPublicServiceType(publicServiceType);
-        return metadata;
-    }
-
-    public static CreateSubmission buildSubmissionToAnnounce(final EncryptedSubmissionPayload submissionPayload) {
-        final ServiceType serviceType = submissionPayload.getServiceType();
-        final List<UUID> attachmentIds = new ArrayList<>(submissionPayload.getEncryptedAttachments().keySet());
-        return createSubmission(submissionPayload.getDestinationId(), serviceType, attachmentIds);
-    }
-
-    public static SubmitSubmission buildSubmitSubmission(final UUID submissionId, final EncryptedSubmissionPayload submissionPayload) {
-        final SubmitSubmission submission = new SubmitSubmission();
-        submission.setSubmissionId(submissionId);
-        submission.setEncryptedData(submissionPayload.getEncryptedData());
-        submission.setEncryptedMetadata(submissionPayload.getEncryptedMetadata());
-        return submission;
-    }
-
     public static SubmitSubmission buildSubmitSubmission(final UUID submissionId, final String encryptedData, final String encryptedMetadata) {
         final SubmitSubmission submission = new SubmitSubmission();
         submission.setSubmissionId(submissionId);
@@ -91,83 +39,6 @@ public final class SubmissionUtil {
         return submission;
     }
 
-    private static Attachment toHashedAttachment(final AttachmentPayload attachmentPayload) {
-        final var attachment = new Attachment();
-        attachment.setAttachmentId(attachmentPayload.getAttachmentId());
-        attachment.setPurpose(Purpose.ATTACHMENT);
-        attachment.setFilename(attachmentPayload.getFile().getName());
-        attachment.setMimeType(attachmentPayload.getMimeType());
-
-        final var hash = new Hash();
-        hash.setContent(attachmentPayload.getHashedData());
-        hash.setSignatureType(SignatureType.SHA_512);
-        attachment.setHash(hash);
-        return attachment;
-    }
-
-    private static List<UUID> toAttachmentIds(final List<AttachmentPayload> attachmentPayloads) {
-        return attachmentPayloads.stream()
-                .map(AttachmentPayload::getAttachmentId)
-                .collect(Collectors.toList());
-    }
-
-    public static List<Attachment> toAttachmentMetadata(final List<AttachmentPayload> attachmentPayloads) {
-        return attachmentPayloads.stream()
-                .map(SubmissionUtil::toHashedAttachment)
-                .collect(Collectors.toList());
-    }
-
-    private static Function<Map.Entry<UUID, String>, AttachmentPayload> getEncryptedAttachmentPayload() {
-        return attachment -> AttachmentPayload.builder()
-                .encryptedData(attachment.getValue())
-                .attachmentId(attachment.getKey())
-                .build();
-    }
-
-    public static Function<File, AttachmentPayload> getFileAttachmentPayload() {
-        return file -> AttachmentPayload.builder()
-                .file(file)
-                .attachmentId(UUID.randomUUID())
-                .build();
-    }
-
-    public static List<AttachmentPayload> toAttachmentPayloads(final Map<UUID, String> encryptedAttachments) {
-        return encryptedAttachments.entrySet()
-                .stream()
-                .map(getEncryptedAttachmentPayload())
-                .collect(Collectors.toList());
-    }
-
-    public static CreateSubmission buildSubmissionToAnnounce(final UUID destinationId, final ServiceType serviceType, final List<AttachmentPayload> encryptedAttachments) {
-        final List<UUID> attachmentIdsToAnnounce = toAttachmentIds(encryptedAttachments);
-        return createSubmission(destinationId, serviceType, attachmentIdsToAnnounce);
-    }
-
-    public static List<SubmissionSchema> getSubmissionSchemasFromDestination(final Destination destination) {
-        return destination.getServices().stream()
-                .map(DestinationService::getSubmissionSchemas)
-                .flatMap(Collection::stream)
-                .collect(Collectors.toList());
-    }
-
-    public static Optional<URI> getSchemaUriForMimeType(final List<SubmissionSchema> submissionSchemas, final MimeType mimeType) {
-        return submissionSchemas.stream()
-                .filter(schema -> schema.getMimeType().equals(mimeType))
-                .map(SubmissionSchema::getSchemaUri)
-                .findFirst();
-    }
-
-    public static DecryptedAttachmentPayload buildDecryptedAttachmentPayload(final Attachment attachmentMetadata, final byte[] decryptedAttachment) {
-        return DecryptedAttachmentPayload.builder()
-                .decryptedContent(decryptedAttachment)
-                .attachmentMetadata(attachmentMetadata)
-                .build();
-    }
-
-    public static SentSubmission buildSentSubmission(final EncryptedSubmissionPayload submissionPayload, final List<AttachmentPayload> encryptedAttachments, final Submission submission) {
-        return buildSentSubmission(encryptedAttachments, submissionPayload.getEncryptedData(), submissionPayload.getEncryptedMetadata(), submission);
-    }
-
     public static SentSubmission buildSentSubmission(final List<AttachmentPayload> encryptedAttachments, final String encryptedData, final String encryptedMetadata, final Submission submission) {
 
         final AuthenticationTags authenticationTags = new AuthenticationTags();
@@ -183,45 +54,6 @@ public final class SubmissionUtil {
                 .build();
     }
 
-    public static List<ReceivedAttachment> mapToReceivedAttachments(final List<DecryptedAttachmentPayload> decryptedAttachmentPayloads) {
-        return decryptedAttachmentPayloads.stream()
-                .map(SubmissionUtil::mapToReceivedAttachment)
-                .collect(Collectors.toList());
-    }
-
-    public static Data createData(final SubmissionPayload submissionPayload, final String hashedData, final URI schemaUri) {
-        final var hash = new Hash();
-        hash.setContent(hashedData);
-        hash.setSignatureType(SignatureType.SHA_512);
-
-        final var submissionSchema = new SubmissionSchema();
-        submissionSchema.setMimeType(submissionPayload.getDataMimeType());
-        submissionSchema.setSchemaUri(schemaUri);
-
-        final var data = new Data();
-        data.setSubmissionSchema(submissionSchema);
-        data.setHash(hash);
-        return data;
-    }
-
-    public static String getDataHashFromMetadata(final Metadata metadata) {
-        return metadata.getContentStructure()
-                .getData()
-                .getHash()
-                .getContent();
-    }
-
-    private static ReceivedAttachment mapToReceivedAttachment(final DecryptedAttachmentPayload payload) {
-        final Attachment metadata = payload.getAttachmentMetadata();
-        return ReceivedAttachment.builder()
-                .attachmentId(metadata.getAttachmentId())
-                .filename(metadata.getFilename())
-                .mimeType(metadata.getMimeType())
-                .description(metadata.getDescription())
-                .data(payload.getDecryptedContent())
-                .build();
-    }
-
     private static Map<UUID, String> getAuthTagsFromAttachments(final List<AttachmentPayload> encryptedAttachments) {
         return encryptedAttachments.stream().collect(Collectors.toMap(AttachmentPayload::getAttachmentId, SubmissionUtil::getAuthTag));
     }
@@ -237,5 +69,4 @@ public final class SubmissionUtil {
             return "";
         }
     }
-
 }
diff --git a/client/src/main/java/dev/fitko/fitconnect/client/util/ValidDataGuard.java b/client/src/main/java/dev/fitko/fitconnect/client/util/ValidDataGuard.java
index b714248a674ef1fcf8772dceb74f14f3ba267013..de8fb13f7198c2374c1633a9a06e0c43755f991f 100644
--- a/client/src/main/java/dev/fitko/fitconnect/client/util/ValidDataGuard.java
+++ b/client/src/main/java/dev/fitko/fitconnect/client/util/ValidDataGuard.java
@@ -1,20 +1,19 @@
 package dev.fitko.fitconnect.client.util;
 
+import dev.fitko.fitconnect.api.domain.model.destination.DestinationService;
 import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
 import dev.fitko.fitconnect.api.domain.model.submission.ServiceType;
 import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
 import dev.fitko.fitconnect.api.services.Sender;
 import dev.fitko.fitconnect.client.sender.model.EncryptedSubmissionPayload;
 import dev.fitko.fitconnect.client.sender.model.SubmissionPayload;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
+import java.util.Objects;
 import java.util.UUID;
 import java.util.regex.Pattern;
 
 public class ValidDataGuard {
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(ValidDataGuard.class);
     private static final Pattern LEIKA_KEY_PATTERN = Pattern.compile("^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,.:=@;$_!*'%/?#-]+$");
     private final Sender sender;
 
@@ -22,57 +21,87 @@ public class ValidDataGuard {
         this.sender = sender;
     }
 
+    /**
+     * Checks if the encrypted submission payload is in a valid state for sending.
+     *
+     * @param encryptedSubmissionPayload payload to be checked
+     * @throws IllegalArgumentException if one of the checks fails
+     * @throws IllegalStateException    if the destination does not have a declared service type
+     */
     public void ensureValidDataPayload(final EncryptedSubmissionPayload encryptedSubmissionPayload) {
         if (encryptedSubmissionPayload == null) {
             throw new IllegalArgumentException("Encrypted payload must not be null.");
         }
+        if (encryptedSubmissionPayload.getEncryptedData() == null) {
+            throw new IllegalArgumentException("Encrypted data is mandatory, but was null.");
+        }
         if (encryptedSubmissionPayload.getEncryptedMetadata() == null) {
             throw new IllegalArgumentException("Encrypted metadata must not be null.");
         }
-        testDefaults(encryptedSubmissionPayload.getDestinationId(), encryptedSubmissionPayload.getEncryptedData(), encryptedSubmissionPayload.getServiceType());
+        testDefaults(encryptedSubmissionPayload.getDestinationId(), encryptedSubmissionPayload.getServiceType());
     }
 
+    /**
+     * Checks if the unencrypted submission payload is in a valid state for sending.
+     *
+     * @param submissionPayload payload to be checked
+     * @throws IllegalArgumentException if one of the checks fails
+     */
     public void ensureValidDataPayload(final SubmissionPayload submissionPayload) {
         if (submissionPayload == null) {
             throw new IllegalArgumentException("Payload must not be null.");
         }
-        if (isDataFormatInvalid(submissionPayload)) {
-            throw new IllegalArgumentException("Data format is invalid, please provide well-formed data.");
+        if (submissionPayload.getData() == null) {
+            throw new IllegalArgumentException("Data is mandatory, but was null.");
         }
-        testDefaults(submissionPayload.getDestinationId(), submissionPayload.getData(), submissionPayload.getServiceType());
+        testOnValidDataFormat(submissionPayload);
+        testDefaults(submissionPayload.getDestinationId(), submissionPayload.getServiceType());
     }
 
-    public void testDefaults(final UUID destinationId, final String data, final ServiceType serviceType) {
+    private void testDefaults(final UUID destinationId, final ServiceType serviceType) {
         if (destinationId == null) {
             throw new IllegalArgumentException("DestinationId is mandatory, but was null.");
-        } else if (data == null) {
-            throw new IllegalArgumentException("Data is mandatory, but was null.");
         } else if (serviceType == null) {
             throw new IllegalArgumentException("ServiceType is mandatory, but was null.");
         } else if (serviceType.getIdentifier() == null) {
             throw new IllegalArgumentException("Leika key is mandatory, but was null.");
         } else if (noValidLeikaKeyPattern(serviceType.getIdentifier())) {
             throw new IllegalArgumentException("LeikaKey has invalid format, please follow: ^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,.:=@;$_!*'%/?#-]+$.");
+        } else if (serviceTypeDoesNotMatchDestination(destinationId, serviceType)) {
+            throw new IllegalArgumentException("Provided service type '" + serviceType.getIdentifier() + "' is not allowed by the destination ");
         }
     }
 
-    private boolean isDataFormatInvalid(final SubmissionPayload submissionPayload) {
+    private boolean serviceTypeDoesNotMatchDestination(final UUID destinationId, final ServiceType serviceType) {
+        return sender.getDestination(destinationId).getServices().stream()
+                .map(DestinationService::getIdentifier)
+                .filter(Objects::nonNull)
+                .filter(serviceIdentifier -> serviceIdentifier.equals(serviceType.getIdentifier()))
+                .findFirst()
+                .isEmpty();
+    }
 
+    private void testOnValidDataFormat(final SubmissionPayload submissionPayload) {
         final MimeType dataMimeType = submissionPayload.getDataMimeType();
         if (dataMimeType.equals(MimeType.APPLICATION_JSON)) {
-            final ValidationResult validationResult = sender.validateJsonFormat(submissionPayload.getData());
-            if (validationResult.hasError()) {
-                LOGGER.error("Data is not in expected json format, please provide valid json {}", validationResult.getError().getMessage());
-                return true;
-            }
+            checkJsonFormat(submissionPayload);
         } else if (dataMimeType.equals(MimeType.APPLICATION_XML)) {
-            final ValidationResult validationResult = sender.validateXmlFormat(submissionPayload.getData());
-            if (validationResult.hasError()) {
-                LOGGER.error("Data is not in expected xml format, please provide valid xml {}", validationResult.getError().getMessage());
-                return true;
-            }
+            checkXmlFormat(submissionPayload);
+        }
+    }
+
+    private void checkXmlFormat(final SubmissionPayload submissionPayload) {
+        final ValidationResult validationResult = sender.validateXmlFormat(submissionPayload.getData());
+        if (validationResult.hasError()) {
+            throw new IllegalArgumentException("Data is not in expected xml format, please provide valid xml: " + validationResult.getError().getMessage());
+        }
+    }
+
+    private void checkJsonFormat(final SubmissionPayload submissionPayload) {
+        final ValidationResult validationResult = sender.validateJsonFormat(submissionPayload.getData());
+        if (validationResult.hasError()) {
+            throw new IllegalArgumentException("Data is not in expected json format, please provide valid json: " + validationResult.getError().getMessage());
         }
-        return false;
     }
 
     private boolean noValidLeikaKeyPattern(final String leikaKey) {
diff --git a/client/src/test/java/dev/fitko/fitconnect/client/ClientIntegrationTest.java b/client/src/test/java/dev/fitko/fitconnect/client/ClientIntegrationTest.java
index dcdf68c90b4be23a2cf604280601063ad0762867..bd74e818eb4d0418cc64af235d1cd8bf2c8bf035 100644
--- a/client/src/test/java/dev/fitko/fitconnect/client/ClientIntegrationTest.java
+++ b/client/src/test/java/dev/fitko/fitconnect/client/ClientIntegrationTest.java
@@ -2,37 +2,46 @@ package dev.fitko.fitconnect.client;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jose.jwk.RSAKey;
-import dev.fitko.fitconnect.api.config.*;
+import dev.fitko.fitconnect.api.config.ApplicationConfig;
+import dev.fitko.fitconnect.api.config.BuildInfo;
+import dev.fitko.fitconnect.api.config.Environment;
+import dev.fitko.fitconnect.api.config.EnvironmentName;
+import dev.fitko.fitconnect.api.config.SenderConfig;
+import dev.fitko.fitconnect.api.config.SubscriberConfig;
 import dev.fitko.fitconnect.api.domain.auth.OAuthToken;
 import dev.fitko.fitconnect.api.domain.model.event.Event;
 import dev.fitko.fitconnect.api.domain.model.event.EventLogEntry;
 import dev.fitko.fitconnect.api.domain.model.event.EventStatus;
 import dev.fitko.fitconnect.api.domain.model.event.problems.submission.InvalidEventLog;
+import dev.fitko.fitconnect.api.domain.model.metadata.ContentStructure;
 import dev.fitko.fitconnect.api.domain.model.metadata.Hash;
 import dev.fitko.fitconnect.api.domain.model.metadata.Metadata;
 import dev.fitko.fitconnect.api.domain.model.metadata.PublicServiceType;
 import dev.fitko.fitconnect.api.domain.model.metadata.SignatureType;
 import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment;
+import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Purpose;
 import dev.fitko.fitconnect.api.domain.model.metadata.data.Data;
 import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
 import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema;
-import dev.fitko.fitconnect.api.domain.model.submission.ServiceType;
+import dev.fitko.fitconnect.api.domain.model.route.Area;
+import dev.fitko.fitconnect.api.domain.model.route.Route;
 import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup;
+import dev.fitko.fitconnect.api.exceptions.RestApiException;
 import dev.fitko.fitconnect.api.services.crypto.CryptoService;
 import dev.fitko.fitconnect.client.factory.ClientFactory;
+import dev.fitko.fitconnect.client.router.DestinationSearch;
 import dev.fitko.fitconnect.client.sender.EncryptedSubmissionBuilder;
 import dev.fitko.fitconnect.client.sender.SubmissionBuilder;
 import dev.fitko.fitconnect.client.sender.model.AttachmentPayload;
 import dev.fitko.fitconnect.client.subscriber.ReceivedSubmission;
-import dev.fitko.fitconnect.client.util.SubmissionUtil;
 import dev.fitko.fitconnect.core.auth.DefaultOAuthService;
 import dev.fitko.fitconnect.core.crypto.HashService;
 import dev.fitko.fitconnect.core.crypto.JWECryptoService;
+import dev.fitko.fitconnect.core.http.RestService;
 import org.apache.tika.mime.MimeTypes;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
-import org.springframework.web.client.RestTemplate;
 
 import java.io.File;
 import java.io.IOException;
@@ -41,23 +50,35 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.text.ParseException;
 import java.time.Duration;
-import java.util.*;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
 import java.util.stream.Collectors;
 
+import static dev.fitko.fitconnect.api.config.SchemaConfig.METADATA_V_1_0_0;
 import static org.awaitility.Awaitility.await;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.is;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * This test uses real credentials and endpoints stored in gitlab ci variables to test the sdk against a real system
  */
 class ClientIntegrationTest {
 
-    private static final String authBaseUrl = "https://auth-testing.fit-connect.fitko.dev";
-    private static final String submissionBaseUrl = "https://submission-api-testing.fit-connect.fitko.dev";
-
     @Nested
     class SendSubmissionTests {
 
@@ -116,19 +137,19 @@ class ClientIntegrationTest {
             final String encryptedData = cryptoService.encryptString(encryptionKey, jsonData);
             final String encryptedAttachment = cryptoService.encryptString(encryptionKey, attachmentData);
 
-            final SubmissionSchema submissionSchema = new SubmissionSchema();
+            final var submissionSchema = new SubmissionSchema();
             submissionSchema.setSchemaUri(URI.create("https://schema.fitko.de/fim/s00000000009_1.0.0.schema.json"));
             submissionSchema.setMimeType(MimeType.APPLICATION_JSON);
 
-            final Hash hash = new Hash();
-            hash.setSignatureType(SignatureType.SHA_512);
-            hash.setContent(cryptoService.hashBytes(jsonData.getBytes(StandardCharsets.UTF_8)));
+            final var dataHash = new Hash();
+            dataHash.setSignatureType(SignatureType.SHA_512);
+            dataHash.setContent(cryptoService.hashBytes(jsonData.getBytes(StandardCharsets.UTF_8)));
 
-            final Data data = new Data();
-            data.setHash(hash);
+            final var data = new Data();
+            data.setHash(dataHash);
             data.setSubmissionSchema(submissionSchema);
 
-            final AttachmentPayload attachmentPayload = AttachmentPayload.builder()
+            final var attachmentPayload = AttachmentPayload.builder()
                     .encryptedData(encryptedAttachment)
                     .file(attachmentFile)
                     .mimeType(MimeTypes.PLAIN_TEXT)
@@ -136,14 +157,29 @@ class ClientIntegrationTest {
                     .hashedData(cryptoService.hashBytes(attachmentData.getBytes(StandardCharsets.UTF_8)))
                     .build();
 
-            final ServiceType serviceType = ServiceType.builder()
-                    .name("Test Service")
-                    .identifier("urn:de:fim:leika:leistung:99400048079000")
-                    .build();
+            final var publicServiceType = new PublicServiceType();
+            publicServiceType.setName("Test Service");
+            publicServiceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
+
+            final var attachment = new Attachment();
+            attachment.setAttachmentId(attachmentPayload.getAttachmentId());
+            attachment.setPurpose(Purpose.ATTACHMENT);
+            attachment.setFilename(attachmentPayload.getFile().getName());
+            attachment.setMimeType(attachmentPayload.getMimeType());
+
+            final var attachmentHash = new Hash();
+            attachmentHash.setContent(attachmentPayload.getHashedData());
+            attachmentHash.setSignatureType(SignatureType.SHA_512);
+            attachment.setHash(attachmentHash);
+
+            final var contentStructure = new ContentStructure();
+            contentStructure.setAttachments(List.of(attachment));
+            contentStructure.setData(data);
 
-            final PublicServiceType publicServiceType = SubmissionUtil.buildPublicServiceType(serviceType);
-            final List<Attachment> attachmentMetadata = SubmissionUtil.toAttachmentMetadata(List.of(attachmentPayload));
-            final Metadata metadata = SubmissionUtil.buildMetadata(attachmentMetadata, data, publicServiceType);
+            final var metadata = new Metadata();
+            metadata.setSchema(METADATA_V_1_0_0.toString());
+            metadata.setContentStructure(contentStructure);
+            metadata.setPublicServiceType(publicServiceType);
 
             final String encryptedMetadata = cryptoService.encryptBytes(encryptionKey, new ObjectMapper().writeValueAsBytes(metadata));
 
@@ -316,7 +352,7 @@ class ClientIntegrationTest {
             final var submission = SubmissionBuilder.Builder()
                     .withJsonData("{ \"data\": \"Beispiel Fachdaten\" }")
                     .withDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID")))
-                    .withServiceType("Test Service", "urn:de:fim:leika:")
+                    .withServiceType("Test Service", "urn:de:fim:leika:leistung:99400048079000")
                     .build();
 
             final var sentSubmission = ClientFactory.senderClient(config).submit(submission);
@@ -348,7 +384,7 @@ class ClientIntegrationTest {
                     .withAttachment(new File("src/test/resources/attachment.txt"))
                     .withJsonData("{ \"data\": \"Beispiel Fachdaten\" }")
                     .withDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID")))
-                    .withServiceType("Test Service", "urn:de:fim:leika:")
+                    .withServiceType("Test Service", "urn:de:fim:leika:leistung:99400048079000")
                     .build();
 
             final var sentSubmission = ClientFactory.senderClient(config).submit(submission);
@@ -387,7 +423,7 @@ class ClientIntegrationTest {
                     .withAttachment(new File("src/test/resources/attachment.txt"))
                     .withJsonData("{ \"data\": \"Beispiel Fachdaten\" }")
                     .withDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID")))
-                    .withServiceType("Test Service", "urn:de:fim:leika:")
+                    .withServiceType("Test Service", "urn:de:fim:leika:leistung:99400048079000")
                     .build();
 
             final var sentSubmission = ClientFactory.senderClient(config).submit(submission);
@@ -406,6 +442,95 @@ class ClientIntegrationTest {
 
     }
 
+    @Nested
+    class RoutingTests {
+
+        @Test
+        @EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*")
+        void testFindDestinationsWithRegionalKey() {
+
+            // Given
+            final ApplicationConfig config = getConfigWithCredentialsFromGitlab("TESTING", true);
+
+            final RoutingClient routingClient = ClientFactory.routingClient(config);
+
+            final DestinationSearch search = DestinationSearch.Builder()
+                    .withLeikaKey("99123456760610")
+                    .withArs("064350014014")
+                    .build();
+
+            // When
+            final List<Route> routes = routingClient.findDestinations(search);
+
+            // Then
+            assertThat(routes, hasSize(1));
+            assertThat(routes.get(0).getDestinationId(), is(UUID.fromString("d40e7b13-da98-4b09-9e16-bbd61ca81510")));
+        }
+
+        @Test
+        @EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*")
+        void testFindDestinationsWithAreaId() {
+
+            // Given
+            final ApplicationConfig config = getConfigWithCredentialsFromGitlab("TESTING", true);
+
+            final RoutingClient routingClient = ClientFactory.routingClient(config);
+
+            final DestinationSearch search = DestinationSearch.Builder()
+                    .withLeikaKey("99123456760610")
+                    .withAreaId("931")
+                    .build();
+
+            // When
+            final List<Route> routes = routingClient.findDestinations(search);
+
+            // Then
+            assertThat(routes, hasSize(1));
+            assertThat(routes.get(0).getDestinationId(), is(UUID.fromString("d40e7b13-da98-4b09-9e16-bbd61ca81510")));
+        }
+
+        @Test
+        @EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*")
+        void testFindDestinationsWithMultipleAreaSearchCriteria() {
+
+            // Given
+            final ApplicationConfig config = getConfigWithCredentialsFromGitlab("TESTING", true);
+
+            final RoutingClient routingClient = ClientFactory.routingClient(config);
+
+            final DestinationSearch search = DestinationSearch.Builder()
+                    .withLeikaKey("99123456760610")
+                    .withArs("064350014014")
+                    .withAreaId("1234")
+                    .build();
+
+            // When
+            final RestApiException exception = assertThrows(RestApiException.class, () -> routingClient.findDestinations(search));
+
+            // Then
+            assertThat(exception.getMessage(), containsString("Only one of ars, ags or areaId must be specified"));
+        }
+
+        @Test
+        @EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*")
+        void testFindAreaWithMultipleSearchCriteria() {
+
+            // Given
+            final ApplicationConfig config = getConfigWithCredentialsFromGitlab("TESTING", true);
+
+            final RoutingClient routingClient = ClientFactory.routingClient(config);
+
+            // When
+            final List<Area> areas = routingClient.findAreas(List.of("Leip*", "04229"), 0, 10);
+
+            // Then
+            assertThat(areas, is(not(empty())));
+            assertThat(areas.size(), is(lessThanOrEqualTo(10)));
+            assertTrue(areas.stream().anyMatch(area -> area.getName().equals("Leipzig")));
+        }
+
+    }
+
     @Nested
     class AuthenticationTests {
         @Test
@@ -414,12 +539,12 @@ class ClientIntegrationTest {
 
             // Given
             final var tokenUrl = "https://auth-testing.fit-connect.fitko.dev/token";
-            final var clientId = "781f6213-0f0f-4a79-9372-e7187ffda98b";
-            final var secret = "PnzR8Vbmhpv_VwTkT34wponqXWK8WBm-LADlryYdV4o";
-            final var scope1 = "send:region:DE";
-            final var scope2 = "send:region:EN";
+            final var clientId = System.getenv("SUBSCRIBER_CLIENT_ID");
+            final var secret = System.getenv("SUBSCRIBER_CLIENT_SECRET");
+
+            final RestService restService = new RestService(new BuildInfo());
 
-            final var authService = new DefaultOAuthService(new RestTemplate(), clientId, secret, tokenUrl);
+            final var authService = new DefaultOAuthService(restService.getRestTemplate(), clientId, secret, tokenUrl);
 
             // When
             final OAuthToken token = authService.getCurrentToken();
diff --git a/client/src/test/java/dev/fitko/fitconnect/client/RoutingClientTest.java b/client/src/test/java/dev/fitko/fitconnect/client/RoutingClientTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a0ee2b95c8f06a868455ae56c7707f4917b4697a
--- /dev/null
+++ b/client/src/test/java/dev/fitko/fitconnect/client/RoutingClientTest.java
@@ -0,0 +1,138 @@
+package dev.fitko.fitconnect.client;
+
+import dev.fitko.fitconnect.api.domain.model.route.Area;
+import dev.fitko.fitconnect.api.domain.model.route.AreaResult;
+import dev.fitko.fitconnect.api.domain.model.route.Route;
+import dev.fitko.fitconnect.api.domain.model.route.RouteResult;
+import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
+import dev.fitko.fitconnect.api.exceptions.RoutingException;
+import dev.fitko.fitconnect.api.services.routing.RoutingService;
+import dev.fitko.fitconnect.client.router.DestinationSearch;
+import dev.fitko.fitconnect.core.routing.RouteVerifier;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class RoutingClientTest {
+
+
+    RoutingService routingServiceMock;
+
+    RouteVerifier routeVerifierMock;
+    RoutingClient underTest;
+
+    @BeforeEach
+    void setup() {
+        routingServiceMock = mock(RoutingService.class);
+        routeVerifierMock = mock(RouteVerifier.class);
+        underTest = new RoutingClient(routingServiceMock, routeVerifierMock);
+    }
+
+    @Test
+    void findDestinationsTest() {
+
+        // Given
+        final var firstExpectedRoute = new Route();
+        firstExpectedRoute.setDestinationId(UUID.randomUUID());
+
+        final var secondExpectedRoute = new Route();
+        secondExpectedRoute.setDestinationId(UUID.randomUUID());
+
+        final var expectedRouteResult = new RouteResult();
+        expectedRouteResult.setRoutes(List.of(firstExpectedRoute, secondExpectedRoute));
+
+        when(routingServiceMock.getRoutes(any(), any(), any(), any(), anyInt(), anyInt())).thenReturn(expectedRouteResult);
+        when(routeVerifierMock.validateRouteDestinations(any(), any(), any())).thenReturn(ValidationResult.ok());
+
+        final DestinationSearch search = DestinationSearch.Builder()
+                .withLeikaKey("99400048079000")
+                .withArs("064350014014")
+                .withLimit(5)
+                .build();
+
+        // When
+        final List<Route> destinations = underTest.findDestinations(search);
+
+        // Then
+        assertThat(destinations.size(), is(2));
+        assertThat(destinations.get(0).getDestinationId(), is(firstExpectedRoute.getDestinationId()));
+        assertThat(destinations.get(1).getDestinationId(), is(secondExpectedRoute.getDestinationId()));
+    }
+
+    @Test
+    void findDestinationsWithFailedValidationTest() {
+
+        // Given
+        final var expectedRoute = new Route();
+        expectedRoute.setDestinationId(UUID.randomUUID());
+
+        final var expectedRouteResult = new RouteResult();
+        expectedRouteResult.setRoutes(List.of(expectedRoute));
+
+        when(routingServiceMock.getRoutes(any(), any(), any(), any(), anyInt(), anyInt())).thenReturn(expectedRouteResult);
+        when(routeVerifierMock.validateRouteDestinations(any(), any(), any())).thenReturn(ValidationResult.error(new RoutingException("Route validation failed")));
+
+        final DestinationSearch search = DestinationSearch.Builder()
+                .withLeikaKey("99400048079000")
+                .withArs("064350014014")
+                .withLimit(5)
+                .build();
+
+        // When
+        final RoutingException exception = assertThrows(RoutingException.class, () -> underTest.findDestinations(search));
+
+        // Then
+        assertThat(exception.getMessage(), containsString("Route validation failed"));
+    }
+
+    @Test
+    void testGetAreasWithSingleSearchExpressionTest() {
+        // Given
+        final var expectedArea = new Area();
+        expectedArea.setName("Leipzig");
+
+        final var expectedAreaResult = new AreaResult();
+        expectedAreaResult.setAreas(List.of(expectedArea));
+
+        when(routingServiceMock.getAreas(anyList(), anyInt(), anyInt())).thenReturn(expectedAreaResult);
+
+        // When
+        final List<Area> areas = underTest.findAreas("04229", 0, 5);
+
+        // Then
+        assertThat(areas.size(), is(1));
+        assertThat(areas.get(0).getName(), is("Leipzig"));
+    }
+
+    @Test
+    void getAreasWithMultipleFiltersTest() {
+
+        // Given
+        final var expectedArea = new Area();
+        expectedArea.setName("Leipzig");
+
+        final var expectedAreaResult = new AreaResult();
+        expectedAreaResult.setAreas(List.of(expectedArea));
+
+        when(routingServiceMock.getAreas(anyList(), anyInt(), anyInt())).thenReturn(expectedAreaResult);
+
+        // When
+        final List<Area> areas = underTest.findAreas(List.of("Leip*", "04229"), 0, 5);
+
+        // Then
+        assertThat(areas.size(), is(1));
+        assertThat(areas.get(0).getName(), is("Leipzig"));
+    }
+}
\ No newline at end of file
diff --git a/client/src/test/java/dev/fitko/fitconnect/client/SenderClientTest.java b/client/src/test/java/dev/fitko/fitconnect/client/SenderClientTest.java
index e6f56c69fe2f19363f563836847ca1950bf5c1ce..d9a9c1c70788fe5c52f2ef292282faea1b68fd55 100644
--- a/client/src/test/java/dev/fitko/fitconnect/client/SenderClientTest.java
+++ b/client/src/test/java/dev/fitko/fitconnect/client/SenderClientTest.java
@@ -121,7 +121,6 @@ public class SenderClientTest {
         when(senderMock.validateMetadata(any())).thenReturn(ValidationResult.ok());
         when(senderMock.validateXmlFormat(any())).thenReturn(ValidationResult.ok());
 
-
         // When
         final var submission = SubmissionBuilder.Builder()
                 .withAttachment(new File("src/test/resources/attachment.txt"))
@@ -162,92 +161,19 @@ public class SenderClientTest {
         logs.assertContains("Detected attachment mime-type application/pdf");
     }
 
-
-    @Test
-    void testWithMissingData() throws Exception {
-
-        // Given
-        final UUID destinationId = setupTestMocks();
-
-        // When
-        final var submission = SubmissionBuilder.Builder()
-                .withJsonData(null)
-                .withDestination(destinationId)
-                .withServiceType("name", "urn:de:fim:leika:leistung:99400048079000")
-                .build();
-
-        // When
-        final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission));
-
-        // Then
-        assertThat(exception.getMessage(), containsString("Data is mandatory, but was null"));
-    }
-
-    @Test
-    void testWithMissingDestinationId() throws Exception {
-
-        // Given
-        setupTestMocks();
-
-        // When
-        final var submission = SubmissionBuilder.Builder()
-                .withJsonData("{}")
-                .withDestination(null)
-                .withServiceType("name", "urn:de:fim:leika:leistung:99400048079000")
-                .build();
-
-        // When
-        final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission));
-
-        // Then
-        assertThat(exception.getMessage(), containsString("DestinationId is mandatory, but was null"));
-    }
-
-    @Test
-    void testWithInvalidDestinationId() {
-
-        // Given
-        final var invalidUUID = "1234:132434:5678";
-
-        // When
-        final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () ->
-                SubmissionBuilder.Builder()
-                        .withJsonData("{}")
-                        .withDestination(UUID.fromString(invalidUUID))
-                        .withServiceType("name", "test:key")
-                        .build());
-
-        // Then
-        assertThat(exception.getMessage(), containsString("Invalid UUID string: " + invalidUUID));
-    }
-
-    @Test
-    void testWithInvalidLeikaKey() throws JOSEException {
-
-        // Given
-        setupTestMocks();
-
-        // Given
-        final var submission = SubmissionBuilder.Builder()
-                .withJsonData("{\"test\" . \"data\"}")
-                .withDestination(UUID.randomUUID())
-                .withServiceType("name", "illegal:test:identifier")
-                .build();
-
-        // When
-        final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission));
-
-        // Then
-        assertThat(exception.getMessage(), containsString("LeikaKey has invalid format"));
-    }
-
     @Test
     void testMissingDataMimeTypeSchema() throws Exception {
 
         // Given
         final var destinationId = UUID.randomUUID();
+
+        final var destinationService = new DestinationService();
+        destinationService.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
+
         final var destination = new Destination();
+        destination.setServices(Set.of(destinationService));
         destination.setDestinationId(destinationId);
+
         final var announcedSubmission = getAnnouncedSubmission(destinationId);
 
         final RSAKey publicKey = generateRsaKey(4096).toPublicJWK();
@@ -272,68 +198,6 @@ public class SenderClientTest {
         logs.assertContains("Required schema to send valid submission not found");
     }
 
-    @Test
-    void testWithMissingServiceTypeIdentifier() throws Exception {
-
-        // Given
-        final var destinationId = setupTestMocks();
-
-        final var submission = SubmissionBuilder.Builder()
-                .withJsonData("{}")
-                .withDestination(destinationId)
-                .withServiceType("Name", null)
-                .build();
-
-        // When
-        final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission));
-
-        // Then
-        assertThat(exception.getMessage(), containsString("Leika key is mandatory, but was null"));
-    }
-
-    @Test
-    void testMissingEncryptedMetadata() throws Exception {
-
-        // Given
-        final var destinationId = setupTestMocks();
-
-        // When
-        final EncryptedSubmissionPayload submission = EncryptedSubmissionBuilder.Builder()
-                .withEncryptedData("encr$&ted d/&)ata")
-                .withEncryptedMetadata(null)
-                .withDestination(destinationId)
-                .withServiceType("name", "urn:de:fim:leika:leistung:99400048079000")
-                .build();
-
-        // When
-        final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission));
-
-        // Then
-        assertThat(exception.getMessage(), containsString("Encrypted metadata must not be null"));
-    }
-
-    @Test
-    void testMissingEncryptedData() throws Exception {
-
-        // Given
-        final var destinationId = setupTestMocks();
-
-        // When
-        final EncryptedSubmissionPayload submission = EncryptedSubmissionBuilder.Builder()
-                .withEncryptedData(null)
-                .withEncryptedMetadata("encr$&ted met@d/&)ata")
-                .withDestination(destinationId)
-                .withServiceType("name", "urn:de:fim:leika:leistung:99400048079000")
-                .build();
-
-        // When
-        final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission));
-
-        // Then
-        assertThat(exception.getMessage(), containsString("Data is mandatory, but was null"));
-    }
-
-
     @Test
     void testWithEmptyAttachments() throws Exception {
 
@@ -445,7 +309,7 @@ public class SenderClientTest {
 
         // Given
         final var destinationId = setupTestMocks();
-        when(senderMock.getDestination(any())).thenThrow(new RestApiException("Loading destination failed"));
+        when(senderMock.createSubmission(any())).thenThrow(new RestApiException("Announcing submission failed"));
 
         // When
         final SubmissionPayload submission = SubmissionBuilder.Builder()
@@ -635,6 +499,7 @@ public class SenderClientTest {
     }
 
     private UUID setupTestMocks() throws JOSEException {
+
         final var destinationId = UUID.randomUUID();
         final var destination = getDestination(destinationId);
         final var announcedSubmission = getAnnouncedSubmission(destinationId);
@@ -674,6 +539,7 @@ public class SenderClientTest {
         schema.setMimeType(MimeType.APPLICATION_JSON);
 
         final var destinationService = new DestinationService();
+        destinationService.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
         destinationService.setSubmissionSchemas(Set.of(schema));
 
         final var destination = new Destination();
@@ -684,6 +550,7 @@ public class SenderClientTest {
 
     private Destination getDestination(final UUID destinationId, final SubmissionSchema schema) {
         final var destinationService = new DestinationService();
+        destinationService.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
         destinationService.setSubmissionSchemas(Set.of(schema));
 
         final var destination = new Destination();
diff --git a/client/src/test/java/dev/fitko/fitconnect/client/factory/ClientFactoryTest.java b/client/src/test/java/dev/fitko/fitconnect/client/factory/ClientFactoryTest.java
index 82e9749dbe451b93647e851cd35de0ba60d9ce40..9c1c539a6704aee684d822e66952c3d384d3f36c 100644
--- a/client/src/test/java/dev/fitko/fitconnect/client/factory/ClientFactoryTest.java
+++ b/client/src/test/java/dev/fitko/fitconnect/client/factory/ClientFactoryTest.java
@@ -16,6 +16,7 @@ class ClientFactoryTest {
     void testMissingConfiguration() {
         assertThrows(InitializationException.class, ClientFactory::senderClient);
         assertThrows(InitializationException.class, ClientFactory::subscriberClient);
+        assertThrows(InitializationException.class, ClientFactory::routingClient);
     }
 
     @Test
@@ -61,6 +62,23 @@ class ClientFactoryTest {
         assertNotNull(ClientFactory.subscriberClient(subscriberConfig));
     }
 
+    @Test
+    void testRoutingClientConstruction() {
+
+        final var envName = new EnvironmentName("DEV");
+        final var environment = new Environment("", "https://routing.fitko.fitconnect.de", "", "", true);
+
+        final var senderConfig = new SenderConfig("1234", "abcd");
+
+        final var routingConfig = ApplicationConfig.builder()
+                .environments(Map.of(envName, environment))
+                .activeEnvironment(envName)
+                .senderConfig(senderConfig)
+                .build();
+
+        assertNotNull(ClientFactory.routingClient(routingConfig));
+    }
+
     @Test
     void testSigningKeyCannotBeParsed() {
 
diff --git a/client/src/test/java/dev/fitko/fitconnect/client/testutil/LogCaptor.java b/client/src/test/java/dev/fitko/fitconnect/client/testutil/LogCaptor.java
index 2c2428fc46829b6f1fb301cee976f1b09ab9ae18..200b2e2e9d3d519880a83d80658f218835da2ccf 100644
--- a/client/src/test/java/dev/fitko/fitconnect/client/testutil/LogCaptor.java
+++ b/client/src/test/java/dev/fitko/fitconnect/client/testutil/LogCaptor.java
@@ -6,7 +6,6 @@ import ch.qos.logback.core.AppenderBase;
 import java.util.ArrayList;
 import java.util.List;
 
-import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public final class LogCaptor extends AppenderBase<LoggingEvent> {
diff --git a/client/src/test/java/dev/fitko/fitconnect/client/util/SubmissionUtilTest.java b/client/src/test/java/dev/fitko/fitconnect/client/util/SubmissionUtilTest.java
deleted file mode 100644
index b9eda5af746b0021a00985a99bdbb277ccba6c04..0000000000000000000000000000000000000000
--- a/client/src/test/java/dev/fitko/fitconnect/client/util/SubmissionUtilTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package dev.fitko.fitconnect.client.util;
-
-import dev.fitko.fitconnect.api.domain.model.metadata.Metadata;
-import dev.fitko.fitconnect.api.domain.model.metadata.PublicServiceType;
-import dev.fitko.fitconnect.api.domain.model.metadata.Signature;
-import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment;
-import dev.fitko.fitconnect.api.domain.model.metadata.data.Data;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-
-import static dev.fitko.fitconnect.api.config.SchemaConfig.METADATA_V_1_0_0;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-
-public class SubmissionUtilTest {
-
-    @Test
-    public void buildMetadata() {
-
-        Signature signature = new Signature();
-        signature.setContent("content");
-        Data data = new Data();
-        data.setSignature(signature);
-
-        Attachment attachment1 = new Attachment();
-        attachment1.setFilename("fileName1");
-        Attachment attachment2 = new Attachment();
-        attachment2.setFilename("fileName2");
-        List<Attachment> attachments = List.of(attachment1, attachment2);
-
-        PublicServiceType publicServiceType = new PublicServiceType();
-        publicServiceType.setName("publicService");
-
-        Metadata metadata = SubmissionUtil.buildMetadata(attachments, data, publicServiceType);
-
-        assertThat(metadata.getSchema(), is(METADATA_V_1_0_0.toString()));
-        assertThat(metadata.getContentStructure().getAttachments().size(), is(2));
-        assertThat(metadata.getContentStructure().getAttachments().get(0).getFilename(), is("fileName1"));
-        assertThat(metadata.getContentStructure().getAttachments().get(1).getFilename(), is("fileName2"));
-        assertThat(metadata.getContentStructure().getData().getSignature().getContent(), is("content"));
-        assertThat(metadata.getPublicServiceType().getName(), is("publicService"));
-    }
-}
diff --git a/client/src/test/java/dev/fitko/fitconnect/client/util/ValidDataGuardTest.java b/client/src/test/java/dev/fitko/fitconnect/client/util/ValidDataGuardTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..86b0e2777d61f8d528da4e38f0d3369c0b26b36c
--- /dev/null
+++ b/client/src/test/java/dev/fitko/fitconnect/client/util/ValidDataGuardTest.java
@@ -0,0 +1,353 @@
+package dev.fitko.fitconnect.client.util;
+
+import dev.fitko.fitconnect.api.domain.model.destination.Destination;
+import dev.fitko.fitconnect.api.domain.model.destination.DestinationService;
+import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
+import dev.fitko.fitconnect.api.services.Sender;
+import dev.fitko.fitconnect.client.sender.EncryptedSubmissionBuilder;
+import dev.fitko.fitconnect.client.sender.SubmissionBuilder;
+import dev.fitko.fitconnect.client.sender.model.EncryptedSubmissionPayload;
+import dev.fitko.fitconnect.client.sender.model.SubmissionPayload;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+import java.util.UUID;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class ValidDataGuardTest {
+
+    ValidDataGuard underTest;
+    Sender senderMock;
+
+    @BeforeEach
+    void setUp() {
+        senderMock = mock(Sender.class);
+        underTest = new ValidDataGuard(senderMock);
+    }
+
+    @Nested
+    class SubmissionPayloadDataTests {
+
+        @Test
+        void testValidSubmissionPayload() {
+
+            // Given
+            final DestinationService service = new DestinationService();
+            service.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
+
+            final Destination destination = new Destination();
+            destination.setDestinationId(UUID.randomUUID());
+            destination.setServices(Set.of(service));
+
+            when(senderMock.getDestination(any())).thenReturn(destination);
+            when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
+
+            final SubmissionPayload submissionPayload = SubmissionBuilder.Builder()
+                    .withJsonData("\"test\": \"json\"")
+                    .withDestination(UUID.randomUUID())
+                    .withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000")
+                    .build();
+
+            // Then valid if no exception is thrown
+            underTest.ensureValidDataPayload(submissionPayload);
+        }
+
+        @Test
+        void testMissingJsonData() {
+
+            // Given
+            final SubmissionPayload submissionPayload = SubmissionBuilder.Builder()
+                    .withJsonData(null)
+                    .withDestination(UUID.randomUUID())
+                    .withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000")
+                    .build();
+
+            // When
+            final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload));
+
+            // Then
+            assertThat(exception.getMessage(), containsString("Data is mandatory, but was null"));
+        }
+
+        @Test
+        void testMissingXmlData() {
+
+            // Given
+            final SubmissionPayload submissionPayload = SubmissionBuilder.Builder()
+                    .withXmlData(null)
+                    .withDestination(UUID.randomUUID())
+                    .withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000")
+                    .build();
+
+            // When
+            final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload));
+
+            // Then
+            assertThat(exception.getMessage(), containsString("Data is mandatory, but was null"));
+        }
+
+        @Test
+        void testMissingDestinationId() {
+
+            // Given
+            when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
+
+            final SubmissionPayload submissionPayload = SubmissionBuilder.Builder()
+                    .withJsonData("{}")
+                    .withDestination(null)
+                    .withServiceType("name", "urn:de:fim:leika:leistung:99400048079000")
+                    .build();
+
+            // When
+            final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload));
+
+            // Then
+            assertThat(exception.getMessage(), containsString("DestinationId is mandatory, but was null"));
+        }
+
+        @Test
+        void testInvalidServiceIdentifier() {
+
+            // Given
+            when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
+
+            final SubmissionPayload submissionPayload = SubmissionBuilder.Builder()
+                    .withJsonData("{\"test\" . \"data\"}")
+                    .withDestination(UUID.randomUUID())
+                    .withServiceType("name", "illegal:test:identifier")
+                    .build();
+            // When
+            final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload));
+
+            // Then
+            assertThat(exception.getMessage(), containsString("LeikaKey has invalid format"));
+        }
+
+        @Test
+        void testMissingServiceIdentifier() {
+
+            // Given
+            when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
+
+            final SubmissionPayload submissionPayload = SubmissionBuilder.Builder()
+                    .withJsonData("{\"test\" . \"data\"}")
+                    .withDestination(UUID.randomUUID())
+                    .withServiceType("name", null)
+                    .build();
+            // When
+            final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload));
+
+            // Then
+            assertThat(exception.getMessage(), containsString("Leika key is mandatory, but was null"));
+        }
+
+        @Test
+        void testServiceIdentifierNotMatchingDestination() {
+
+            // Given
+            final DestinationService service = new DestinationService();
+            service.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
+
+            final Destination destination = new Destination();
+            destination.setServices(Set.of(service));
+
+            when(senderMock.getDestination(any())).thenReturn(destination);
+            when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
+
+            final SubmissionPayload submissionPayload = SubmissionBuilder.Builder()
+                    .withJsonData("\"test\": \"json\"")
+                    .withDestination(UUID.randomUUID())
+                    .withServiceType("Test", "urn:de:fim:leika:leistung:123456789101114")
+                    .build();
+            // When
+            final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload));
+
+            // Then
+            assertThat(exception.getMessage(), containsString("Provided service type 'urn:de:fim:leika:leistung:123456789101114' is not allowed by the destination"));
+        }
+
+    }
+
+
+    @Nested
+    class EncryptedSubmissionPayloadDataTests {
+
+
+        @Test
+        void testValidEncryptedSubmissionPayload() {
+
+            // Given
+            final DestinationService service = new DestinationService();
+            service.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
+
+            final Destination destination = new Destination();
+            destination.setDestinationId(UUID.randomUUID());
+            destination.setServices(Set.of(service));
+
+            when(senderMock.getDestination(any())).thenReturn(destination);
+            when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
+
+            final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder()
+                    .withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF")
+                    .withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF")
+                    .withDestination(UUID.randomUUID())
+                    .withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000")
+                    .build();
+
+            // Then valid if no exception is thrown
+            underTest.ensureValidDataPayload(encryptedSubmissionPayload);
+        }
+
+        @Test
+        void testMissingEncryptedData() {
+
+            // Given
+            final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder()
+                    .withEncryptedData(null)
+                    .withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF")
+                    .withDestination(UUID.randomUUID())
+                    .withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000")
+                    .build();
+
+            // When
+            final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload));
+
+            // Then
+            assertThat(exception.getMessage(), containsString("Encrypted data is mandatory, but was null"));
+        }
+
+        @Test
+        void testMissingEncryptedMetadata() {
+
+            // Given
+            final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder()
+                    .withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF")
+                    .withEncryptedMetadata(null)
+                    .withDestination(UUID.randomUUID())
+                    .withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000")
+                    .build();
+
+            // When
+            final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload));
+
+            // Then
+            assertThat(exception.getMessage(), containsString("Encrypted metadata must not be null"));
+        }
+
+        @Test
+        void testMissingDestinationId() {
+
+            // Given
+            when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
+
+            final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder()
+                    .withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF")
+                    .withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF")
+                    .withDestination(null)
+                    .withServiceType("name", "urn:de:fim:leika:leistung:99400048079000")
+                    .build();
+
+            // When
+            final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload));
+
+            // Then
+            assertThat(exception.getMessage(), containsString("DestinationId is mandatory, but was null"));
+        }
+
+        @Test
+        void testInvalidServiceIdentifier() {
+
+            // Given
+            when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
+
+            final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder()
+                    .withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF")
+                    .withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF")
+                    .withDestination(UUID.randomUUID())
+                    .withServiceType("name", "illegal:test:identifier")
+                    .build();
+
+            // When
+            final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload));
+
+            // Then
+            assertThat(exception.getMessage(), containsString("LeikaKey has invalid format"));
+        }
+
+        @Test
+        void testMissingServiceIdentifier() {
+
+            // Given
+            when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
+
+            final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder()
+                    .withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF")
+                    .withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF")
+                    .withDestination(UUID.randomUUID())
+                    .withServiceType("name", null)
+                    .build();
+            // When
+            final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload));
+
+            // Then
+            assertThat(exception.getMessage(), containsString("Leika key is mandatory, but was null"));
+        }
+
+        @Test
+        void testServiceIdentifierNotMatchingDestination() {
+
+            // Given
+            final DestinationService service = new DestinationService();
+            service.setIdentifier("urn:de:fim:leika:leistung:99400048079000");
+
+            final Destination destination = new Destination();
+            destination.setServices(Set.of(service));
+
+            when(senderMock.getDestination(any())).thenReturn(destination);
+            when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok());
+
+            final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder()
+                    .withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF")
+                    .withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF")
+                    .withDestination(UUID.randomUUID())
+                    .withServiceType("name", "urn:de:fim:leika:leistung:11111111111111")
+                    .build();
+
+            // When
+            final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload));
+
+            // Then
+            assertThat(exception.getMessage(), containsString("Provided service type 'urn:de:fim:leika:leistung:11111111111111' is not allowed by the destination"));
+        }
+    }
+
+    @Nested
+    class MiscTests {
+
+        @Test
+        void testInvalidDestinationId() {
+
+            // Given
+            final var invalidUUID = "1234:132434:5678";
+
+            // When
+            final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () ->
+                    SubmissionBuilder.Builder()
+                            .withJsonData("{}")
+                            .withDestination(UUID.fromString(invalidUUID))
+                            .withServiceType("name", "test:key")
+                            .build());
+
+            // Then
+            assertThat(exception.getMessage(), containsString("Invalid UUID string: " + invalidUUID));
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSender.java b/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSender.java
index 5b4d94837ece7ef8773daf624ac8b5113221eab8..f66087eabaf1bc8d1f01c07c79039eefdd130b96 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSender.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSender.java
@@ -60,8 +60,9 @@ public class SubmissionSender implements Sender {
     }
 
     @Override
-    public ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret) {
-        return this.validationService.validateCallback(hmac, timestamp, httpBody, callbackSecret);
+    public ValidationResult validateCallback(final String hmac, final Long timestamp, final String httpBody, final String callbackSecret) {
+        LOGGER.info("Validating callback integrity");
+        return validationService.validateCallback(hmac, timestamp, httpBody, callbackSecret);
     }
 
     @Override
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSubscriber.java b/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSubscriber.java
index d1f74480454709c086ca92589e4c951a20acf64b..c47927ed62883d7403df37fa50ea5429a9d26554 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSubscriber.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSubscriber.java
@@ -86,8 +86,9 @@ public class SubmissionSubscriber implements Subscriber {
     }
 
     @Override
-    public ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret) {
-        return this.validationService.validateCallback(hmac, timestamp, httpBody, callbackSecret);
+    public ValidationResult validateCallback(final String hmac, final Long timestamp, final String httpBody, final String callbackSecret) {
+        LOGGER.info("Validating callback integrity");
+        return validationService.validateCallback(hmac, timestamp, httpBody, callbackSecret);
     }
 
     @Override
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/auth/DefaultOAuthService.java b/core/src/main/java/dev/fitko/fitconnect/core/auth/DefaultOAuthService.java
index f59a1b858894bd744c757ad0978b9a99b17686bb..908d0b2ce48e155d05f0bac9e9688e082f8e3101 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/auth/DefaultOAuthService.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/auth/DefaultOAuthService.java
@@ -38,7 +38,7 @@ public class DefaultOAuthService implements OAuthService {
         this.restTemplate = restTemplate;
         this.clientId = clientId;
         this.clientSecret = clientSecret;
-        this.tokenUrl = authUrl;
+        tokenUrl = authUrl;
         resetExistingToken();
     }
 
@@ -48,26 +48,26 @@ public class DefaultOAuthService implements OAuthService {
             LOGGER.info("Current token is expired, authenticating ...");
             authenticate();
         }
-        return this.currentToken;
+        return currentToken;
     }
 
     private boolean tokenExpired() {
-        if (this.currentToken == null || this.tokenExpirationTime == null) {
+        if (currentToken == null || tokenExpirationTime == null) {
             return true;
         }
         final var now = LocalDateTime.now();
-        return this.tokenExpirationTime.isBefore(now) || this.tokenExpirationTime.isEqual(now);
+        return tokenExpirationTime.isBefore(now) || tokenExpirationTime.isEqual(now);
     }
 
     private void resetExistingToken() {
-        this.currentToken = null;
-        this.tokenExpirationTime = null;
+        currentToken = null;
+        tokenExpirationTime = null;
     }
 
     private void authenticate() throws AuthenticationException {
-        final String requestBody = buildRequestBody(this.clientId, this.clientSecret);
-        this.currentToken = performTokenRequest(requestBody);
-        this.tokenExpirationTime = LocalDateTime.now().plusSeconds(this.currentToken.getExpiresIn());
+        final String requestBody = buildRequestBody(clientId, clientSecret);
+        currentToken = performTokenRequest(requestBody);
+        tokenExpirationTime = LocalDateTime.now().plusSeconds(currentToken.getExpiresIn());
     }
 
     private String buildRequestBody(final String clientId, final String clientSecret, final String... scope) {
@@ -95,9 +95,9 @@ public class DefaultOAuthService implements OAuthService {
         final HttpEntity<String> entity = new HttpEntity<>(requestBody, headers);
         try {
             LOGGER.info("Sending authentication request");
-            return this.restTemplate.exchange(this.tokenUrl, HttpMethod.POST, entity, OAuthToken.class).getBody();
+            return restTemplate.exchange(tokenUrl, HttpMethod.POST, entity, OAuthToken.class).getBody();
         } catch (final RestClientException e) {
-            LOGGER.error(e.getMessage(),e);
+            LOGGER.error(e.getMessage(), e);
             throw new RestApiException("Could not retrieve OAuth token", e);
         }
     }
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogVerifier.java b/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogVerifier.java
index 5cb1aef2bffa6bcc504df1bf5667d74c7634ecef..905a5d52a3cb65adaa5753c0816da5b614863c53 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogVerifier.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogVerifier.java
@@ -20,11 +20,24 @@ import dev.fitko.fitconnect.api.services.events.EventLogVerificationService;
 import dev.fitko.fitconnect.api.services.validation.ValidationService;
 
 import java.text.ParseException;
-import java.util.*;
-
-import static com.nimbusds.jwt.JWTClaimNames.*;
-import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.*;
-import static dev.fitko.fitconnect.core.util.EventLogUtil.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+import static com.nimbusds.jwt.JWTClaimNames.ISSUED_AT;
+import static com.nimbusds.jwt.JWTClaimNames.ISSUER;
+import static com.nimbusds.jwt.JWTClaimNames.JWT_ID;
+import static com.nimbusds.jwt.JWTClaimNames.SUBJECT;
+import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.CLAIM_EVENTS;
+import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.CLAIM_SCHEMA;
+import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.CLAIM_TXN;
+import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.HEADER_TYPE;
+import static dev.fitko.fitconnect.core.util.EventLogUtil.getAuthenticationTags;
+import static dev.fitko.fitconnect.core.util.EventLogUtil.getDestinationId;
+import static dev.fitko.fitconnect.core.util.EventLogUtil.getEventFromClaims;
+import static dev.fitko.fitconnect.core.util.EventLogUtil.resolveIssuerType;
 
 public class EventLogVerifier implements EventLogVerificationService {
 
@@ -150,7 +163,7 @@ public class EventLogVerifier implements EventLogVerificationService {
     private RSAKey getSignatureKey(final String issuer, final String keyId) throws ParseException, EventLogException {
         final EventIssuer issuerType = resolveIssuerType(issuer);
         if (issuerType == EventIssuer.SUBMISSION_SERVICE) {
-            return keyService.getSubmissionServiceSignatureKey(keyId);
+            return keyService.getSubmissionServicePublicKey(keyId);
         } else {
             return keyService.getPublicSignatureKey(getDestinationId(issuer), keyId);
         }
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/http/ProxyRestTemplate.java b/core/src/main/java/dev/fitko/fitconnect/core/http/ProxyRestTemplate.java
deleted file mode 100644
index 3c3c0c18ee7e7ae7c9a3c310e6130c29b82eb906..0000000000000000000000000000000000000000
--- a/core/src/main/java/dev/fitko/fitconnect/core/http/ProxyRestTemplate.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package dev.fitko.fitconnect.core.http;
-
-import org.springframework.http.client.ClientHttpRequestFactory;
-import org.springframework.web.client.RestTemplate;
-
-public class ProxyRestTemplate extends RestTemplate {
-
-    public ProxyRestTemplate(final ClientHttpRequestFactory requestFactory) {
-        super(requestFactory);
-        this.getMessageConverters().add(new X509CRLHttpMessageConverter());
-    }
-}
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/http/ProxyConfig.java b/core/src/main/java/dev/fitko/fitconnect/core/http/RestService.java
similarity index 68%
rename from core/src/main/java/dev/fitko/fitconnect/core/http/ProxyConfig.java
rename to core/src/main/java/dev/fitko/fitconnect/core/http/RestService.java
index a65aa05ebd2510f5f31a5493bee0c933e13a0d56..7711d95adc82632c6b1960827535861278554093 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/http/ProxyConfig.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/http/RestService.java
@@ -16,25 +16,25 @@ import java.util.List;
 
 import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
 
-public class ProxyConfig {
+public class RestService {
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(ProxyConfig.class);
+    private static final Logger LOGGER = LoggerFactory.getLogger(RestService.class);
 
-    private BuildInfo buildInfo;
-    private String host;
-    private int port;
+    private final BuildInfo buildInfo;
+    private final String proxyHost;
+    private final int proxyPort;
 
-    public ProxyConfig(String host, int port, BuildInfo buildInfo) {
-        this.host = host;
-        this.port = port;
+    public RestService(final String proxyHost, final int proxyPort, final BuildInfo buildInfo) {
+        this.proxyHost = proxyHost;
+        this.proxyPort = proxyPort;
         this.buildInfo = buildInfo;
     }
 
-    ProxyConfig() {
-        this(null, 0, new BuildInfo());
+    public RestService(final BuildInfo buildInfo) {
+        this(null, 0, buildInfo);
     }
 
-    public RestTemplate proxyRestTemplate() {
+    public RestTemplate getRestTemplate() {
         return hasProxySet() ? getProxyRestTemplate() : getDefaultRestTemplate();
     }
 
@@ -45,41 +45,41 @@ public class ProxyConfig {
         return restTemplate;
     }
 
-    private ProxyRestTemplate getProxyRestTemplate() {
+    private RestTemplate getProxyRestTemplate() {
         LOGGER.info("Using proxy {}", this);
         final var requestFactory = new SimpleClientHttpRequestFactory();
-        final var proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(this.host, this.port));
+        final var proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
         requestFactory.setProxy(proxy);
 
-        final ProxyRestTemplate proxyRestTemplate = new ProxyRestTemplate(requestFactory);
+        final RestTemplate proxyRestTemplate = new RestTemplate(requestFactory);
         setupTemplate(proxyRestTemplate);
         return proxyRestTemplate;
     }
 
-    private void setMappingConverter(final RestTemplate restTemplate) {
+    private void setMappingConverters(final RestTemplate restTemplate) {
         final MappingJackson2HttpMessageConverter jacksonMessageConverter = new MappingJackson2HttpMessageConverter();
         jacksonMessageConverter.setObjectMapper(new ObjectMapper().configure(FAIL_ON_UNKNOWN_PROPERTIES, false));
         restTemplate.getMessageConverters().add(jacksonMessageConverter);
     }
 
-    private void setLoggingInterceptor(final RestTemplate restTemplate) {
+    private void setLoggingInterceptors(final RestTemplate restTemplate) {
         final List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
         interceptors.add(new ApiRequestInterceptor());
-        interceptors.add(new UserAgentInterceptor(this.buildInfo));
+        interceptors.add(new UserAgentInterceptor(buildInfo));
         restTemplate.setInterceptors(interceptors);
     }
 
     private void setupTemplate(final RestTemplate restTemplate) {
-        setLoggingInterceptor(restTemplate);
-        setMappingConverter(restTemplate);
+        setLoggingInterceptors(restTemplate);
+        setMappingConverters(restTemplate);
     }
 
     boolean hasProxySet() {
-        return !Strings.isNullOrEmpty(this.host) && this.port > 0;
+        return !Strings.isNullOrEmpty(proxyHost) && proxyPort > 0;
     }
 
     @Override
     public String toString() {
-        return String.format("ProxyConfig {host='%s', port=%d}", this.host, this.port);
+        return String.format("ProxyConfig {host='%s', port=%d}", proxyHost, proxyPort);
     }
 }
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/http/X509CRLHttpMessageConverter.java b/core/src/main/java/dev/fitko/fitconnect/core/http/X509CRLHttpMessageConverter.java
deleted file mode 100644
index bc41d6f3650053304b8306a09aaccb4bb5300503..0000000000000000000000000000000000000000
--- a/core/src/main/java/dev/fitko/fitconnect/core/http/X509CRLHttpMessageConverter.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package dev.fitko.fitconnect.core.http;
-
-import org.springframework.http.HttpInputMessage;
-import org.springframework.http.HttpOutputMessage;
-import org.springframework.http.MediaType;
-import org.springframework.http.converter.AbstractHttpMessageConverter;
-import org.springframework.http.converter.HttpMessageNotReadableException;
-import org.springframework.http.converter.HttpMessageNotWritableException;
-import org.springframework.lang.NonNull;
-
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509CRL;
-
-public class X509CRLHttpMessageConverter extends AbstractHttpMessageConverter<X509CRL> {
-
-    public X509CRLHttpMessageConverter() {
-        super(new MediaType("application", "pkix-crl"));
-    }
-
-    @Override
-    protected boolean supports(@NonNull final Class<?> clazz) {
-        return X509CRL.class == clazz;
-    }
-
-    @Override
-    protected X509CRL readInternal(@NonNull final Class<? extends X509CRL> clazz, @NonNull final HttpInputMessage inputMessage)
-            throws HttpMessageNotReadableException {
-        try {
-            final var cf = CertificateFactory.getInstance("X.509");
-            return (X509CRL) cf.generateCRL(inputMessage.getBody());
-        } catch (final Exception e) {
-            throw new HttpMessageNotReadableException("CertificateFactory of type X.509 could not be created", inputMessage);
-        }
-    }
-
-    @Override
-    protected void writeInternal(@NonNull final X509CRL x509CRL, @NonNull final HttpOutputMessage outputMessage)
-            throws HttpMessageNotWritableException {
-        throw new HttpMessageNotWritableException("Writing X509CRL is not supported in ProxyRestTemplate");
-    }
-
-}
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/keys/PublicKeyService.java b/core/src/main/java/dev/fitko/fitconnect/core/keys/PublicKeyService.java
index 40c773fd25eed619654524a758958179d6856829..2ab47f490335b6d9aee31e5e76e7274b0867de14 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/keys/PublicKeyService.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/keys/PublicKeyService.java
@@ -57,7 +57,7 @@ public class PublicKeyService implements KeyService {
     public RSAKey getPublicEncryptionKey(final UUID destinationId) {
         final Destination destination = submissionService.getDestination(destinationId);
         final String destinationUrl = config.getDestinationsKeyEndpoint();
-        final ApiJwk publicKey = performRequest(destinationUrl, ApiJwk.class, destinationId, destination.getEncryptionKid());
+        final ApiJwk publicKey = performRequest(destinationUrl, ApiJwk.class, getHeaders(), destinationId, destination.getEncryptionKid());
         final RSAKey rsaKey = toRSAKey(publicKey);
         validateEncryptionKey(rsaKey);
         return rsaKey;
@@ -66,25 +66,33 @@ public class PublicKeyService implements KeyService {
     @Override
     public RSAKey getPublicSignatureKey(final UUID destinationId, final String keyId) {
         final String destinationUrl = config.getDestinationsKeyEndpoint();
-        final ApiJwk signatureKey = performRequest(destinationUrl, ApiJwk.class, destinationId, keyId);
+        final ApiJwk signatureKey = performRequest(destinationUrl, ApiJwk.class, getHeaders(), destinationId, keyId);
         final RSAKey rsaKey = toRSAKey(signatureKey);
         validateSignatureKey(rsaKey);
         return rsaKey;
     }
 
     @Override
-    public RSAKey getSubmissionServiceSignatureKey(final String keyId) {
+    public RSAKey getSubmissionServicePublicKey(final String keyId) {
         final String submissionServiceUrl = config.getSubmissionServiceWellKnownKeysEndpoint();
-        final ApiJwkSet wellKnownKeys = performRequest(submissionServiceUrl, ApiJwkSet.class);
+        final ApiJwkSet wellKnownKeys = performRequest(submissionServiceUrl, ApiJwkSet.class, getHeaders());
         final RSAKey signatureKey = filterKeysById(keyId, wellKnownKeys.getKeys());
         validateSignatureKey(signatureKey);
         return signatureKey;
     }
 
     @Override
-    public RSAKey getPortalSignatureKey(final String keyId) {
+    public RSAKey getPortalPublicKey(final String keyId) {
         final String portalUrl = config.getSelfServicePortalWellKnownKeysEndpoint();
-        final ApiJwkSet wellKnownKeys = performRequest(portalUrl, ApiJwkSet.class);
+        final ApiJwkSet wellKnownKeys = performRequest(portalUrl, ApiJwkSet.class, getHeaders());
+        final RSAKey signatureKey = filterKeysById(keyId, wellKnownKeys.getKeys());
+        validateSignatureKey(signatureKey);
+        return signatureKey;
+    }
+    @Override
+    public RSAKey getWellKnownKeysForSubmissionUrl(final String url, final String keyId) {
+        final var requestUrl = !url.endsWith("/") ? url + config.getWellKnownKeysPath() : url;
+        final ApiJwkSet wellKnownKeys = performRequest(requestUrl, ApiJwkSet.class,  getHeadersWithoutAuth(), keyId);
         final RSAKey signatureKey = filterKeysById(keyId, wellKnownKeys.getKeys());
         validateSignatureKey(signatureKey);
         return signatureKey;
@@ -126,8 +134,7 @@ public class PublicKeyService implements KeyService {
         }
     }
 
-    private <T> T performRequest(final String url, final Class<T> responseType, final Object... params) {
-        final HttpHeaders headers = getHeaders();
+    private <T> T performRequest(final String url, final Class<T> responseType, final HttpHeaders headers, final Object... params) {
         final HttpEntity<String> entity = new HttpEntity<>(headers);
         try {
             return restTemplate.exchange(url, HttpMethod.GET, entity, responseType, params).getBody();
@@ -136,11 +143,15 @@ public class PublicKeyService implements KeyService {
         }
     }
 
-    private HttpHeaders getHeaders() {
+    private HttpHeaders getHeadersWithoutAuth() {
         final var headers = new HttpHeaders();
-        headers.setBearerAuth(authService.getCurrentToken().getAccessToken());
         headers.setContentType(MediaType.APPLICATION_JSON);
         headers.setAcceptCharset(List.of(StandardCharsets.UTF_8));
         return headers;
     }
+    private HttpHeaders getHeaders() {
+        final var headers = getHeadersWithoutAuth();
+        headers.setBearerAuth(authService.getCurrentToken().getAccessToken());
+        return headers;
+    }
 }
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/routing/RouteVerifier.java b/core/src/main/java/dev/fitko/fitconnect/core/routing/RouteVerifier.java
new file mode 100644
index 0000000000000000000000000000000000000000..b1920628b492018dd022329d4126b9edc7c6f94c
--- /dev/null
+++ b/core/src/main/java/dev/fitko/fitconnect/core/routing/RouteVerifier.java
@@ -0,0 +1,195 @@
+package dev.fitko.fitconnect.core.routing;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.RSASSAVerifier;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import dev.fitko.fitconnect.api.domain.model.route.Route;
+import dev.fitko.fitconnect.api.domain.model.route.RouteDestination;
+import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
+import dev.fitko.fitconnect.api.exceptions.InvalidKeyException;
+import dev.fitko.fitconnect.api.exceptions.RestApiException;
+import dev.fitko.fitconnect.api.exceptions.ValidationException;
+import dev.fitko.fitconnect.api.services.keys.KeyService;
+import dev.fitko.fitconnect.api.services.routing.RoutingVerificationService;
+import dev.fitko.fitconnect.api.services.validation.ValidationService;
+import dev.fitko.fitconnect.core.util.Strings;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static dev.fitko.fitconnect.api.domain.validation.ValidationResult.error;
+import static dev.fitko.fitconnect.api.domain.validation.ValidationResult.ok;
+
+public class RouteVerifier implements RoutingVerificationService {
+
+    private static final JsonMapper MAPPER = getConfiguredJsonMapper();
+    private final KeyService keyService;
+    private final ValidationService validationService;
+
+    public RouteVerifier(final KeyService keyService, final ValidationService validationService) {
+        this.keyService = keyService;
+        this.validationService = validationService;
+    }
+
+    @Override
+    public ValidationResult validateRouteDestinations(final List<Route> routes, final String requestedServiceIdentifier, final String requestedRegion) {
+        return routes.stream()
+                .map(route -> validateRoute(route, requestedServiceIdentifier, requestedRegion))
+                .filter(ValidationResult::hasError)
+                .findFirst().orElse(ValidationResult.ok());
+    }
+
+    private ValidationResult validateRoute(final Route route, final String requestedServiceIdentifier, final String requestedRegion) {
+        try {
+            validateDestinationSignature(route, requestedServiceIdentifier, requestedRegion);
+            validateDestinationParameterSignature(route);
+            return ok();
+        } catch (final ValidationException e) {
+            return error(e);
+        } catch (final InvalidKeyException e) {
+            return error(new ValidationException("Public signature key is invalid: " + e.getMessage()));
+        } catch (final RestApiException e) {
+            return error(new ValidationException("Could not retrieve public signature key: " + e.getMessage()));
+        } catch (final ParseException | JOSEException | JsonProcessingException e) {
+            return error(new ValidationException("Signature processing failed: " + e.getMessage()));
+        }
+    }
+
+    private void validateDestinationParameterSignature(final Route route) throws JOSEException, ParseException, JsonProcessingException {
+        final SignedJWT completedSignature = combineDetachedSignatureWithPayload(route);
+        final RSAKey publicSignatureKey = loadPublicKey(route, completedSignature);
+        if (!completedSignature.verify(new RSASSAVerifier(publicSignatureKey))) {
+            throw new ValidationException("Invalid destination parameter signature for route " + route.getDestinationId());
+        }
+        checkHeaderAlgorithm(completedSignature.getHeader());
+    }
+
+    private RSAKey loadPublicKey(final Route route, final SignedJWT completedSignature) {
+        final String keyId = completedSignature.getHeader().getKeyID();
+        final String submissionUrl = route.getDestinationParameters().getSubmissionUrl();
+        return keyService.getWellKnownKeysForSubmissionUrl(submissionUrl, keyId);
+    }
+
+    private SignedJWT combineDetachedSignatureWithPayload(final Route route) throws ParseException, JsonProcessingException {
+        final SignedJWT detachedSignature = SignedJWT.parse(route.getDestinationParametersSignature());
+        final Base64URL encodedDetachedPayloadPart = getBase64EncodedDetachedPayload(route);
+        final Base64URL headerPart = detachedSignature.getHeader().getParsedBase64URL();
+        final Base64URL signaturePart = detachedSignature.getSignature();
+        return new SignedJWT(headerPart, encodedDetachedPayloadPart, signaturePart);
+
+    }
+
+    private Base64URL getBase64EncodedDetachedPayload(final Route route) throws JsonProcessingException {
+        final RouteDestination detachedPayload = route.getDestinationParameters();
+        final String cleanedDetachedPayload = Strings.cleanNonPrintableChars(MAPPER.writeValueAsString(detachedPayload));
+        // FIXME email vs. eMail difference between DVDV and SubmissionAPI -> https://git.fitko.de/fit-connect/planning/-/issues/601
+        return Base64URL.encode(cleanedDetachedPayload.replace("eMail", "email").getBytes(StandardCharsets.UTF_8));
+    }
+
+    private void validateDestinationSignature(final Route route, final String requestedServiceIdentifier, final String requestedRegion) throws ParseException, JOSEException, JsonProcessingException {
+        final SignedJWT signature = SignedJWT.parse(route.getDestinationSignature());
+
+        final JWSHeader header = signature.getHeader();
+        final JWTClaimsSet claims = signature.getJWTClaimsSet();
+        final String submissionUrl = route.getDestinationParameters().getSubmissionUrl();
+
+        checkHeaderAlgorithm(header);
+        validatePayloadSchema(claims);
+        checkMatchingSubmissionHost(claims, submissionUrl);
+        checkExpectedServices(claims, requestedServiceIdentifier, requestedRegion);
+        validateAgainstPublicKey(signature, header.getKeyID());
+    }
+
+    private void validatePayloadSchema(final JWTClaimsSet claims) {
+        final ValidationResult validationResult = validationService.validateDestinationSchema(claims.toJSONObject());
+        if (validationResult.hasError()) {
+            throw new ValidationException(validationResult.getError().getMessage(), validationResult.getError());
+        }
+    }
+
+    private void validateAgainstPublicKey(final SignedJWT signature, final String keyId) throws JOSEException {
+        final RSAKey portalPublicKey = keyService.getPortalPublicKey(keyId);
+        if (!signature.verify(new RSASSAVerifier(portalPublicKey))) {
+            throw new ValidationException("Invalid destination signature for public key id " + keyId);
+        }
+    }
+
+    private void checkExpectedServices(final JWTClaimsSet claims, final String requestedServiceIdentifier, final String requestedRegion) {
+        final Map services = (Map) ((ArrayList<?>) (claims.getClaim("services"))).get(0);
+        final List<String> areaIds = mapIdentifiersToNumericIds(services, "gebietIDs");
+        final List<String> serviceIds = mapIdentifiersToNumericIds(services, "leistungIDs");
+        if (requestedRegion != null && !areaIds.contains(getIdFromIdentifier(requestedRegion))) {
+            throw new ValidationException("Requested region '" + requestedRegion + "' is not supported by the destinations services");
+        }
+        if (!serviceIds.contains(getIdFromIdentifier(requestedServiceIdentifier))) {
+            throw new ValidationException("Requested service identifier '" + requestedServiceIdentifier + "' is not supported by the destinations services");
+        }
+    }
+
+    private static List<String> mapIdentifiersToNumericIds(final Map services, final String claim) {
+        return ((List<String>) services.get(claim)).stream().map(RouteVerifier::getIdFromIdentifier).collect(Collectors.toList());
+    }
+
+    private static String getIdFromIdentifier(final String identifier) {
+        if (isNumericId(identifier)) {
+            return identifier;
+        }
+        return Arrays.stream(identifier.split(":"))
+                .reduce((first, second) -> second)
+                .orElse(null);
+    }
+
+    private static boolean isNumericId(final String identifier) {
+        return Pattern.compile("\\d+").matcher(identifier).matches();
+    }
+
+    private void checkHeaderAlgorithm(final JWSHeader header) {
+        if (!header.getAlgorithm().equals(JWSAlgorithm.PS512)) {
+            throw new ValidationException("Algorithm in signature header is not " + JWSAlgorithm.PS512);
+        }
+    }
+
+    private void checkMatchingSubmissionHost(final JWTClaimsSet claims, final String submissionUrl) throws ParseException {
+        final String submissionHostClaim = claims.getStringClaim("submissionHost");
+        final String submissionUrlHost = getHostFromSubmissionUrl(submissionUrl);
+        if (!submissionUrlHost.equals(submissionHostClaim)) {
+            throw new ValidationException("Submission host does not match destinationParameters submission url " + submissionHostClaim);
+        }
+    }
+
+    private static String getHostFromSubmissionUrl(final String submissionUrl) {
+        if (submissionUrl == null) {
+            throw new ValidationException("SubmissionUrl must not be null");
+        }
+        try {
+            return URI.create(submissionUrl).getHost();
+        } catch (final IllegalArgumentException e) {
+            throw new ValidationException("SubmissionUrl could not be parsed", e);
+        }
+    }
+
+    private static JsonMapper getConfiguredJsonMapper() {
+        return JsonMapper.builder()
+                .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true)
+                .configure(SerializationFeature.INDENT_OUTPUT, false)
+                .serializationInclusion(JsonInclude.Include.NON_NULL)
+                .build();
+    }
+}
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/routing/RoutingApiService.java b/core/src/main/java/dev/fitko/fitconnect/core/routing/RoutingApiService.java
new file mode 100644
index 0000000000000000000000000000000000000000..5639d27979c32fbed0d376736b8bbf82d4f1688c
--- /dev/null
+++ b/core/src/main/java/dev/fitko/fitconnect/core/routing/RoutingApiService.java
@@ -0,0 +1,107 @@
+package dev.fitko.fitconnect.core.routing;
+
+import dev.fitko.fitconnect.api.config.ApplicationConfig;
+import dev.fitko.fitconnect.api.domain.model.route.AreaResult;
+import dev.fitko.fitconnect.api.domain.model.route.RouteResult;
+import dev.fitko.fitconnect.api.exceptions.RestApiException;
+import dev.fitko.fitconnect.api.services.routing.RoutingService;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import static java.util.function.Predicate.not;
+
+public class RoutingApiService implements RoutingService {
+
+    private final RestTemplate restTemplate;
+    private final ApplicationConfig config;
+
+    public RoutingApiService(final ApplicationConfig config, final RestTemplate restTemplate) {
+        this.config = config;
+        this.restTemplate = restTemplate;
+    }
+
+    @Override
+    public AreaResult getAreas(final List<String> searchExpressions, final int offset, final int limit) {
+
+        final String url = config.getAreaEndpoint();
+        final HttpHeaders headers = getHttpHeaders(MediaType.parseMediaType("application/problem+json"));
+        final HttpEntity<String> entity = new HttpEntity<>(headers);
+
+        final UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(url);
+        searchExpressions.forEach(search -> uriBuilder.queryParam("areaSearchexpression", search));
+        uriBuilder.queryParam("offset", offset);
+        uriBuilder.queryParam("limit", limit);
+
+        try {
+            return restTemplate.exchange(uriBuilder.toUriString(), HttpMethod.GET, entity, AreaResult.class).getBody();
+        } catch (final RestClientException e) {
+            throw new RestApiException("Area query failed", e);
+        }
+    }
+
+    @Override
+    public RouteResult getRoutes(final String leikaKey, final String ars, final String ags, final String areaId, final int offset, final int limit) {
+
+        testIfSearchCriterionIsSet(ars, ags, areaId);
+
+        final String url = config.getRoutesEndpoint();
+        final HttpHeaders headers = getHttpHeaders(MediaType.APPLICATION_JSON);
+        final HttpEntity<String> entity = new HttpEntity<>(headers);
+
+        final UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(url);
+        uriBuilder.queryParam("leikaKey", leikaKey);
+
+        if (ars != null) {
+            uriBuilder.queryParam("ars", ars);
+        }
+        if (ags != null) {
+            uriBuilder.queryParam("ags", ags);
+        }
+        if (areaId != null) {
+            uriBuilder.queryParam("areaId", areaId);
+        }
+
+        uriBuilder.queryParam("offset", offset);
+        uriBuilder.queryParam("limit", limit);
+
+        try {
+            return restTemplate.exchange(uriBuilder.toUriString(), HttpMethod.GET, entity, RouteResult.class).getBody();
+        } catch (final RestClientException e) {
+            throw new RestApiException("Route query failed", e);
+        }
+    }
+
+    private static void testIfSearchCriterionIsSet(final String ars, final String ags, final String areaId) {
+        final List<String> areaSearchCriteria = Arrays.asList(ars, ags, areaId);
+
+        if(areaSearchCriteria.stream().allMatch(CriterionEmpty())){
+            throw new RestApiException("At least one search criterion out of ags, ars or areaId must be set");
+        }
+
+        if(areaSearchCriteria.stream().filter(not(CriterionEmpty())).collect(Collectors.toSet()).size() > 1){
+            throw new RestApiException("Only one of ars, ags or areaId must be specified.");
+        }
+    }
+
+    private static Predicate<String> CriterionEmpty() {
+        return criterion -> criterion == null || criterion.isEmpty();
+    }
+
+    private HttpHeaders getHttpHeaders(final MediaType mediaType) {
+        final var headers = new HttpHeaders();
+        headers.setAccept(List.of(mediaType));
+        headers.setAcceptCharset(List.of(StandardCharsets.UTF_8));
+        return headers;
+    }
+}
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java b/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java
index 873f2a82f7c37783798584cbf20224c29e1ba9d8..88433e047792bb8e919a386d33a4ba6b1b53ee3c 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java
@@ -3,6 +3,7 @@ package dev.fitko.fitconnect.core.schema;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import dev.fitko.fitconnect.api.config.SchemaConfig;
+import dev.fitko.fitconnect.api.domain.schema.SchemaResources;
 import dev.fitko.fitconnect.api.exceptions.InitializationException;
 import dev.fitko.fitconnect.api.exceptions.SchemaNotFoundException;
 import dev.fitko.fitconnect.api.services.schema.SchemaProvider;
@@ -16,38 +17,50 @@ import java.net.URI;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 public class SchemaResourceProvider implements SchemaProvider {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(SchemaResourceProvider.class);
 
+    private static final Pattern ALLOWED_SCHEMA_PATTERN = Pattern.compile("1.\\d+.\\d+");
+
     private final Map<URI, String> setSchemas;
 
     private final Map<URI, String> metadataSchemas;
 
+    private final Map<URI, String> destinationSchemas;
+
     private static final ObjectMapper MAPPER = new ObjectMapper();
 
-    public SchemaResourceProvider(final List<String> setSchemaFiles, final List<String> metadataSchemaFiles) {
+    public SchemaResourceProvider(final SchemaResources schemaResources) {
         setSchemas = new HashMap<>();
         metadataSchemas = new HashMap<>();
-        populateSetSchemas(setSchemaFiles);
-        populateMetadataSchemas(metadataSchemaFiles);
+        destinationSchemas = new HashMap<>();
+        populateSetSchemas(schemaResources.getSetSchemaPaths());
+        populateMetadataSchemas(schemaResources.getMetadataSchemaPaths());
+        populateDestinationSchemas(schemaResources.getDestinationSchemaPaths());
     }
 
-    private void populateMetadataSchemas(final List<String> metadataSchemaFiles) {
+    private void populateMetadataSchemas(final List<String> metadataSchemaPaths) {
         LOGGER.info("Initializing metadata schemas");
-        getResourceFiles(metadataSchemaFiles).forEach(this::addMetadataSchema);
+        getResourceFiles(metadataSchemaPaths).forEach(this::addMetadataSchema);
     }
 
-    private void populateSetSchemas(final List<String> setSchemaFiles) {
+    private void populateSetSchemas(final List<String> setSchemaPaths) {
         LOGGER.info("Initializing set schemas");
-        getResourceFiles(setSchemaFiles).forEach(this::addSetSchema);
+        getResourceFiles(setSchemaPaths).forEach(this::addSetSchema);
+    }
+
+    private void populateDestinationSchemas(final List<String> destinationSchemaPaths) {
+        LOGGER.info("Initializing destination schemas");
+        getResourceFiles(destinationSchemaPaths).forEach(this::addDestinationSchema);
     }
 
     @Override
     public boolean isAllowedSetSchema(final URI schemaUri) {
-        return schemaHasMajorVersion(schemaUri, "1");
+        return schemaVersionMatchesPattern(schemaUri, ALLOWED_SCHEMA_PATTERN);
     }
 
     @Override
@@ -74,6 +87,15 @@ public class SchemaResourceProvider implements SchemaProvider {
         return schema;
     }
 
+    @Override
+    public String loadDestinationSchema(final URI schemaUri) throws SchemaNotFoundException {
+        final String schema = destinationSchemas.get(schemaUri);
+        if (schema == null) {
+            throw new SchemaNotFoundException("Destination schema " + schemaUri.toString() + " is not available.");
+        }
+        return schema;
+    }
+
     private void addSetSchema(final String schema) {
         setSchemas.put(readIdFromSchema(schema), schema);
     }
@@ -82,6 +104,10 @@ public class SchemaResourceProvider implements SchemaProvider {
         metadataSchemas.put(readIdFromSchema(schema), schema);
     }
 
+    private void addDestinationSchema(final String schema) {
+        destinationSchemas.put(readIdFromSchema(schema), schema);
+    }
+
     private URI readIdFromSchema(final String schema) {
         try {
             return URI.create(MAPPER.readTree(schema).get("$id").asText());
@@ -102,8 +128,9 @@ public class SchemaResourceProvider implements SchemaProvider {
             throw new InitializationException("Could not read schema file " + schemaFile, e);
         }
     }
-    private boolean schemaHasMajorVersion(final URI schemaUri, final String majorVersion) {
+
+    private boolean schemaVersionMatchesPattern(final URI schemaUri, final Pattern pattern) {
         final String schemaVersion = schemaUri.getPath().split("/")[3];
-        return schemaVersion.startsWith(majorVersion + ".");
+        return pattern.matcher(schemaVersion).matches();
     }
 }
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/submission/SubmissionApiService.java b/core/src/main/java/dev/fitko/fitconnect/core/submission/SubmissionApiService.java
index 35725f737416b8a3f84564431ab82e6f0e1a973d..c7f8ba441b92e2c093aa7751671d4ecb3c1de7c5 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/submission/SubmissionApiService.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/submission/SubmissionApiService.java
@@ -101,8 +101,7 @@ public class SubmissionApiService implements SubmissionService {
 
     @Override
     public Submission sendSubmission(final SubmitSubmission submission) {
-        final Map<String, Object> params = new HashMap<>();
-        params.put("submissionId", submission.getSubmissionId());
+        final Map<String, Object> params = Map.of("submissionId", submission.getSubmissionId());
         final RequestSettings requestSettings = RequestSettings.builder()
                 .url(config.getSubmissionEndpoint())
                 .method(HttpMethod.PUT)
@@ -139,8 +138,7 @@ public class SubmissionApiService implements SubmissionService {
 
     @Override
     public Submission getSubmission(final UUID submissionId) {
-        final Map<String, Object> params = new HashMap<>();
-        params.put("submissionId", submissionId);
+        final Map<String, Object> params = Map.of("submissionId", submissionId);
         final RequestSettings requestSettings = RequestSettings.builder()
                 .url(config.getSubmissionEndpoint())
                 .method(HttpMethod.GET)
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/util/StopWatch.java b/core/src/main/java/dev/fitko/fitconnect/core/util/StopWatch.java
index f2d50ef11cb3e7fe1fa4687ca4815f3d75d133ab..7c1eb4a47e6dc5da9c769dc2c2921c5f8810be07 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/util/StopWatch.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/util/StopWatch.java
@@ -5,7 +5,8 @@ package dev.fitko.fitconnect.core.util;
  */
 public final class StopWatch {
 
-    private StopWatch(){}
+    private StopWatch() {
+    }
 
     /**
      * Get current system time in ms.
@@ -20,14 +21,13 @@ public final class StopWatch {
      * Formats end time based on start time (end - start) in a readable format (e.g. sec:ms)
      *
      * @param startTime start time of the measured call
-     *
      * @return formatted elapsed time since start
      */
-    public static String stopWithFormattedTime(final long startTime){
+    public static String stopWithFormattedTime(final long startTime) {
         return Formatter.formatMillis(stop(startTime));
     }
 
-    private static long stop(final long startTime){
+    private static long stop(final long startTime) {
         return System.currentTimeMillis() - startTime;
     }
 }
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/util/Strings.java b/core/src/main/java/dev/fitko/fitconnect/core/util/Strings.java
index 9a722f69143763dd7488bdb0ee2290ce3d87025d..c422a8f36da209601d4479ace25d8e0670a73ef0 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/util/Strings.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/util/Strings.java
@@ -7,6 +7,7 @@ public final class Strings {
 
     /**
      * Tests a given string on null or empty
+     *
      * @param s string to test
      * @return true if the string is null OR empty, false if both conditions do not apply
      */
@@ -16,6 +17,7 @@ public final class Strings {
 
     /**
      * Tests a given string on not null and not empty
+     *
      * @param s string to test
      * @return true if the string is not null AND not empty, false if one condition does not apply
      */
@@ -23,4 +25,14 @@ public final class Strings {
         return !isNullOrEmpty(s);
     }
 
+    /**
+     * Replace all non-printable control characters like \t, \r, \n and spaces from the given s.
+     *
+     * @param s string that should be cleaned
+     * @return cleaned string without non-printable chars
+     */
+    public static String cleanNonPrintableChars(final String s) {
+        return s.replaceAll("\\p{C}", "");
+    }
+
 }
diff --git a/core/src/main/java/dev/fitko/fitconnect/core/validation/DefaultValidationService.java b/core/src/main/java/dev/fitko/fitconnect/core/validation/DefaultValidationService.java
index 52a322aef04330c0bebaa171fc04df397ded32c2..37455f13cf52a0e8c6b8e053712ae8ce11da4454 100644
--- a/core/src/main/java/dev/fitko/fitconnect/core/validation/DefaultValidationService.java
+++ b/core/src/main/java/dev/fitko/fitconnect/core/validation/DefaultValidationService.java
@@ -33,6 +33,7 @@ import org.xml.sax.XMLReader;
 
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.parsers.SAXParserFactory;
+
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.net.InetSocketAddress;
@@ -47,6 +48,7 @@ import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -57,7 +59,9 @@ public class DefaultValidationService implements ValidationService {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(DefaultValidationService.class);
     private static final ObjectMapper MAPPER = new ObjectMapper().setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));
-    private static final JsonSchemaFactory SCHEMA_FACTORY = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
+    private static final JsonSchemaFactory SCHEMA_FACTORY_DRAFT_2020 = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
+
+    private static final JsonSchemaFactory SCHEMA_FACTORY_DRAFT_2007 = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
     public static final String VALID_SCHEMA_URL_EXPRESSION = "https://schema\\.fitko\\.de/fit-connect/metadata/1\\.\\d+\\.\\d+/metadata.schema.json";
 
     private final MessageDigestService messageDigestService;
@@ -96,7 +100,7 @@ public class DefaultValidationService implements ValidationService {
             final JsonNode inputNode = MAPPER.readTree(setEventPayload);
             final URI schemaUri = URI.create(inputNode.get(EventClaimFields.CLAIM_SCHEMA).asText());
             if (schemaProvider.isAllowedSetSchema(schemaUri)) {
-                return validateJsonSchema(schemaProvider.loadLatestSetSchema(), inputNode);
+                return validate2020JsonSchema(schemaProvider.loadLatestSetSchema(), inputNode);
             } else {
                 return ValidationResult.error(new SchemaNotFoundException("SET payload schema not supported: " + schemaUri));
             }
@@ -115,7 +119,19 @@ public class DefaultValidationService implements ValidationService {
         try {
             final String metadataJson = MAPPER.writeValueAsString(metadata);
             final JsonNode inputNode = MAPPER.readTree(metadataJson);
-            return validateJsonSchema(schemaProvider.loadMetadataSchema(config.getMetadataSchemaWriteVersion()), inputNode);
+            return validate2020JsonSchema(schemaProvider.loadMetadataSchema(config.getMetadataSchemaWriteVersion()), inputNode);
+        } catch (final JsonProcessingException e) {
+            return ValidationResult.error(e);
+        }
+    }
+
+    @Override
+    public ValidationResult validateDestinationSchema(final Map<String, Object> destinationPayload) {
+        try {
+            final String destinationPayloadJson = MAPPER.writeValueAsString(destinationPayload);
+            final JsonNode inputNode = MAPPER.readTree(destinationPayloadJson);
+            final String schema = schemaProvider.loadDestinationSchema(config.getDestinationSchemaVersion());
+            return returnValidationResult(SCHEMA_FACTORY_DRAFT_2007.getSchema(schema).validate(inputNode));
         } catch (final JsonProcessingException e) {
             return ValidationResult.error(e);
         }
@@ -157,25 +173,25 @@ public class DefaultValidationService implements ValidationService {
     }
 
     @Override
-    public ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret) {
+    public ValidationResult validateCallback(final String hmac, final Long timestamp, final String httpBody, final String callbackSecret) {
 
-        ZonedDateTime providedTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
-        ZonedDateTime currentTimeFiveMinutesAgo = ZonedDateTime.now().minusMinutes(5);
+        final ZonedDateTime providedTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault());
+        final ZonedDateTime currentTimeFiveMinutesAgo = ZonedDateTime.now().minusMinutes(5);
 
         if (providedTime.isBefore(currentTimeFiveMinutesAgo)) {
             return ValidationResult.error(new ValidationException("Timestamp provided by callback is expired."));
         }
 
-        String expectedHmac = this.messageDigestService.calculateHMAC(timestamp + "." + httpBody, callbackSecret);
+        final String expectedHmac = messageDigestService.calculateHMAC(timestamp + "." + httpBody, callbackSecret);
         if (!hmac.equals(expectedHmac)) {
-            return  ValidationResult.error(new ValidationException("HMAC provided by callback does not match the expected result."));
+            return ValidationResult.error(new ValidationException("HMAC provided by callback does not match the expected result."));
         }
 
         return ValidationResult.ok();
     }
 
-    private ValidationResult validateJsonSchema(final String schema, final JsonNode inputNode) {
-        return returnValidationResult(SCHEMA_FACTORY.getSchema(schema).validate(inputNode));
+    private ValidationResult validate2020JsonSchema(final String schema, final JsonNode inputNode) {
+        return returnValidationResult(SCHEMA_FACTORY_DRAFT_2020.getSchema(schema).validate(inputNode));
     }
 
     private void validateKey(final RSAKey publicKey, final KeyOperation purpose) throws JWKValidationException, CertificateEncodingException {
diff --git a/core/src/main/resources/destination-schema/destination_schema.json b/core/src/main/resources/destination-schema/destination_schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..db8a5dcd9e751f5f2206b7e854fc4446139e00da
--- /dev/null
+++ b/core/src/main/resources/destination-schema/destination_schema.json
@@ -0,0 +1,150 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema",
+  "$id": "https://schema.fitko.de/fit-connect/xzufi/destination.schema.json",
+  "type": "object",
+  "title": "FIT-Connect Adressierungsinformationen",
+  "description": "Payload der signierten Adressierungsinformationen",
+  "examples": [
+    {
+      "submissionHost": "submission-api-testing.fit-connect.fitko.dev",
+      "iss": "https://portal.auth-testing.fit-connect.fitko.dev",
+      "services": [
+        {
+          "gebietIDs": [
+            "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs:12345"
+          ],
+          "leistungIDs": [
+            "urn:de:fim:leika:leistung:100"
+          ]
+        }
+      ],
+      "destinationId": "0c438057-ec3e-4ce0-b154-1683b5d3c2e8",
+      "iat": 1637685592,
+      "jti": "ae37e58b-e280-4706-b99e-738f24c8d98f"
+    }
+  ],
+  "required": [
+    "submissionHost",
+    "iss",
+    "services",
+    "destinationId",
+    "iat",
+    "jti"
+  ],
+  "properties": {
+    "submissionHost": {
+      "type": "string",
+      "title": "Submission-Host",
+      "description": "Technischer Bezeichner für die Host-Adresse des für eine bestimmte Destination-ID zuständigen Zustelldienstes.",
+      "examples": [
+        "submission-api-testing.fit-connect.fitko.dev"
+      ]
+    },
+    "iss": {
+      "type": "string",
+      "title": "Issuer des JWT",
+      "description": "URL des Self-Service-Portals",
+      "examples": [
+        "https://portal.auth-testing.fit-connect.fitko.dev"
+      ]
+    },
+    "services": {
+      "type": "array",
+      "title": "Verwaltungsleistungen",
+      "description": "Die Liste der vom Zustellpunkt angebotenen Verwaltungsleistungen und geographischen Zuordnungen (siehe gebietIDs und leistungIDs)",
+      "examples": [
+        [
+          {
+            "gebietIDs": [
+              "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs:12345"
+            ],
+            "leistungIDs": [
+              "urn:de:fim:leika:leistung:100"
+            ]
+          }
+        ]
+      ],
+      "items": {
+        "type": "object",
+        "title": "Verwaltungsleistung",
+        "description": "Eine vom Zustellpunkt angebotene Verwaltungsleistung und geographischen Zuordnungen (siehe gebietIDs und leistungIDs)",
+        "examples": [
+          {
+            "gebietIDs": [
+              "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs:12345"
+            ],
+            "leistungIDs": [
+              "urn:de:fim:leika:leistung:100"
+            ]
+          }
+        ],
+        "required": [
+          "gebietIDs",
+          "leistungIDs"
+        ],
+        "properties": {
+          "gebietIDs": {
+            "type": "array",
+            "title": "gebietIDs",
+            "description": "Eine Liste der zur Zuständigkeitsabbildung gehörigen Gebiets-IDs",
+            "examples": [
+              [
+                "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs:12345"
+              ]
+            ],
+            "items": {
+              "type": "string",
+              "title": "gebietID",
+              "description": "Gebiets-ID als URN",
+              "examples": [
+                "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs:12345"
+              ]
+            }
+          },
+          "leistungIDs": {
+            "type": "array",
+            "title": "leistungIDs",
+            "description": "Eine Liste der zur Zuständigkeitsabbildung gehörigen Leistungs-IDs",
+            "examples": [
+              [
+                "urn:de:fim:leika:leistung:100"
+              ]
+            ],
+            "items": {
+              "type": "string",
+              "title": "leistungID",
+              "description": "Leistungs-ID als URN",
+              "examples": [
+                "urn:de:fim:leika:leistung:100"
+              ]
+            }
+          }
+        }
+      }
+    },
+    "destinationId": {
+      "type": "string",
+      "title": "Destination-ID",
+      "description": "Die für den Zustellpunkt vergebene ID",
+      "examples": [
+        "0c438057-ec3e-4ce0-b154-1683b5d3c2e8"
+      ]
+    },
+    "iat": {
+      "type": "integer",
+      "title": "Zeitpunkt der Signaturerstellung (issued at)",
+      "description": "Zeitpunkt der Signaturerstellung als Unix Timestamp",
+      "examples": [
+        1637685592
+      ]
+    },
+    "jti": {
+      "type": "string",
+      "title": "JWT Token ID",
+      "description": "Eindeutige ID des JWT",
+      "examples": [
+        "ae37e58b-e280-4706-b99e-738f24c8d98f"
+      ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/RestEndpointBase.java b/core/src/test/java/dev/fitko/fitconnect/core/RestEndpointBase.java
index 47c0b9c9f2fd43743b63abd11715b85bde2b5ceb..7ddaeffe04914a28eeea144648c545bffaf248bd 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/RestEndpointBase.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/RestEndpointBase.java
@@ -30,7 +30,7 @@ public abstract class RestEndpointBase {
     protected ApplicationConfig getTestConfig(final String fakeBaseUrl) {
 
         final var envName = new EnvironmentName("TESTING");
-        final var environments = Map.of(envName, new Environment(fakeBaseUrl, fakeBaseUrl,fakeBaseUrl,fakeBaseUrl, false));
+        final var environments = Map.of(envName, new Environment(fakeBaseUrl, fakeBaseUrl, fakeBaseUrl, fakeBaseUrl, false));
 
         return ApplicationConfig.builder()
                 .environments(environments)
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/events/EventLogVerifierTest.java b/core/src/test/java/dev/fitko/fitconnect/core/events/EventLogVerifierTest.java
index f2cc9c9d4c9dd5a5217b0fb2028950bd4e4c9588..d4ccb711db8fc3df8e10a5bc368e30e6e0b79ea4 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/events/EventLogVerifierTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/events/EventLogVerifierTest.java
@@ -445,7 +445,7 @@ class EventLogVerifierTest {
 
         when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok());
         when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok());
-        when(keyServiceMock.getSubmissionServiceSignatureKey(any())).thenReturn(rsaKey);
+        when(keyServiceMock.getSubmissionServicePublicKey(any())).thenReturn(rsaKey);
 
         // When
         final List<ValidationResult> validationResults = underTest.validateEventLogs(ctx, List.of(signedJWT));
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/events/SecurityEventTokenServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/events/SecurityEventTokenServiceTest.java
index 2500d1e32979e5bfd1c52bbad446e22ce29d3805..a790df4634bbef654f6e6d7eefda5a7661df9da7 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/events/SecurityEventTokenServiceTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/events/SecurityEventTokenServiceTest.java
@@ -15,6 +15,7 @@ import dev.fitko.fitconnect.api.domain.model.event.EventPayload;
 import dev.fitko.fitconnect.api.domain.model.event.Event;
 import dev.fitko.fitconnect.api.domain.model.event.problems.submission.AttachmentsMismatch;
 import dev.fitko.fitconnect.api.domain.model.submission.Submission;
+import dev.fitko.fitconnect.api.domain.schema.SchemaResources;
 import dev.fitko.fitconnect.api.exceptions.EventCreationException;
 import dev.fitko.fitconnect.api.services.crypto.CryptoService;
 import dev.fitko.fitconnect.api.services.events.SecurityEventService;
@@ -53,26 +54,30 @@ class SecurityEventTokenServiceTest {
     @BeforeEach
     void setUp() throws IOException, ParseException {
         final ApplicationConfig config = getApplicationConfig();
-        this.cryptoService = new JWECryptoService(new HashService());
-        this.signingKey = JWK.parse(getResourceAsString("private_test_signing_key.json")).toRSAKey();
-        this.encryptionKey = JWK.parse(getResourceAsString("public_encryption_test_key.json")).toRSAKey();
+        cryptoService = new JWECryptoService(new HashService());
+        signingKey = JWK.parse(getResourceAsString("private_test_signing_key.json")).toRSAKey();
+        encryptionKey = JWK.parse(getResourceAsString("public_encryption_test_key.json")).toRSAKey();
+
         final List<String> setSchemas = SchemaConfig.getSetSchemaFilePaths("/set-schema");
         final List<String> metadataSchemas = SchemaConfig.getMetadataSchemaFileNames("/metadata-schema");
-        final SchemaProvider schemaProvider = new SchemaResourceProvider(setSchemas, metadataSchemas);
-        this.validationService = new DefaultValidationService(config, new HashService(), schemaProvider);
-        this.underTest = new SecurityEventTokenService(config, this.validationService, this.signingKey);
+        final List<String> destinationSchemas = SchemaConfig.getDestinationSchemaPaths("/destination-schema");
+        final SchemaResources schemaResources = new SchemaResources(setSchemas, metadataSchemas, destinationSchemas);
+        final SchemaProvider schemaProvider = new SchemaResourceProvider(schemaResources);
+
+        validationService = new DefaultValidationService(config, new HashService(), schemaProvider);
+        underTest = new SecurityEventTokenService(config, validationService, signingKey);
     }
 
     @Test
     void testAcceptSubmissionEventWithoutProblems() throws JOSEException {
 
         // Given
-        final var encryptedData = this.cryptoService.encryptString(this.encryptionKey, "test data");
-        final var encryptedMetadata = this.cryptoService.encryptString(this.encryptionKey, "test metadata");
+        final var encryptedData = cryptoService.encryptString(encryptionKey, "test data");
+        final var encryptedMetadata = cryptoService.encryptString(encryptionKey, "test metadata");
 
         final var encryptedAttachments = new HashMap<UUID, String>();
-        encryptedAttachments.put(UUID.randomUUID(), this.cryptoService.encryptString(this.encryptionKey, "test attachment 1"));
-        encryptedAttachments.put(UUID.randomUUID(), this.cryptoService.encryptString(this.encryptionKey, "test attachment 2"));
+        encryptedAttachments.put(UUID.randomUUID(), cryptoService.encryptString(encryptionKey, "test attachment 1"));
+        encryptedAttachments.put(UUID.randomUUID(), cryptoService.encryptString(encryptionKey, "test attachment 2"));
 
         final var submission = new Submission();
         submission.setSubmissionId(UUID.randomUUID());
@@ -84,15 +89,15 @@ class SecurityEventTokenServiceTest {
         final EventPayload eventPayload = new EventPayload(submission, encryptedAttachments);
 
         // When
-        final SignedJWT signedJWT = this.underTest.createAcceptSubmissionEvent(eventPayload);
+        final SignedJWT signedJWT = underTest.createAcceptSubmissionEvent(eventPayload);
 
         // Then
         final Map<String, Object> payload = signedJWT.getPayload().toJSONObject();
         final var eventsClaim = (Map) payload.get("events");
         final var eventObject = (Map) eventsClaim.get(Event.ACCEPT.getSchemaUri());
-        final JWSVerifier jwsVerifier = new RSASSAVerifier(this.signingKey);
+        final JWSVerifier jwsVerifier = new RSASSAVerifier(signingKey);
 
-        assertEquals(JWSAlgorithm.PS512, this.signingKey.getAlgorithm());
+        assertEquals(JWSAlgorithm.PS512, signingKey.getAlgorithm());
         assertTrue(signedJWT.verify(jwsVerifier));
         assertThat(payload.get("sub"), is("submission:" + eventPayload.getSubmissionId()));
         assertThat(payload.get("txn"), is("case:" + eventPayload.getCaseId()));
@@ -104,11 +109,11 @@ class SecurityEventTokenServiceTest {
     void testAcceptSubmissionEventWithProblem() throws JOSEException {
 
         // Given
-        final var encryptedData = this.cryptoService.encryptString(this.encryptionKey, "test data");
-        final var encryptedMetadata = this.cryptoService.encryptString(this.encryptionKey, "test metadata");
+        final var encryptedData = cryptoService.encryptString(encryptionKey, "test data");
+        final var encryptedMetadata = cryptoService.encryptString(encryptionKey, "test metadata");
 
         final var encryptedAttachments = new HashMap<UUID, String>();
-        encryptedAttachments.put(UUID.randomUUID(), this.cryptoService.encryptString(this.encryptionKey, "test attachment"));
+        encryptedAttachments.put(UUID.randomUUID(), cryptoService.encryptString(encryptionKey, "test attachment"));
 
         final var submission = new Submission();
         submission.setSubmissionId(UUID.randomUUID());
@@ -122,15 +127,15 @@ class SecurityEventTokenServiceTest {
         final EventPayload eventPayload = new EventPayload(submission, encryptedAttachments, List.of(problem));
 
         // When
-        final SignedJWT signedJWT = this.underTest.createAcceptSubmissionEvent(eventPayload);
+        final SignedJWT signedJWT = underTest.createAcceptSubmissionEvent(eventPayload);
 
         // Then
         final Map<String, Object> payload = signedJWT.getPayload().toJSONObject();
         final var eventsClaim = (Map) payload.get("events");
         final var eventObject = (Map) eventsClaim.get(Event.ACCEPT.getSchemaUri());
-        final JWSVerifier jwsVerifier = new RSASSAVerifier(this.signingKey);
+        final JWSVerifier jwsVerifier = new RSASSAVerifier(signingKey);
 
-        assertEquals(JWSAlgorithm.PS512, this.signingKey.getAlgorithm());
+        assertEquals(JWSAlgorithm.PS512, signingKey.getAlgorithm());
         assertTrue(signedJWT.verify(jwsVerifier));
         assertThat(payload.get("sub"), is("submission:" + eventPayload.getSubmissionId()));
         assertThat(payload.get("txn"), is("case:" + eventPayload.getCaseId()));
@@ -153,15 +158,15 @@ class SecurityEventTokenServiceTest {
         final EventPayload eventPayload = new EventPayload(submission, List.of(problem));
 
         // When
-        final SignedJWT signedJWT = this.underTest.createRejectSubmissionEvent(eventPayload);
+        final SignedJWT signedJWT = underTest.createRejectSubmissionEvent(eventPayload);
 
         // Then
         final Map<String, Object> payload = signedJWT.getPayload().toJSONObject();
         final var eventsClaim = (Map) payload.get("events");
         final var eventObject = (Map) eventsClaim.get(Event.REJECT.getSchemaUri());
-        final JWSVerifier jwsVerifier = new RSASSAVerifier(this.signingKey);
+        final JWSVerifier jwsVerifier = new RSASSAVerifier(signingKey);
 
-        assertEquals(JWSAlgorithm.PS512, this.signingKey.getAlgorithm());
+        assertEquals(JWSAlgorithm.PS512, signingKey.getAlgorithm());
         assertTrue(signedJWT.verify(jwsVerifier));
         assertThat(payload.get("sub"), is("submission:" + eventPayload.getSubmissionId()));
         assertThat(payload.get("txn"), is("case:" + eventPayload.getCaseId()));
@@ -178,12 +183,12 @@ class SecurityEventTokenServiceTest {
         submission.setSubmissionId(UUID.randomUUID());
         submission.setDestinationId(UUID.randomUUID());
         submission.setCaseId(UUID.randomUUID());
-        submission.setEncryptedData(this.cryptoService.encryptString(this.encryptionKey, "test data"));
+        submission.setEncryptedData(cryptoService.encryptString(encryptionKey, "test data"));
 
         final EventPayload eventPayload = new EventPayload(submission, Map.of());
 
         // When
-        final EventCreationException exception = assertThrows(EventCreationException.class, () -> this.underTest.createAcceptSubmissionEvent(eventPayload));
+        final EventCreationException exception = assertThrows(EventCreationException.class, () -> underTest.createAcceptSubmissionEvent(eventPayload));
 
         // Then
         assertThat(exception.getCause().getMessage(), containsString("$.events.https://schema.fitko.de/fit-connect/events/accept-submission.authenticationTags.metadata: null found"));
@@ -197,12 +202,12 @@ class SecurityEventTokenServiceTest {
         submission.setSubmissionId(UUID.randomUUID());
         submission.setDestinationId(UUID.randomUUID());
         submission.setCaseId(UUID.randomUUID());
-        submission.setEncryptedMetadata(this.cryptoService.encryptString(this.encryptionKey, "test metadata"));
+        submission.setEncryptedMetadata(cryptoService.encryptString(encryptionKey, "test metadata"));
 
         final EventPayload eventPayload = new EventPayload(submission, Map.of());
 
         // When
-        final EventCreationException exception = assertThrows(EventCreationException.class, () -> this.underTest.createAcceptSubmissionEvent(eventPayload));
+        final EventCreationException exception = assertThrows(EventCreationException.class, () -> underTest.createAcceptSubmissionEvent(eventPayload));
 
         // Then
         assertThat(exception.getCause().getMessage(), containsString("$.events.https://schema.fitko.de/fit-connect/events/accept-submission.authenticationTags.data: null found"));
@@ -220,7 +225,7 @@ class SecurityEventTokenServiceTest {
         final EventPayload eventPayload = new EventPayload(submission);
 
         // When
-        final EventCreationException exception = assertThrows(EventCreationException.class, () -> this.underTest.createAcceptSubmissionEvent(eventPayload));
+        final EventCreationException exception = assertThrows(EventCreationException.class, () -> underTest.createAcceptSubmissionEvent(eventPayload));
 
         // Then
         assertThat(exception.getCause().getMessage(), containsString("$.events.https://schema.fitko.de/fit-connect/events/accept-submission.authenticationTags.metadata: null found"));
@@ -239,7 +244,7 @@ class SecurityEventTokenServiceTest {
         final EventPayload eventPayload = new EventPayload(submission);
 
         // When
-        final EventCreationException exception = assertThrows(EventCreationException.class, () -> this.underTest.createRejectSubmissionEvent(eventPayload));
+        final EventCreationException exception = assertThrows(EventCreationException.class, () -> underTest.createRejectSubmissionEvent(eventPayload));
 
         // Then
         assertThat(exception.getCause().getMessage(), containsString("$.events.https://schema.fitko.de/fit-connect/events/reject-submission.problems: is missing but it is required"));
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyRestTemplateTest.java b/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyRestTemplateTest.java
index 262d9a2146291ec789a2aa7f488dcc67e8f74d41..5dd93e1bdaacedef13ac6076321832eb07fe3240 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyRestTemplateTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyRestTemplateTest.java
@@ -17,28 +17,28 @@ public class ProxyRestTemplateTest {
 
     @BeforeEach
     public void startWireMock() {
-        this.wireMockServer.start();
+        wireMockServer.start();
     }
 
     @AfterEach
     public void shutDownWireMock() {
-        this.wireMockServer.stop();
+        wireMockServer.stop();
     }
 
     @Test
     void userAgentIsCorrect() {
 
         WireMock.configureFor("localhost", 8080);
-        this.wireMockServer.stubFor(get(urlEqualTo("/test"))
+        wireMockServer.stubFor(get(urlEqualTo("/test"))
                 .withHeader("User-Agent", equalTo("productName/productVersion (commit:commit;os:" + System.getProperty("os.name") + ")"))
                 .willReturn(ok().withBody(""))
         );
 
-        BuildInfo buildInfo = new BuildInfo();
+        final BuildInfo buildInfo = new BuildInfo();
         buildInfo.setProductName("productName");
         buildInfo.setProductVersion("productVersion");
         buildInfo.setCommit("commit");
-        RestTemplate restTemplate = new ProxyConfig(null, 0, buildInfo).proxyRestTemplate();
+        final RestTemplate restTemplate = new RestService(null, 0, buildInfo).getRestTemplate();
 
         restTemplate.exchange("http://localhost:8080/test", HttpMethod.GET, null, String.class);
 
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyConfigTest.java b/core/src/test/java/dev/fitko/fitconnect/core/http/RestServiceTest.java
similarity index 77%
rename from core/src/test/java/dev/fitko/fitconnect/core/http/ProxyConfigTest.java
rename to core/src/test/java/dev/fitko/fitconnect/core/http/RestServiceTest.java
index cc0d2feec72f2d0cf1210c894983749036518fab..1c88e3ffc82bb29104e427f94cf454f74ace4320 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyConfigTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/http/RestServiceTest.java
@@ -9,18 +9,18 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
 import static org.junit.jupiter.api.Assertions.*;
 
-class ProxyConfigTest {
+class RestServiceTest {
 
-    ProxyConfig underTest;
+    RestService underTest;
 
     @Test
     void proxyRestTemplate() {
 
         // Given
-        underTest = new ProxyConfig("http://testhost.de", 8080, new BuildInfo());
+        underTest = new RestService("http://testhost.de", 8080, new BuildInfo());
 
         // When
-        final RestTemplate template = underTest.proxyRestTemplate();
+        final RestTemplate template = underTest.getRestTemplate();
 
         // Then
         assertNotNull(template);
@@ -32,7 +32,7 @@ class ProxyConfigTest {
     void hasProxySet() {
 
         // Given
-        underTest = new ProxyConfig("http://testhost.de", 8080, new BuildInfo());
+        underTest = new RestService("http://testhost.de", 8080, new BuildInfo());
 
         // When
         final boolean proxySet = underTest.hasProxySet();
@@ -45,7 +45,7 @@ class ProxyConfigTest {
     void hasProxyNotSet() {
 
         // Given
-        underTest = new ProxyConfig(null, 0, new BuildInfo());
+        underTest = new RestService(null, 0, new BuildInfo());
 
         // When
         final boolean proxySet = underTest.hasProxySet();
@@ -58,7 +58,7 @@ class ProxyConfigTest {
     void testToString() {
 
         // Given
-        underTest = new ProxyConfig("http://testhost.de", 8080, new BuildInfo());
+        underTest = new RestService("http://testhost.de", 8080, new BuildInfo());
 
         // When
         final String configAsString = underTest.toString();
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/http/RestTemplateTest.java b/core/src/test/java/dev/fitko/fitconnect/core/http/RestTemplateTest.java
index c80fc734980c2ce426bc0dbe158565f5d5daaed6..7a9c0bc41a58172104bad780906bb3c17a503acd 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/http/RestTemplateTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/http/RestTemplateTest.java
@@ -34,7 +34,7 @@ public class RestTemplateTest extends RestEndpointBase {
 
     @BeforeEach
     void setup() {
-        underTest = new ProxyConfig(null, 0, new BuildInfo()).proxyRestTemplate();
+        underTest = new RestService(null, 0, new BuildInfo()).getRestTemplate();
     }
 
     @Test
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/keys/PublicKeyServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/keys/PublicKeyServiceTest.java
index 49adb82eb0d723ddfdcc6d008eb9132d31506bcf..54b90334da0c9a89fc1ae3323418afa358daa032 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/keys/PublicKeyServiceTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/keys/PublicKeyServiceTest.java
@@ -198,7 +198,7 @@ class PublicKeyServiceTest extends RestEndpointBase {
         when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok());
 
         // When
-        final RSAKey retrievedSignatureKey = underTest.getSubmissionServiceSignatureKey("123");
+        final RSAKey retrievedSignatureKey = underTest.getSubmissionServicePublicKey("123");
 
         // Then
         assertThat(retrievedSignatureKey, is(RSAKey.parse(jwkSet.getKeyByKeyId("123").toJSONObject())));
@@ -228,7 +228,37 @@ class PublicKeyServiceTest extends RestEndpointBase {
         when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok());
 
         // When
-        final RSAKey retrievedSignatureKey = underTest.getPortalSignatureKey("123");
+        final RSAKey retrievedSignatureKey = underTest.getPortalPublicKey("123");
+
+        // Then
+        assertThat(retrievedSignatureKey, is(RSAKey.parse(jwkSet.getKeyByKeyId("123").toJSONObject())));
+    }
+
+    @Test
+    void getSignatureKeyFromCustomUrlTest() throws IOException, ParseException {
+
+        // Given
+        final Destination destination = new Destination();
+        destination.setDestinationId(UUID.randomUUID());
+        destination.setEncryptionKid("123");
+
+        final OAuthToken authToken = new OAuthToken();
+        authToken.setAccessToken("abc123");
+
+        final JWKSet jwkSet = new JWKSet(JWK.parse(getResourceAsString("/public_signature_test_key.json")));
+
+        wireMockServer.stubFor(
+                get(urlEqualTo("/custom/path/.well-known/jwks.json"))
+                        .willReturn(ok()
+                                .withBody(new ObjectMapper().writeValueAsString(jwkSet.toJSONObject()))
+                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withStatus(200)));
+
+        when(authServiceMock.getCurrentToken()).thenReturn(authToken);
+        when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok());
+
+        // When
+        final RSAKey retrievedSignatureKey = underTest.getWellKnownKeysForSubmissionUrl("http://localhost:" + wireMockServer.port() + "/custom/path", "123");
 
         // Then
         assertThat(retrievedSignatureKey, is(RSAKey.parse(jwkSet.getKeyByKeyId("123").toJSONObject())));
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/routing/RouteVerifierTest.java b/core/src/test/java/dev/fitko/fitconnect/core/routing/RouteVerifierTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4b441543955a154e657bbe5b228cfb2d64d78c3e
--- /dev/null
+++ b/core/src/test/java/dev/fitko/fitconnect/core/routing/RouteVerifierTest.java
@@ -0,0 +1,258 @@
+package dev.fitko.fitconnect.core.routing;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.util.Base64URL;
+import com.nimbusds.jwt.SignedJWT;
+import dev.fitko.fitconnect.api.domain.model.route.Route;
+import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
+import dev.fitko.fitconnect.api.exceptions.ValidationException;
+import dev.fitko.fitconnect.api.services.keys.KeyService;
+import dev.fitko.fitconnect.api.services.validation.ValidationService;
+import dev.fitko.fitconnect.core.SubmissionSenderTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class RouteVerifierTest {
+
+    private RouteVerifier underTest;
+    private KeyService keyServiceMock;
+    private ValidationService validatorMock;
+    private RSAKey portalSignatureKey;
+    private RSAKey submissionServiceSignatureKey;
+
+    @BeforeEach
+    void setup() throws IOException, ParseException {
+        keyServiceMock = mock(KeyService.class);
+        validatorMock = mock(ValidationService.class);
+        underTest = new RouteVerifier(keyServiceMock, validatorMock);
+        portalSignatureKey = RSAKey.parse(getResourceAsString("/portal_well_known_signature_key.json"));
+        submissionServiceSignatureKey = RSAKey.parse(getResourceAsString("/submission_service_well_known_signature_key.json"));
+    }
+
+    @Test
+    void validateRouteDestinationTest() throws IOException {
+
+        // Given
+        final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class);
+
+        when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey);
+        when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey);
+        when(validatorMock.validateDestinationSchema(anyMap())).thenReturn(ValidationResult.ok());
+
+
+        // When
+        final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014");
+
+        // Then
+        assertTrue(validationResult.isValid());
+    }
+
+    @Test
+    void hostUrlDoesNotMatchTest() throws IOException {
+
+        // Given
+        final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class);
+        route.getDestinationParameters().setSubmissionUrl("https://dev.null.net");
+
+        when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey);
+        when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey);
+        when(validatorMock.validateDestinationSchema(anyMap())).thenReturn(ValidationResult.ok());
+
+        // When
+        final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014");
+
+        // Then
+        assertTrue(validationResult.hasError());
+        assertThat(validationResult.getError().getMessage(), containsString("Submission host does not match destinationParameters submission url submission-api-testing.fit-connect.fitko.dev"));
+    }
+
+    @Test
+    void requiredAlgorithmDoesNotMatchTest() throws IOException, ParseException {
+
+        // Given
+        final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class);
+        final SignedJWT signature = SignedJWT.parse(route.getDestinationSignature());
+
+        final Base64URL headerWithWrongAlgorithm = new JWSHeader(JWSAlgorithm.ES512).toBase64URL();
+        final SignedJWT signatureWithWrongAlg = new SignedJWT(headerWithWrongAlgorithm, signature.getParsedParts()[1], signature.getParsedParts()[2]);
+        route.setDestinationSignature(signatureWithWrongAlg.serialize());
+
+        when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey);
+        when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey);
+
+        // When
+        final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014");
+
+        // Then
+        assertTrue(validationResult.hasError());
+        assertThat(validationResult.getError().getMessage(), containsString("Algorithm in signature header is not PS512"));
+    }
+
+    @Test
+    void requiredClaimsNotPresentTest() throws IOException, ParseException {
+
+        // Given
+        final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class);
+        final SignedJWT signature = SignedJWT.parse(route.getDestinationSignature());
+
+        final Payload payloadWithMissingClaims = new Payload(Map.of("submissionHost", "submission-api-testing.fit-connect.fitko.dev"));
+        final SignedJWT signatureWithMissingClaims = new SignedJWT(signature.getHeader().toBase64URL(), payloadWithMissingClaims.toBase64URL(), signature.getParsedParts()[2]);
+
+        route.setDestinationSignature(signatureWithMissingClaims.serialize());
+
+        when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey);
+        when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey);
+        when(validatorMock.validateDestinationSchema(anyMap())).thenReturn(ValidationResult.error(new ValidationException("Payload missing required claims")));
+
+
+        // When
+        final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014");
+
+        // Then
+        assertTrue(validationResult.hasError());
+        assertThat(validationResult.getError().getMessage(), containsString("Payload missing required claims"));
+    }
+
+    @Test
+    void requestedRegionNotSupportedByDestinationTest() throws IOException, ParseException {
+
+        // Given
+        final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class);
+        final SignedJWT signature = SignedJWT.parse(route.getDestinationSignature());
+
+        final List<Map<String, List<String>>> serviceClaim = List.of(
+                Map.of(
+                        "gebietIDs", List.of("1234567"),
+                        "leistungIDs", List.of("urn:de:fim:leika:leistung:99123456760610")
+                )
+        );
+
+        final Map<String, Object> claims = Map.of(
+                "submissionHost", "submission-api-testing.fit-connect.fitko.dev",
+                "iss", "fit-connect",
+                "iat", new Date().getTime(),
+                "jti", UUID.randomUUID().toString(),
+                "destinationId", UUID.randomUUID().toString(),
+                "services", serviceClaim);
+
+        final Payload payloadWithServicesNotMatching = new Payload(claims);
+        final SignedJWT signatureWithServices = new SignedJWT(signature.getHeader().toBase64URL(), payloadWithServicesNotMatching.toBase64URL(), signature.getParsedParts()[2]);
+
+        route.setDestinationSignature(signatureWithServices.serialize());
+
+        when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey);
+        when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey);
+        when(validatorMock.validateDestinationSchema(anyMap())).thenReturn(ValidationResult.ok());
+
+
+        // When
+        final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014");
+
+        // Then
+        assertTrue(validationResult.hasError());
+        assertThat(validationResult.getError().getMessage(), containsString("Requested region '064350014014' is not supported by the destinations services"));
+    }
+
+    @Test
+    void requestedServiceIdentifierNotSupportedByDestinationTest() throws IOException, ParseException {
+
+        // Given
+        final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class);
+        final SignedJWT signature = SignedJWT.parse(route.getDestinationSignature());
+
+        final List<Map<String, List<String>>> serviceClaim = List.of(
+                Map.of(
+                        "gebietIDs", List.of("064350014014"),
+                        "leistungIDs", List.of("urn:de:fim:leika:leistung:1111111111111")
+                )
+        );
+
+        final Map<String, Object> claims = Map.of(
+                "submissionHost", "submission-api-testing.fit-connect.fitko.dev",
+                "iss", "fit-connect",
+                "iat", new Date().getTime(),
+                "jti", UUID.randomUUID().toString(),
+                "destinationId", UUID.randomUUID().toString(),
+                "services", serviceClaim);
+
+        final Payload payloadWithServicesNotMatching = new Payload(claims);
+        final SignedJWT signatureWithServices = new SignedJWT(signature.getHeader().toBase64URL(), payloadWithServicesNotMatching.toBase64URL(), signature.getParsedParts()[2]);
+
+        route.setDestinationSignature(signatureWithServices.serialize());
+
+        when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey);
+        when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey);
+        when(validatorMock.validateDestinationSchema(anyMap())).thenReturn(ValidationResult.ok());
+
+        // When
+        final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014");
+
+        // Then
+        assertTrue(validationResult.hasError());
+        assertThat(validationResult.getError().getMessage(), containsString("Requested service identifier 'urn:de:fim:leika:leistung:99123456760610' is not supported by the destinations services"));
+    }
+
+    @Test
+    void signatureVerificationFailedTest() throws IOException {
+
+        // Given
+        final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class);
+        route.getDestinationParameters().setEncryptionKid("Wrong_Key_Id_That_Fails_The_Public_Key_Retrieval");
+
+        when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey);
+        when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey);
+        when(validatorMock.validateDestinationSchema(anyMap())).thenReturn(ValidationResult.ok());
+
+
+        // When
+        final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014");
+
+        // Then
+        assertTrue(validationResult.hasError());
+        assertThat(validationResult.getError().getMessage(), containsString("Invalid destination parameter signature for route d40e7b13-da98-4b09-9e16-bbd61ca81510"));
+    }
+
+    @Test
+    void combiningDetachedSignatureWithPayloadFailedTest() throws IOException {
+
+        // Given
+        final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class);
+
+        // replace detached signature payload part with wrong data  (..)
+       route.setDestinationParametersSignature(route.getDestinationParametersSignature().replace("..", ""));
+
+        when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey);
+        when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey);
+        when(validatorMock.validateDestinationSchema(anyMap())).thenReturn(ValidationResult.ok());
+
+
+        // When
+        final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014");
+
+        assertTrue(validationResult.hasError());
+        assertThat(validationResult.getError().getMessage(), containsString("Invalid serialized unsecured/JWS/JWE object: Missing part delimiters"));
+    }
+
+    private String getResourceAsString(final String filename) throws IOException {
+        return new String(SubmissionSenderTest.class.getResourceAsStream(filename).readAllBytes());
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/routing/RoutingApiServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/routing/RoutingApiServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..486a1150b398200e7b3c97319795b39c29b39630
--- /dev/null
+++ b/core/src/test/java/dev/fitko/fitconnect/core/routing/RoutingApiServiceTest.java
@@ -0,0 +1,123 @@
+package dev.fitko.fitconnect.core.routing;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.fitko.fitconnect.api.config.ApplicationConfig;
+import dev.fitko.fitconnect.api.config.Environment;
+import dev.fitko.fitconnect.api.config.EnvironmentName;
+import dev.fitko.fitconnect.api.domain.model.route.Area;
+import dev.fitko.fitconnect.api.domain.model.route.AreaResult;
+import dev.fitko.fitconnect.api.domain.model.route.Route;
+import dev.fitko.fitconnect.api.domain.model.route.RouteResult;
+import dev.fitko.fitconnect.api.exceptions.RestApiException;
+import dev.fitko.fitconnect.api.services.routing.RoutingService;
+import dev.fitko.fitconnect.core.RestEndpointBase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.MediaType;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class RoutingApiServiceTest extends RestEndpointBase {
+
+    private RoutingService underTest;
+
+    @BeforeEach
+    void setup() {
+
+        final var fakeBaseUrl = "http://localhost:" + wireMockServer.port();
+
+        final EnvironmentName envName = new EnvironmentName("TESTING");
+        final Environment environment = new Environment();
+        environment.setRoutingBaseUrl(fakeBaseUrl);
+
+        final ApplicationConfig config = new ApplicationConfig();
+        config.setEnvironments(Map.of(envName, environment));
+        config.setActiveEnvironment(envName);
+
+        underTest = new RoutingApiService(config, new RestTemplate());
+    }
+
+    @Test
+    void testFindAreas() throws JsonProcessingException {
+
+        // Given
+        final Area area = new Area();
+        area.setName("Berlin");
+
+        final AreaResult expectedResult = new AreaResult();
+        expectedResult.setCount(1);
+        expectedResult.setOffset(0);
+        expectedResult.setTotalCount(1);
+        expectedResult.setAreas(List.of(area));
+
+        wireMockServer.stubFor(
+                get(urlEqualTo("/v1/areas?areaSearchexpression=Berlin&offset=0&limit=10"))
+                        .willReturn(ok()
+                                .withBody(new ObjectMapper().writeValueAsString(expectedResult))
+                                .withHeader("Content-Type", "application/problem+json")
+                                .withStatus(200)));
+
+        // When
+        final AreaResult areaResult = underTest.getAreas(List.of("Berlin"), 0, 10);
+
+
+        // Then
+        assertNotNull(areaResult);
+        assertFalse(areaResult.getAreas().isEmpty());
+        assertThat(areaResult.getAreas().get(0), is(area));
+    }
+
+    @Test
+    void testFindRoutes() throws JsonProcessingException {
+
+        // Given
+        final Route route = new Route();
+        route.setDestinationId(UUID.randomUUID());
+        route.setDestinationName("test destination");
+
+        final RouteResult expectedResult = new RouteResult();
+        expectedResult.setCount(1);
+        expectedResult.setOffset(0);
+        expectedResult.setTotalCount(1);
+        expectedResult.setRoutes(List.of(route));
+
+        wireMockServer.stubFor(
+                get(urlEqualTo("/v1/routes?leikaKey=99123456760610&ars=064350014014&offset=0&limit=10"))
+                        .willReturn(ok()
+                                .withBody(new ObjectMapper().writeValueAsString(expectedResult))
+                                .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+                                .withStatus(200)));
+
+        // When
+        final RouteResult routeResult = underTest.getRoutes("99123456760610", "064350014014", null, null, 0, 10);
+
+        // Then
+        assertNotNull(routeResult);
+        assertFalse(routeResult.getRoutes().isEmpty());
+        assertThat(routeResult.getRoutes().get(0), is(route));
+    }
+
+    @Test
+    void testFindRoutesWithNoSearchCriterion() {
+
+        // Given
+        final var leikaKey = "99123456760610";
+
+        // When
+        final RestApiException exception = assertThrows(RestApiException.class, () -> underTest.getRoutes(leikaKey, null, null, null, 0, 10));
+
+        // Then
+        assertThat(exception.getMessage(), containsString(" one search criterion out of ags, ars or areaId must be set"));
+
+    }
+}
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/schema/SchemaResourceProviderTest.java b/core/src/test/java/dev/fitko/fitconnect/core/schema/SchemaResourceProviderTest.java
index f4d1fae5007141e16dad971c4fc6350e94b4abdb..0b1899e945d8a6eda17770d21ff9a9ba381ae8e5 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/schema/SchemaResourceProviderTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/schema/SchemaResourceProviderTest.java
@@ -3,6 +3,7 @@ package dev.fitko.fitconnect.core.schema;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import dev.fitko.fitconnect.api.config.SchemaConfig;
+import dev.fitko.fitconnect.api.domain.schema.SchemaResources;
 import dev.fitko.fitconnect.api.exceptions.SchemaNotFoundException;
 import dev.fitko.fitconnect.api.services.schema.SchemaProvider;
 import org.junit.jupiter.api.BeforeEach;
@@ -23,29 +24,31 @@ class SchemaResourceProviderTest {
     void setup() {
         final List<String> setSchemas = SchemaConfig.getSetSchemaFilePaths("/set-schema");
         final List<String> metadataSchemas = SchemaConfig.getMetadataSchemaFileNames("/metadata-schema");
-        this.underTest = new SchemaResourceProvider(setSchemas, metadataSchemas);
+        final List<String> destinationSchemas = SchemaConfig.getDestinationSchemaPaths("/destination-schema");
+        final SchemaResources schemaResources = new SchemaResources(setSchemas, metadataSchemas, destinationSchemas);
+        underTest = new SchemaResourceProvider(schemaResources);
     }
 
     @Test
     void isAllowedSetSchema() {
-        assertTrue(this.underTest.isAllowedSetSchema(SchemaConfig.SET_V_1_0_1.getSchemaUri()));
-        assertTrue(this.underTest.isAllowedSetSchema(SchemaConfig.SET_V_1_0_0.getSchemaUri()));
-        assertTrue(this.underTest.isAllowedSetSchema(URI.create("https://schema.fitko.de/fit-connect/set-payload/1.2.3/set-payload.schema.json")));
+        assertTrue(underTest.isAllowedSetSchema(SchemaConfig.SET_V_1_0_1.getSchemaUri()));
+        assertTrue(underTest.isAllowedSetSchema(SchemaConfig.SET_V_1_0_0.getSchemaUri()));
+        assertTrue(underTest.isAllowedSetSchema(URI.create("https://schema.fitko.de/fit-connect/set-payload/1.2.3/set-payload.schema.json")));
     }
 
     @Test
     void isNoAllowedSetSchema() {
-        assertFalse(this.underTest.isAllowedSetSchema(URI.create("https://schema.fitko.de/fit-connect/set-payload/2.2.3/set-payload.schema.json")));
+        assertFalse(underTest.isAllowedSetSchema(URI.create("https://schema.fitko.de/fit-connect/set-payload/2.2.3/set-payload.schema.json")));
     }
 
     @Test
     void isAllowedMetadataSchema() {
-        assertTrue(this.underTest.isAllowedMetadataSchema(SchemaConfig.METADATA_V_1_0_0.getSchemaUri()));
+        assertTrue(underTest.isAllowedMetadataSchema(SchemaConfig.METADATA_V_1_0_0.getSchemaUri()));
     }
 
     @Test
     void isNoAllowedMetadataSchema() {
-        assertFalse(this.underTest.isAllowedSetSchema(URI.create("https://schema.fitko.de/fit-connect/metadata/9.9.9/metadata.schema.json")));
+        assertFalse(underTest.isAllowedSetSchema(URI.create("https://schema.fitko.de/fit-connect/metadata/9.9.9/metadata.schema.json")));
     }
 
     @Test
@@ -54,7 +57,7 @@ class SchemaResourceProviderTest {
         final URI schemaUri = SchemaConfig.SET_V_1_0_1.getSchemaUri();
 
         // When
-        final String setSchema = this.underTest.loadLatestSetSchema();
+        final String setSchema = underTest.loadLatestSetSchema();
 
         //Then
         assertThat(new ObjectMapper().readTree(setSchema).get("$id").asText(), equalTo(schemaUri.toString()));
@@ -66,7 +69,19 @@ class SchemaResourceProviderTest {
         final URI schemaUri = SchemaConfig.METADATA_V_1_0_0.getSchemaUri();
 
         // When
-        final String metadataSchema = this.underTest.loadMetadataSchema(schemaUri);
+        final String metadataSchema = underTest.loadMetadataSchema(schemaUri);
+
+        //Then
+        assertThat(new ObjectMapper().readTree(metadataSchema).get("$id").asText(), equalTo(schemaUri.toString()));
+    }
+
+    @Test
+    void loadDestinationSchema() throws JsonProcessingException {
+        // Given
+        final URI schemaUri = SchemaConfig.XZUFI_DESTINATION_SCHEMA.getSchemaUri();
+
+        // When
+        final String metadataSchema = underTest.loadDestinationSchema(schemaUri);
 
         //Then
         assertThat(new ObjectMapper().readTree(metadataSchema).get("$id").asText(), equalTo(schemaUri.toString()));
@@ -75,6 +90,6 @@ class SchemaResourceProviderTest {
     @Test
     void loadMetadataSchemaThatDoesNotExist() {
         final URI schemaUri = URI.create("https://schema.fitko.de/fit-connect/metadata/9.9.9/metadata.schema.json");
-        assertThrows(SchemaNotFoundException.class, () -> this.underTest.loadMetadataSchema(schemaUri));
+        assertThrows(SchemaNotFoundException.class, () -> underTest.loadMetadataSchema(schemaUri));
     }
 }
\ No newline at end of file
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/submission/SubmissionApiServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/submission/SubmissionApiServiceTest.java
index 16aecff189934eda4632c84d4dff2b244ba50b11..ba35c8acd3eb1d12eccb251955f9cf781263488c 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/submission/SubmissionApiServiceTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/submission/SubmissionApiServiceTest.java
@@ -274,8 +274,10 @@ class SubmissionApiServiceTest extends RestEndpointBase {
                                 .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
                                 .withStatus(200))).getResponse();
 
+        // When
         final Submission submission = underTest.getSubmission(submissionId);
 
+        // Then
         assertNotNull(submission);
         assertThat(submission.getSubmissionId(), is(expectedSubmission.getSubmissionId()));
     }
diff --git a/core/src/test/java/dev/fitko/fitconnect/core/validation/DefaultValidationServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/validation/DefaultValidationServiceTest.java
index 7de74f80fb56dd95de3235964a58785bc47c0e9f..ba7f6ec3e1f553294408cf421a9218f2ad9f2462 100644
--- a/core/src/test/java/dev/fitko/fitconnect/core/validation/DefaultValidationServiceTest.java
+++ b/core/src/test/java/dev/fitko/fitconnect/core/validation/DefaultValidationServiceTest.java
@@ -19,6 +19,7 @@ import dev.fitko.fitconnect.api.domain.model.metadata.SignatureType;
 import dev.fitko.fitconnect.api.domain.model.metadata.data.Data;
 import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
 import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema;
+import dev.fitko.fitconnect.api.domain.schema.SchemaResources;
 import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
 import dev.fitko.fitconnect.api.exceptions.ValidationException;
 import dev.fitko.fitconnect.api.services.crypto.MessageDigestService;
@@ -57,7 +58,9 @@ class DefaultValidationServiceTest {
         hashService = new HashService();
         final List<String> setSchemas = SchemaConfig.getSetSchemaFilePaths("/set-schema");
         final List<String> metadataSchemas = SchemaConfig.getMetadataSchemaFileNames("/metadata-schema");
-        schemaProvider = new SchemaResourceProvider(setSchemas, metadataSchemas);
+        final List<String> destinationSchemas = SchemaConfig.getDestinationSchemaPaths("/destination-schema");
+        final SchemaResources schemaResources = new SchemaResources(setSchemas, metadataSchemas, destinationSchemas);
+        schemaProvider = new SchemaResourceProvider(schemaResources);
         underTest = new DefaultValidationService(config, hashService, schemaProvider);
     }
 
@@ -210,10 +213,10 @@ class DefaultValidationServiceTest {
     @Test
     void validateMetadataWithInvalidSchemaAttribute() {
 
-        Metadata metadata = new Metadata();
+        final Metadata metadata = new Metadata();
         metadata.setSchema("https://schema.fitko.de/fit-connect/metadata/2.0.0/metadata.schema.json");
 
-        ValidationResult validationResult = this.underTest.validateMetadataSchema(metadata);
+        final ValidationResult validationResult = underTest.validateMetadataSchema(metadata);
 
         assertTrue(validationResult.hasError());
         assertThat(validationResult.getError().getClass(), equalTo(ValidationException.class));
@@ -363,6 +366,45 @@ class DefaultValidationServiceTest {
         assertFalse(validationResult.hasError());
     }
 
+    @Test
+    void testValidDestinationPayload() {
+
+        // Given
+        final Map<String,Object> claims = Map.of(
+                "iss", "submission-service",
+                "jti", UUID.randomUUID().toString(),
+                "iat", new Date().getTime(),
+                "destinationId", UUID.randomUUID().toString(),
+                "submissionHost", "host",
+                "services", Collections.emptyList()
+        );
+
+        // When
+        final ValidationResult validationResult = underTest.validateDestinationSchema(claims);
+
+        // Then
+        assertTrue(validationResult.isValid());
+    }
+
+    @Test
+    void testDestinationPayloadIsMissingMandatoryClaims() {
+
+        // Given
+        final Map<String,Object> claims = Map.of("test", "claim");
+
+        // When
+        final ValidationResult validationResult = underTest.validateDestinationSchema(claims);
+
+        // Then
+        assertTrue(validationResult.hasError());
+        assertThat(validationResult.getError().getMessage(), containsString("jti: is missing but it is required"));
+        assertThat(validationResult.getError().getMessage(), containsString("iss: is missing but it is required"));
+        assertThat(validationResult.getError().getMessage(), containsString("iat: is missing but it is required"));
+        assertThat(validationResult.getError().getMessage(), containsString("services: is missing but it is required"));
+        assertThat(validationResult.getError().getMessage(), containsString("submissionHost: is missing but it is required"));
+        assertThat(validationResult.getError().getMessage(), containsString("destinationId: is missing but it is required"));
+    }
+
     @Test
     void testValidJson() throws IOException {
 
@@ -395,13 +437,13 @@ class DefaultValidationServiceTest {
     @Test
     void validateCallback() {
 
-        MessageDigestService mockedMessageDigestService = mock(MessageDigestService.class);
+        final MessageDigestService mockedMessageDigestService = mock(MessageDigestService.class);
         when(mockedMessageDigestService.calculateHMAC(anyString(), anyString())).thenReturn("valid");
 
-        DefaultValidationService defaultValidationService = new DefaultValidationService(
+        final DefaultValidationService defaultValidationService = new DefaultValidationService(
                 new ApplicationConfig(), mockedMessageDigestService, mock(SchemaProvider.class));
 
-        ValidationResult validationResult = defaultValidationService.validateCallback(
+        final ValidationResult validationResult = defaultValidationService.validateCallback(
                 "valid", ZonedDateTime.now().toInstant().toEpochMilli(), "body", "secret");
 
         verify(mockedMessageDigestService, times(1)).calculateHMAC(anyString(), anyString());
@@ -412,7 +454,7 @@ class DefaultValidationServiceTest {
     @Test
     void validateCallbackWithExpiredTimestamp() {
 
-        ValidationResult validationResult = this.underTest.validateCallback("", 0L, "", "");
+        final ValidationResult validationResult = underTest.validateCallback("", 0L, "", "");
 
         assertFalse(validationResult.isValid());
         assertTrue(validationResult.hasError());
@@ -422,7 +464,7 @@ class DefaultValidationServiceTest {
     @Test
     void validateCallbackWithInvalidHmac() {
 
-        ValidationResult validationResult = this.underTest.validateCallback(
+        final ValidationResult validationResult = underTest.validateCallback(
                 "invalid", ZonedDateTime.now().toInstant().toEpochMilli(), "body", "secret");
 
         assertFalse(validationResult.isValid());
diff --git a/core/src/test/resources/portal_well_known_signature_key.json b/core/src/test/resources/portal_well_known_signature_key.json
new file mode 100644
index 0000000000000000000000000000000000000000..b273876e27c055a48b994743a868fb5bc36d0437
--- /dev/null
+++ b/core/src/test/resources/portal_well_known_signature_key.json
@@ -0,0 +1,11 @@
+{
+  "kty": "RSA",
+  "e": "AQAB",
+  "kid": "aeBUhQS8uaJvtzMcTyiEAN3KW4m65uDmL0X1AAIqdCE",
+  "key_ops": [
+    "verify"
+  ],
+  "alg": "PS512",
+  "n": "4Y0sJhadfrQnNZXeS7Pqh73FvtFPXLvLw11h7OiZM0DlqvRNgoYHO5k-kxJKOVCaFek0LjKM1_VQxMVpdChCkHeapdTg60oQTQZj3pG0boR3LStbqN3hNEx_JZC4aHH16kau0vqBBPiOOoq-ExUz-hXz_GMLsp9QVqIkw9okO_tzNPjQOo--GM8r4eSsKzgSHZzmepc9Gfk16eraGicBevlkclk32TmWIE_ErD31dtVbBlK-7GG2NUe-o_5rkiCJ2EwKRHZlLkBYJkkj_IjeUdKc4dawXoE8L83DSBPyapX47_L1VHTnT0hJdOVe6WHtvzzpusZ0Au-YDhp6LSwXnU9d0-VzBJmQvtrep1FM0d9aQrz0e0lVf8wCn13VdKO_FBZw9D7i0XRhF8JqQRblqhcCY7UGshbTTM8HORMFONHFmSQm10qfV29PLmztOhIuubMyYe1DPnlfRkpn5jnt8IPoopl6MliDKSc3m4dgG23KylBpTLr3U-XGQrTlerjrYh4t1LXiJ-jQhLefkak_WnExZJSXv601BgmbGj3GdIhS6lxdMX62cOuwKLVISOmHHxvimpQwhtYwiFR9OmGoKVgtCQ5eMKLwGWVwXSvUJ5YXH-yUyNW1_vOrt0DAtYmXwS_Ij0bMg9WoXKJ-5NtQpnnIzw1lr5bW5fNn2TgWpHk"
+}
+
diff --git a/core/src/test/resources/sample_route.json b/core/src/test/resources/sample_route.json
new file mode 100644
index 0000000000000000000000000000000000000000..ba509741a65ef6b606a4cd2026398924a8effefd
--- /dev/null
+++ b/core/src/test/resources/sample_route.json
@@ -0,0 +1,46 @@
+{
+  "destinationId": "d40e7b13-da98-4b09-9e16-bbd61ca81510",
+  "destinationSignature": "eyJraWQiOiJhZUJVaFFTOHVhSnZ0ek1jVHlpRUFOM0tXNG02NXVEbUwwWDFBQUlxZENFIiwidHlwIjoiSldUIiwiYWxnIjoiUFM1MTIifQ.eyJzdWJtaXNzaW9uSG9zdCI6InN1Ym1pc3Npb24tYXBpLXRlc3RpbmcuZml0LWNvbm5lY3QuZml0a28uZGV2IiwiaXNzIjoiaHR0cHM6XC9cL3BvcnRhbC5hdXRoLXRlc3RpbmcuZml0LWNvbm5lY3QuZml0a28uZGV2Iiwic2VydmljZXMiOlt7ImdlYmlldElEcyI6WyJ1cm46ZGU6YnVuZDpkZXN0YXRpczpiZXZvZWxrZXJ1bmdzc3RhdGlzdGlrOnNjaGx1ZXNzZWw6cnM6MDY0MzUwMDE0MDE0Il0sImxlaXN0dW5nSURzIjpbInVybjpkZTpmaW06bGVpa2E6bGVpc3R1bmc6OTkxMjM0NTY3NjA2MTAiXX1dLCJkZXN0aW5hdGlvbklkIjoiZDQwZTdiMTMtZGE5OC00YjA5LTllMTYtYmJkNjFjYTgxNTEwIiwiaWF0IjoxNjUyMjkxMzkwLCJqdGkiOiJkOGI1NTM2NS1mNDMzLTRiNjMtYjg3Yi0xZWRiNTY3YzlmYWMifQ.RSdgZWBwPvsnlCajXF3Rh8uPDEKkAwgxNbzuO5HWCaAKroHQ8NQtoDGHe2iXFULR8ML7Ca5aHmGKR34CgGmdpxitbzDn_rjHe2WModuRclu8n_eEmGhMTAkcH1aQ8pcDnQAcfI44KPqQZ_D8X6IGqxOEMtoYiETay8OAN3Vzk1Ew9n4vrvK5r5eWIx-nu5uMtHhMqT7xg09jH0Ma4owfCiOrobEdM5fz9a5sWoi0aBTufEMR9ai-SuDR1ibNmwD7s7wYCdqCgaOtj-_dNbPNOYVUdHdRKMvPuFYEx8rs32mehgEEHX649m8QN0FAHsuXdPeagFv7ndceH2vrPPenKk6fz6W68fGZhfs6MzaoUBaz2p3uZJdTCk5gPrJu46TBW0uPEI8dY8Aw_SNMGWsyfviAzjbuhJ3JBoKk8tjPjkERWF9hovlrXu9l9s-jbAn-U-PYWsLYNAaCqvMEGLNmyTnsEMQuKWCQDr1mi0K8QqPiPNDe4NHmy49ObcesLB64LIE5did6XVUGOu3qOACg-s9dN0tFmqEw_-CWOhiWoAQRcu4uyzjCUWKoa6zpw_G3I5STzV328bwHf4hBDnTzZ8PH9OwG0vwqX1jeGdqYz3fWEz8gdU_N18t10393BYJy1tEggCnZzIVBqjQXAjV4pcI6G19D-xT3ITonxdMTAZw",
+  "destinationParameters": {
+    "encryptionKid": "1e95f036-ccff-425c-a0de-3d89d5cc59fa",
+    "metadataVersions": [
+      "1.0.0"
+    ],
+    "publicKeys": {
+      "keys": [
+        {
+          "kty": "RSA",
+          "key_ops": [
+            "wrapKey"
+          ],
+          "alg": "RSA-OAEP-256",
+          "x5c": [
+            "MIIE6jCCAtKgAwIBAgIGAXo1pG0GMA0GCSqGSIb3DQEBCwUAMDYxNDAyBgNVBAMMK2FaV0ptMG1XRmFxRGFOTkJOaDNabVluVmw3eG52SVMxVEJ5SHhaVnNUNm8wHhcNMjEwNjIyMjEzMzI2WhcNMjIwNDE4MjEzMzI2WjA2MTQwMgYDVQQDDCthWldKbTBtV0ZhcURhTk5CTmgzWm1ZblZsN3hudklTMVRCeUh4WlZzVDZvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2ch1Ir3/Lyb9/HxW9RqIodxi9fXhix6APKwqiSfi+JlRqVa1FoAFsW1nW0IbQjkW6sNkWUFWuA9AfVoKT9nnIcnLSjSQ84SI+if6qTbornyKvBjXg8BSecSUUPYyT0+4NmxrXMGHPYbJV7fQq6jPXzkWC5P5jqQ7ObraQp752BcE/JVQUmFk1ydhhbnroHpGUkA+8jG/kiVL+lAz7uUmZCh6i3ZJD5HN1JOE5LMyzUQOgOFUUPiviBywQAbPQuDLydZ2diO5wqm4mwBadAAzC27OllSkNXSgnd9MVajXmtBVpz2ksMaSCAbfB4rK9q5jXd5YMwu1ZlA+ZuWKYm/p1GjbZdx4xk9w23Zkgnrr3SvWnW98686fd03MG1ACAGatq5FcAvGp8BXCKwz5FpyYtOONx+tECYHcHhx/SafOe9siLYObLmBSsLF3TAjigZjpGOuEjBtKyv5OwJj+6YfIYYjlofuqv6GHUGDvv8iQsy6U4eHCoRpKJzmX6L22MUQgisYvQdGY2jbdEni3g/MpciMIbnZFLENVrqHXYcgHN+SbXl/GVR5b3F0ompES55xA7fuYlt4lp5j0IUo0OWM2/tYYHtASZicVAwnbzLQZEA0u+wXZr0ByMWE07Od/KaLUomlBPi1Ac/FU3KOx0APKJUm7D3//aiLZll3Sh9EnIvECAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAFi24JFEmYL2sOqaaQOMHCVm+WQ9QyN/rkvjHLG8th5TIF8NJ4uwxl+n6GBO2uOZOBixHbU5pK9slyt9Xryw7CbL/hhRGSs3GSFR2hkxoBHdzfFnAwegnd1H5XHQwSgh43jGOn9AxsyISpkvFo7+DObh9Cv8NsjpL57No4UJ62XggfYJW43u4+I/fHcDwnvIN0dvnYpQkCbTmrjLa5IkGim9BynfW1k6VuxLya1SsyjBHWw2YNQ4xBJI4OzXhL6OmBSrohF1RbIKOpjtqGfkXZpxufLNV2CnL36r/41c1nop6cmCIMDtnFEQdAmGe8m/8wvVpLnks59C02/WotlK3iORHCYB6G0pHMKFB4zOVANYtLFgqTgN4HNciV3FN0TvI19qzjkAdcB+m+L+LdseIzcQ/BToGyPvWkJ1mvJZIp0ejnlMWIl3VlNpMKeZ7lJbPpZvABO00lK+FynhITtb6N29toE+7JgHAlWmxw6PFFY1x+3xTHBTOU0oUR/TyKsEU0+bNSb/0S+ZyodmnIFbgYWarjK5pUwfTRyPyeVEukg1Gf30c/7f/5KZ/dpLFUNBb/YTNIzYEhGNUyLJ1mrSz33gr4MtvI4uSu0Jpr1NrwdMGvFhr5QOCULuoC9KlokusUpi0GTH0gK3K/TUi6qvU+Wztfa7mqah17BVVFT1wATs="
+          ],
+          "kid": "1e95f036-ccff-425c-a0de-3d89d5cc59fa",
+          "n": "2ch1Ir3_Lyb9_HxW9RqIodxi9fXhix6APKwqiSfi-JlRqVa1FoAFsW1nW0IbQjkW6sNkWUFWuA9AfVoKT9nnIcnLSjSQ84SI-if6qTbornyKvBjXg8BSecSUUPYyT0-4NmxrXMGHPYbJV7fQq6jPXzkWC5P5jqQ7ObraQp752BcE_JVQUmFk1ydhhbnroHpGUkA-8jG_kiVL-lAz7uUmZCh6i3ZJD5HN1JOE5LMyzUQOgOFUUPiviBywQAbPQuDLydZ2diO5wqm4mwBadAAzC27OllSkNXSgnd9MVajXmtBVpz2ksMaSCAbfB4rK9q5jXd5YMwu1ZlA-ZuWKYm_p1GjbZdx4xk9w23Zkgnrr3SvWnW98686fd03MG1ACAGatq5FcAvGp8BXCKwz5FpyYtOONx-tECYHcHhx_SafOe9siLYObLmBSsLF3TAjigZjpGOuEjBtKyv5OwJj-6YfIYYjlofuqv6GHUGDvv8iQsy6U4eHCoRpKJzmX6L22MUQgisYvQdGY2jbdEni3g_MpciMIbnZFLENVrqHXYcgHN-SbXl_GVR5b3F0ompES55xA7fuYlt4lp5j0IUo0OWM2_tYYHtASZicVAwnbzLQZEA0u-wXZr0ByMWE07Od_KaLUomlBPi1Ac_FU3KOx0APKJUm7D3__aiLZll3Sh9EnIvE",
+          "e": "AQAB"
+        }
+      ]
+    },
+    "replyChannels": {
+      "deMail": null,
+      "elster": null,
+      "eMail": {
+        "usePgp": true
+      },
+      "fink": null
+    },
+    "status": "active",
+    "submissionSchemas": [
+      {
+        "schemaUri": "https://schema.fitko.de/fim/s06000178_0.4.schema.json",
+        "mimeType": "application/json"
+      }
+    ],
+    "submissionUrl": "https://submission-api-testing.fit-connect.fitko.dev"
+  },
+  "destinationParametersSignature": "eyJraWQiOiIzMjg1ODE0Ny1mMDkwLTQzYTktYjJmZC1kMjZhZTViNDFjMDMiLCJjdHkiOiJhcHBsaWNhdGlvblwvam9zZSIsInR5cCI6IkpPU0UiLCJhbGciOiJQUzUxMiJ9..uzPUSfF-SRfVmES8G7uNWW199sqbVElrrGmf8THd4BzavAHrw00yrWiUNpXcHT6KsZS5y4tW8gx6oBlyhlleb9SONygHy2N6ooknHnYwfowPmqk2xsRNn-sU1TPOuGsIgcPEirnp7jn1xa9-LMxA2Pbao8PAGVOtNw7Hkh4xbBrh3sI_6A2Yxg-s3puekBnH3j19eu5-i3otiG9hEQb70D2d-yyNfzCF4-kqFk2uuR8PihjBD9gfqtPDd7xYZ-htyd6jqMVOO0CI2ryiN6SDd-rybw0HK63EfOop4cCoCmk8FXJH9UywoRNRK90BKUtlqBYWR7byZ30qk_YZrovN7yGKLEc9Jpc-YXsx17MDuU76G8dv_YaN4d8Vo82Pqd3r7l_NdLPQEdMGYJMtIL70APjUHV9SN131NhX3MTH_aeNzduifZy3HZmFETtnl6Ly_pEiYGwAYYh3CBXut6VxRKu2g9MQ60X-k39IoVULcOEt7NwpGd5bHaj4EtRn5u8CrY9A0-6kOWFSjij3nlDmcSk4zCOVRf5OddONnlFk-AqxI9i1hqtlJWfDrP3tNTowSzy-ysXEn00H4xrpZfXofZSPpEASsSvYH4GudNpoup32tWfUwrfCLbNrdiMCkvjZ0rFaTeq5IBtWvF7txllfP2MA-Dh2NRf0uK98tzJouado",
+  "destinationName": "OEID_TC_010_OE01_99 Name Titel  ",
+  "destinationLogo": "https://address.local/logo"
+}
\ No newline at end of file
diff --git a/core/src/test/resources/submission_service_well_known_signature_key.json b/core/src/test/resources/submission_service_well_known_signature_key.json
new file mode 100644
index 0000000000000000000000000000000000000000..7bd895f4fb923b7b52c86f1829613bf4a2db8666
--- /dev/null
+++ b/core/src/test/resources/submission_service_well_known_signature_key.json
@@ -0,0 +1,10 @@
+{
+  "kty": "RSA",
+  "e": "AQAB",
+  "kid": "32858147-f090-43a9-b2fd-d26ae5b41c03",
+  "key_ops": [
+    "verify"
+  ],
+  "alg": "PS512",
+  "n": "3UltHnOKF38UbpfQ-sG6_p-_SiWTzDohXE1PT6I5xpnQncczwyAF8j-PZv8TxxtvuVxYVJdt610B3bMCwPaD1Kw2ht1mS_Iu2d_SqTxhCDF4S0yrCovo6fKVQXG-IPsbs59-1lefUqjrUbkxRRxH335p22Lmg9Wf0XBJqMWsMBNnul8lKjLP7krk9TKQKR35js42oIliEakByWpq6kzPPlgV4mkFlivnseQaLxItFMh6rs5lLS1kHdrYfwCWS97wf5TO2ubygo_617qKAeN3e6mYYb6k_30WYwnH-vc1_gMf8JuBOZ7sO-OpW09XrQLBkUXTHbIcmLExpm3yOizc-UI9NrLgwomPyg0Ml1n3bKpIWq6dIpyO2LJ6euu8CzPDs2NNv12Z_FUuJMPQWvt-nv3g6AgOHPJZQJ8TjI27yPgjFwtef91OT1jQ_IzgCVp7EFNn-rZtgxhV2PLmhXick8jLszodEgcki5Ooj5oBbze0yV7zPZ3cdXle_NwDKlxkwbAs3WCyTY7mOVA0avsW9cDDwplQnBMDbwKSGOqjDHVI2EY2SN4ur0lYp-gm5IxtTMm2d4CjAXh4XVHBvNNLxBJb9byDA7qM1QgJa1DaKdV5yoTL2VNTWUBYbK4ag7K6avACM7KjL9YvzUIYNnegu5qFtUVXfjBlE_5wNbYzBTk"
+}
\ No newline at end of file