diff --git a/afs-server/src/main/java/ch/ethz/sis/afsserver/server/impl/ApiServerAdapter.java b/afs-server/src/main/java/ch/ethz/sis/afsserver/server/impl/ApiServerAdapter.java
index d8ce01b6482aee8051b22c42888736491477c6bf..20aed622dd6e6ce3803a4b94f6e111cda171950e 100644
--- a/afs-server/src/main/java/ch/ethz/sis/afsserver/server/impl/ApiServerAdapter.java
+++ b/afs-server/src/main/java/ch/ethz/sis/afsserver/server/impl/ApiServerAdapter.java
@@ -5,6 +5,7 @@ import ch.ethz.sis.afsserver.http.*;
 import ch.ethz.sis.afsserver.server.*;
 import ch.ethz.sis.afsserver.server.performance.Event;
 import ch.ethz.sis.afsserver.server.performance.PerformanceAuditor;
+import ch.ethz.sis.shared.io.IOUtils;
 import ch.ethz.sis.shared.json.JSONObjectMapper;
 import ch.ethz.sis.shared.log.LogManager;
 import ch.ethz.sis.shared.log.Logger;
@@ -43,7 +44,7 @@ public class ApiServerAdapter<CONNECTION, API> implements HttpServerHandler {
             String transactionManagerKey = null;
             Map<String, Object> methodParameters = new HashMap<>();
             for (Map.Entry<String, List<String>> entry:uriParameters.entrySet()) {
-                Object value = null;
+                String value = null;
                 if (entry.getValue() != null) {
                     if (entry.getValue().size() == 1) {
                         value = entry.getValue().get(0);
@@ -54,16 +55,31 @@ public class ApiServerAdapter<CONNECTION, API> implements HttpServerHandler {
 
                 switch (entry.getKey()) {
                     case "method":
-                        method = (String) value;
+                        method = value;
                         break;
                     case "sessionToken":
-                        sessionToken = (String) value;
+                        sessionToken = value;
                         break;
                     case "interactiveSessionKey":
-                        interactiveSessionKey = (String) value;
+                        interactiveSessionKey = value;
                         break;
                     case "transactionManagerKey":
-                        transactionManagerKey = (String) value;
+                        transactionManagerKey = value;
+                        break;
+                    case "transactionId":
+                        methodParameters.put(entry.getKey(), UUID.fromString(value));
+                        break;
+                    case "recursively":
+                        methodParameters.put(entry.getKey(), Boolean.valueOf(value));
+                        break;
+                    case "offset":
+                        methodParameters.put(entry.getKey(), Long.valueOf(value));
+                        break;
+                    case "limit":
+                        methodParameters.put(entry.getKey(), Integer.valueOf(value));
+                        break;
+                    case "md5Hash":
+                        methodParameters.put(entry.getKey(), IOUtils.decodeBase64(value));
                         break;
                     default:
                         methodParameters.put(entry.getKey(), value);
@@ -71,6 +87,10 @@ public class ApiServerAdapter<CONNECTION, API> implements HttpServerHandler {
                 }
             }
 
+            if (method.equals("write")) {
+                methodParameters.put("data", requestBody);
+            }
+
             ApiRequest apiRequest = new ApiRequest("1", method, methodParameters, sessionToken, interactiveSessionKey, transactionManagerKey);
             Response response = server.processOperation(apiRequest, apiResponseBuilder, performanceAuditor);
             HttpResponse httpResponse = getHTTPResponse(response);
diff --git a/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/APIServerAdapterWrapper.java b/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/APIServerAdapterWrapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..301484634415e3c955dca573e25dc0c24c7834b0
--- /dev/null
+++ b/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/APIServerAdapterWrapper.java
@@ -0,0 +1,91 @@
+package ch.ethz.sis.afsserver.impl;
+
+
+import ch.ethz.sis.afsserver.core.AbstractPublicAPIWrapper;
+import ch.ethz.sis.afsserver.http.HttpResponse;
+import ch.ethz.sis.afsserver.server.impl.ApiRequest;
+import ch.ethz.sis.afsserver.server.impl.ApiResponse;
+import ch.ethz.sis.afsserver.server.impl.ApiServerAdapter;
+import ch.ethz.sis.afsserver.server.performance.PerformanceAuditor;
+import ch.ethz.sis.shared.io.IOUtils;
+import ch.ethz.sis.shared.json.JSONObjectMapper;
+import ch.ethz.sis.shared.log.LogManager;
+import ch.ethz.sis.shared.log.Logger;
+import io.netty.handler.codec.http.HttpMethod;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+import static ch.ethz.sis.afsserver.http.HttpResponse.CONTENT_TYPE_BINARY_DATA;
+import static ch.ethz.sis.afsserver.http.HttpResponse.CONTENT_TYPE_JSON;
+
+public class APIServerAdapterWrapper extends AbstractPublicAPIWrapper {
+
+    private static final Logger logger = LogManager.getLogger(APIServerAdapterWrapper.class);
+
+    private ApiServerAdapter apiServerAdapter;
+    private JSONObjectMapper jsonObjectMapper;
+
+    public APIServerAdapterWrapper(ApiServerAdapter apiServerAdapter, JSONObjectMapper jsonObjectMapper) {
+        this.apiServerAdapter = apiServerAdapter;
+        this.jsonObjectMapper = jsonObjectMapper;
+    }
+
+    public Map<String, List<String>> getURIParameters(Map<String, Object> args) {
+        Map<String, List<String>> result = new HashMap<>(args.size());
+        for (Map.Entry<String, Object> entry:args.entrySet()) {
+            if (entry.getKey().equals("data") && entry.getValue() instanceof byte[]) {
+                continue; // Skip
+            } else if(entry.getValue() instanceof byte[]) {
+                result.put(entry.getKey(), List.of(IOUtils.encodeBase64((byte[]) entry.getValue())));
+            } else {
+                result.put(entry.getKey(), List.of(String.valueOf(entry.getValue())));
+            }
+        }
+        return result;
+    }
+
+    public <E> E process(String method, Map<String, Object> args) {
+        try {
+            HttpMethod httpMethod = null;
+            switch (method){
+                case "delete":
+                    httpMethod = HttpMethod.DELETE;
+                    break;
+                case "write":
+                    httpMethod = HttpMethod.PUT;
+                    break;
+                case "list":
+                case "read":
+                case "isSessionValid":
+                    httpMethod = HttpMethod.GET;
+                    break;
+                default:
+                    httpMethod = HttpMethod.POST;
+            }
+
+            Map<String, List<String>> uriParameters = getURIParameters(args);
+            uriParameters.put("method", List.of(method));
+            uriParameters.put("sessionToken", List.of(UUID.randomUUID().toString()));
+
+            byte[] requestBody = null;
+            if (method.equals("write")) {
+                requestBody = (byte[]) args.get("data");
+            }
+
+            HttpResponse response = apiServerAdapter.process(httpMethod, uriParameters, requestBody);
+            switch (response.getContentType()) {
+                case CONTENT_TYPE_BINARY_DATA:
+                    return (E) response.getBody();
+                case CONTENT_TYPE_JSON:
+                    ApiResponse apiResponse = jsonObjectMapper.readValue(new ByteArrayInputStream(response.getBody()), ApiResponse.class);
+                    return  (E) apiResponse.getResult();
+            }
+        } catch (Throwable throwable) {
+            throw new RuntimeException(throwable);
+        }
+        throw new RuntimeException("This line should be unreachable");
+    }
+
+}
diff --git a/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/PublicAPIAdapterWrapper.java b/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/APIServerWrapper.java
similarity index 82%
rename from afs-server/src/test/java/ch/ethz/sis/afsserver/impl/PublicAPIAdapterWrapper.java
rename to afs-server/src/test/java/ch/ethz/sis/afsserver/impl/APIServerWrapper.java
index 42e5ba48c0aae18b147d3dde1db0305ee8603751..356e728da2f50e97fc6f6602e8f1c2f380a6aa7c 100644
--- a/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/PublicAPIAdapterWrapper.java
+++ b/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/APIServerWrapper.java
@@ -13,15 +13,15 @@ import ch.ethz.sis.shared.log.Logger;
 import java.util.Map;
 import java.util.UUID;
 
-public class PublicAPIAdapterWrapper extends AbstractPublicAPIWrapper {
+public class APIServerWrapper extends AbstractPublicAPIWrapper {
 
-    private static final Logger logger = LogManager.getLogger(PublicAPIAdapterWrapper.class);
+    private static final Logger logger = LogManager.getLogger(APIServerWrapper.class);
 
     private APIServer apiServer;
     private final ApiResponseBuilder apiResponseBuilder;
 
-    public PublicAPIAdapterWrapper(APIServer apiServerAdapter) {
-        this.apiServer = apiServerAdapter;
+    public APIServerWrapper(APIServer apiServer) {
+        this.apiServer = apiServer;
         this.apiResponseBuilder = new ApiResponseBuilder();
     }
 
diff --git a/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/ApiServerAdapterTest.java b/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/ApiServerAdapterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..6067da289e4d5eb6868c1ac8242c71de41c5c47f
--- /dev/null
+++ b/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/ApiServerAdapterTest.java
@@ -0,0 +1,21 @@
+package ch.ethz.sis.afsserver.impl;
+
+import ch.ethz.sis.afsserver.ServerClientEnvironmentFS;
+import ch.ethz.sis.afsserver.api.PublicAPI;
+import ch.ethz.sis.afsserver.server.APIServer;
+import ch.ethz.sis.afsserver.server.impl.ApiServerAdapter;
+import ch.ethz.sis.afsserver.startup.AtomicFileSystemServerParameter;
+import ch.ethz.sis.shared.json.JSONObjectMapper;
+import ch.ethz.sis.shared.startup.Configuration;
+
+public class ApiServerAdapterTest extends ApiServerTest {
+
+    @Override
+    public PublicAPI getPublicAPI() throws Exception {
+        APIServer apiServer = getAPIServer();
+        Configuration configuration = ServerClientEnvironmentFS.getInstance().getDefaultServerConfiguration();
+        JSONObjectMapper jsonObjectMapper = configuration.getSharableInstance(AtomicFileSystemServerParameter.jsonObjectMapperClass);
+        ApiServerAdapter apiServerAdapter = new ApiServerAdapter(apiServer, jsonObjectMapper);
+        return new APIServerAdapterWrapper(apiServerAdapter, jsonObjectMapper);
+    }
+}
diff --git a/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/ApiServerTest.java b/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/ApiServerTest.java
index e88563710e71a09aaee239a3c912eefe9ebbde9f..878ced130896a38df4ec6b9a9339c1ab98030316 100644
--- a/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/ApiServerTest.java
+++ b/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/ApiServerTest.java
@@ -15,8 +15,7 @@ import ch.ethz.sis.shared.startup.Configuration;
 
 public class ApiServerTest extends PublicApiTest {
 
-    @Override
-    public PublicAPI getPublicAPI() throws Exception {
+    protected APIServer getAPIServer() throws Exception {
         Configuration configuration = ServerClientEnvironmentFS.getInstance().getDefaultServerConfiguration();
 
         ConnectionFactory connectionFactory = new ConnectionFactory();
@@ -35,6 +34,11 @@ public class ApiServerTest extends PublicApiTest {
         observer.init(configuration);
         APIServer apiServer = new APIServer(connectionsPool, workersPool, PublicAPI.class, interactiveSessionKey, transactionManagerKey, apiServerWorkerTimeout, observer);
         observer.init(apiServer, configuration);
-        return new PublicAPIAdapterWrapper(apiServer);
+        return apiServer;
+    }
+
+    @Override
+    public PublicAPI getPublicAPI() throws Exception {
+        return new APIServerWrapper(getAPIServer());
     }
 }
diff --git a/afs/build.gradle b/afs/build.gradle
index 89f23edfc9a2570f8196e49be65ee78ea9ba51c9..e9fc6a1dc7286f834adbd59fc556e30c349d190f 100644
--- a/afs/build.gradle
+++ b/afs/build.gradle
@@ -19,7 +19,8 @@ dependencies {
                     'log4j:log4j-core:2.10.0',
                     'fasterxml:jackson-annotations:2.9.10',
                     'fasterxml:jackson-core:2.9.10',
-                    'fasterxml:jackson-databind:2.9.10.8';
+                    'fasterxml:jackson-databind:2.9.10.8',
+                    'fasterxml:jackson-datatype-jsr310:2.9.10';
     testImplementation 'junit:junit:4.10'
     testRuntimeOnly 'hamcrest:hamcrest-core:1.3'
 }
diff --git a/afs/src/main/java/ch/ethz/sis/shared/io/IOUtils.java b/afs/src/main/java/ch/ethz/sis/shared/io/IOUtils.java
index 95562011cd0cdd3a471934fdaab0ee66717a8b47..4f1b4a076e6271ca9a7964eacb69cd34c405d834 100644
--- a/afs/src/main/java/ch/ethz/sis/shared/io/IOUtils.java
+++ b/afs/src/main/java/ch/ethz/sis/shared/io/IOUtils.java
@@ -47,10 +47,7 @@ import java.nio.file.attribute.UserPrincipal;
 import java.security.MessageDigest;
 import java.time.OffsetDateTime;
 import java.time.ZoneId;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 
 public class IOUtils {
 
@@ -617,6 +614,14 @@ public class IOUtils {
         }
     }
 
+    public static String encodeBase64(byte[] input) {
+        return Base64.getEncoder().encodeToString(input);
+    }
+
+    public static byte[] decodeBase64(String input) {
+        return Base64.getDecoder().decode(input);
+    }
+
     public static boolean exists(String source) {
         Path sourcePath = getPathObject(source);
         return Files.exists(sourcePath);
diff --git a/afs/src/main/java/ch/ethz/sis/shared/json/jackson/JSONObjectMapper.java b/afs/src/main/java/ch/ethz/sis/shared/json/jackson/JSONObjectMapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..9684c26621a0c7026f5c51e5a8b81991595eca90
--- /dev/null
+++ b/afs/src/main/java/ch/ethz/sis/shared/json/jackson/JSONObjectMapper.java
@@ -0,0 +1,16 @@
+package ch.ethz.sis.shared.json.jackson;
+
+import java.io.InputStream;
+import java.nio.charset.Charset;
+
+public interface JSONObjectMapper {
+
+    Charset getCharset();
+
+    void setCharset(Charset charset);
+
+    <T> T readValue(InputStream src, Class<T> valueType) throws Exception;
+
+    byte[] writeValue(Object value) throws Exception;
+
+}
diff --git a/afs/src/main/java/ch/ethz/sis/shared/json/jackson/JacksonObjectMapper.java b/afs/src/main/java/ch/ethz/sis/shared/json/jackson/JacksonObjectMapper.java
index 9f46f8cb2d11587d20b2fde27cc1b559addd67bd..40b16605109b693ee4e32c91703d712a0861d4a9 100644
--- a/afs/src/main/java/ch/ethz/sis/shared/json/jackson/JacksonObjectMapper.java
+++ b/afs/src/main/java/ch/ethz/sis/shared/json/jackson/JacksonObjectMapper.java
@@ -20,6 +20,7 @@ import ch.ethz.sis.shared.json.JSONObjectMapper;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 
 import java.io.InputStream;
 
@@ -49,6 +50,7 @@ public class JacksonObjectMapper implements JSONObjectMapper
     public JacksonObjectMapper()
     {
         objectMapper = new ObjectMapper();
+        objectMapper.registerModule(new JavaTimeModule()); // new module, NOT JSR310Module
         objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
         objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
         objectMapper.enableDefaultTyping();