diff --git a/mailbox-core/build.gradle b/mailbox-core/build.gradle
index cbf2b5ce91682019cceb3b02207fc2646bfa496b..198bff74dfbca8d44375f04a8b78fe24d8b7a4b1 100644
--- a/mailbox-core/build.gradle
+++ b/mailbox-core/build.gradle
@@ -23,6 +23,7 @@ dependencies {
     implementation "io.ktor:ktor-server-core:$ktor_version"
     implementation "io.ktor:ktor-server-netty:$ktor_version"
     implementation "io.ktor:ktor-auth:$ktor_version"
+    implementation "io.ktor:ktor-jackson:$ktor_version"
     api "org.slf4j:slf4j-api:1.7.32"
 
     implementation 'com.h2database:h2:1.4.192' // The last version that supports Java 1.6
@@ -34,6 +35,7 @@ dependencies {
     testImplementation "io.mockk:mockk:$mockk_version"
     testImplementation "ch.qos.logback:logback-classic:1.2.5"
     testImplementation "io.ktor:ktor-client-cio:$ktor_version"
+    testImplementation "io.ktor:ktor-client-jackson:$ktor_version"
     testImplementation "com.google.dagger:hilt-core:$hilt_version"
     kaptTest "com.google.dagger:dagger-compiler:$hilt_version"
 }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/api/Contact.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/contacts/Contact.kt
similarity index 57%
rename from mailbox-core/src/main/java/org/briarproject/mailbox/core/api/Contact.kt
rename to mailbox-core/src/main/java/org/briarproject/mailbox/core/contacts/Contact.kt
index 86eded1fdf72cb27b6c025e6396725026c5f25a2..2565668dfab29e2636dc4d8d96c990820769df09 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/api/Contact.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/contacts/Contact.kt
@@ -1,7 +1,7 @@
-package org.briarproject.mailbox.core.api
+package org.briarproject.mailbox.core.contacts
 
 data class Contact(
-    val id: Int,
+    val contactId: Int,
     val token: String,
     val inboxId: String,
     val outboxId: String,
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt
index e108f9da656e355e7ef445cca88cde27ed214cc1..7fbb0c5c8dc23f5a8925bbf7f1c9d5f65d411b1b 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt
@@ -1,6 +1,6 @@
 package org.briarproject.mailbox.core.db
 
-import org.briarproject.mailbox.core.api.Contact
+import org.briarproject.mailbox.core.contacts.Contact
 import org.briarproject.mailbox.core.settings.Settings
 
 interface Database : TransactionManager {
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt
index 651263331b3c9c85bf447f1d22a93e188eaeab7c..c85ecb099fcb7700029688f28c1b6490cef5fa6f 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt
@@ -1,6 +1,6 @@
 package org.briarproject.mailbox.core.db
 
-import org.briarproject.mailbox.core.api.Contact
+import org.briarproject.mailbox.core.contacts.Contact
 import org.briarproject.mailbox.core.db.DatabaseConstants.Companion.DB_SETTINGS_NAMESPACE
 import org.briarproject.mailbox.core.db.DatabaseConstants.Companion.DIRTY_KEY
 import org.briarproject.mailbox.core.db.DatabaseConstants.Companion.LAST_COMPACTED_KEY
@@ -449,7 +449,7 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc
                                 VALUES (?, ?, ?, ?)
             """.trimIndent()
             ps = connection.prepareStatement(sql)
-            ps.setInt(1, contact.id)
+            ps.setInt(1, contact.contactId)
             ps.setString(2, contact.token)
             ps.setString(3, contact.inboxId)
             ps.setString(4, contact.outboxId)
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt
index 6d4f5a8a1f533c359862249344b757dfe51018c9..1982b8b503842cbce8db6c2d8ed4d2befd41bc07 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt
@@ -1,7 +1,7 @@
 package org.briarproject.mailbox.core.server
 
 import io.ktor.auth.Principal
-import org.briarproject.mailbox.core.api.Contact
+import org.briarproject.mailbox.core.contacts.Contact
 import org.briarproject.mailbox.core.db.Database
 import org.briarproject.mailbox.core.server.MailboxPrincipal.ContactPrincipal
 import org.briarproject.mailbox.core.server.MailboxPrincipal.OwnerPrincipal
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
index 8cecb21ae2b1fa458d5147ed13a6c21dc028bf77..60cd89b501261cbb3135999727565ba09ae18bc5 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
@@ -3,6 +3,8 @@ package org.briarproject.mailbox.core.server
 import io.ktor.application.install
 import io.ktor.auth.Authentication
 import io.ktor.features.CallLogging
+import io.ktor.features.ContentNegotiation
+import io.ktor.jackson.jackson
 import io.ktor.server.engine.embeddedServer
 import io.ktor.server.netty.Netty
 import org.briarproject.mailbox.core.files.FileManager
@@ -41,6 +43,9 @@ internal class WebServerManagerImpl @Inject constructor(
                     }
                 }
             }
+            install(ContentNegotiation) {
+                jackson()
+            }
             configureBasicApi()
             configureContactApi()
             configureFilesApi(fileManager)
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/RandomIdManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/RandomIdManager.kt
index e7379b9903d5ebece7825fad8523a8770e57aec3..66c89d17ca66c261845310241a16d4034abab19c 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/RandomIdManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/system/RandomIdManager.kt
@@ -3,7 +3,7 @@ package org.briarproject.mailbox.core.system
 import java.security.SecureRandom
 import javax.inject.Inject
 
-private const val ID_SIZE = 32
+internal const val ID_SIZE = 32
 private const val ID_HEX_SIZE = ID_SIZE * 2
 
 /**
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestUtils.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestUtils.kt
index 2025f841c43d7de9eae5293f0adc759d9616710f..9ec2c1b1be157711e7f86ff4316e9fb913692c17 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestUtils.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestUtils.kt
@@ -1,7 +1,15 @@
 package org.briarproject.mailbox.core
 
+import io.mockk.every
+import io.mockk.mockk
+import org.briarproject.mailbox.core.contacts.Contact
+import org.briarproject.mailbox.core.db.Database
+import org.briarproject.mailbox.core.db.Transaction
+import org.briarproject.mailbox.core.system.ID_SIZE
+import org.briarproject.mailbox.core.system.toHex
 import org.briarproject.mailbox.core.util.IoUtils
 import java.io.File
+import kotlin.random.Random
 
 object TestUtils {
 
@@ -10,4 +18,26 @@ object TestUtils {
         testDir.parentFile.delete() // Delete if empty
     }
 
+    fun getNewRandomId(): String = Random.nextBytes(ID_SIZE).toHex()
+
+    fun getNewRandomContact(id: Int = Random.nextInt(1, Int.MAX_VALUE)) = Contact(
+        contactId = id,
+        token = getNewRandomId(),
+        inboxId = getNewRandomId(),
+        outboxId = getNewRandomId(),
+    )
+
+    /**
+     * Allows you to mock [Database] access happening within a [Transaction] more comfortably.
+     * Calls to [Database.transactionWithResult] will be mocked.
+     * The given lambda [block] will get captured and invoked.
+     */
+    fun <T> everyTransactionWithResult(db: Database, readOnly: Boolean, block: (Transaction) -> T) {
+        val txn = Transaction(mockk(), readOnly)
+        every { db.transactionWithResult<T>(true, captureLambda()) } answers {
+            lambda<(Transaction) -> T>().captured.invoke(txn)
+        }
+        block(txn)
+    }
+
 }
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt
index ac4d1a7909a2ab29b4725e08b26121b10e32de6a..67b41d18f3238abd45fa8208c10e17615b04e090 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/JdbcDatabaseTest.kt
@@ -1,7 +1,7 @@
 package org.briarproject.mailbox.core.db
 
 import org.briarproject.mailbox.core.TestUtils.deleteTestDirectory
-import org.briarproject.mailbox.core.api.Contact
+import org.briarproject.mailbox.core.contacts.Contact
 import org.briarproject.mailbox.core.settings.Settings
 import org.briarproject.mailbox.core.system.Clock
 import org.briarproject.mailbox.core.system.RandomIdManager
@@ -40,13 +40,13 @@ abstract class JdbcDatabaseTest {
     open fun testPersistence() {
         // Store some records
         val contact1 = Contact(
-            id = 1,
+            contactId = 1,
             token = randomIdManager.getNewRandomId(),
             inboxId = randomIdManager.getNewRandomId(),
             outboxId = randomIdManager.getNewRandomId()
         )
         val contact2 = Contact(
-            id = 2,
+            contactId = 2,
             token = randomIdManager.getNewRandomId(),
             inboxId = randomIdManager.getNewRandomId(),
             outboxId = randomIdManager.getNewRandomId()
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1b2e339914e5008210bc2a507d9271fe74bcf92b
--- /dev/null
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt
@@ -0,0 +1,84 @@
+package org.briarproject.mailbox.core.server
+
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.cio.CIO
+import io.ktor.client.features.json.JacksonSerializer
+import io.ktor.client.features.json.JsonFeature
+import io.ktor.client.request.HttpRequestBuilder
+import io.ktor.client.request.headers
+import io.ktor.http.HttpHeaders
+import org.briarproject.mailbox.core.DaggerTestComponent
+import org.briarproject.mailbox.core.TestComponent
+import org.briarproject.mailbox.core.TestModule
+import org.briarproject.mailbox.core.TestUtils.getNewRandomContact
+import org.briarproject.mailbox.core.TestUtils.getNewRandomId
+import org.briarproject.mailbox.core.contacts.Contact
+import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT
+import org.briarproject.mailbox.core.settings.Settings
+import org.junit.jupiter.api.AfterAll
+import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.TestInstance
+import org.junit.jupiter.api.TestInstance.Lifecycle
+import org.junit.jupiter.api.io.TempDir
+import java.io.File
+
+@TestInstance(Lifecycle.PER_CLASS)
+abstract class IntegrationTest {
+
+    protected lateinit var testComponent: TestComponent
+    private val lifecycleManager by lazy { testComponent.getLifecycleManager() }
+    protected val httpClient = HttpClient(CIO) {
+        expectSuccess = false // prevents exceptions on non-success responses
+        install(JsonFeature) {
+            serializer = JacksonSerializer()
+        }
+    }
+    protected val baseUrl = "http://127.0.0.1:$PORT"
+
+    protected val ownerToken = getNewRandomId()
+    protected val token = getNewRandomId()
+    protected val id = getNewRandomId()
+    protected val contact1 = getNewRandomContact()
+    protected val contact2 = getNewRandomContact()
+
+    @BeforeAll
+    fun setUp(@TempDir tempDir: File) {
+        testComponent = DaggerTestComponent.builder().testModule(TestModule(tempDir)).build()
+        testComponent.injectCoreEagerSingletons()
+        lifecycleManager.startServices()
+        lifecycleManager.waitForStartup()
+        initDb()
+    }
+
+    open fun initDb() {
+        // sub-classes can initialize the DB here as needed
+    }
+
+    @AfterAll
+    fun tearDown() {
+        lifecycleManager.stopServices()
+        lifecycleManager.waitForShutdown()
+    }
+
+    protected fun addOwnerToken() {
+        val settingsManager = testComponent.getSettingsManager()
+        val settings = Settings()
+        settings[SETTINGS_OWNER_TOKEN] = ownerToken
+        settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE_OWNER)
+    }
+
+    protected fun addContact(c: Contact) {
+        val db = testComponent.getDatabase()
+        db.transaction(false) { txn ->
+            db.addContact(txn, c)
+        }
+    }
+
+    protected fun HttpRequestBuilder.authenticateWithToken(t: String) {
+        headers {
+            @Suppress("EXPERIMENTAL_API_USAGE_FUTURE_ERROR")
+            append(HttpHeaders.Authorization, "Bearer $t")
+        }
+    }
+
+}
diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/WebServerIntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/WebServerIntegrationTest.kt
index 753642437efe430a862737084eff97296ec2cf33..95dc851d1fea703529fdeb4ad69687a9a7dc936d 100644
--- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/WebServerIntegrationTest.kt
+++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/WebServerIntegrationTest.kt
@@ -1,47 +1,13 @@
 package org.briarproject.mailbox.core.server
 
-import io.ktor.client.HttpClient
-import io.ktor.client.engine.cio.CIO
 import io.ktor.client.request.get
 import io.ktor.client.statement.HttpResponse
 import io.ktor.client.statement.readText
 import kotlinx.coroutines.runBlocking
-import org.briarproject.mailbox.core.DaggerTestComponent
-import org.briarproject.mailbox.core.TestComponent
-import org.briarproject.mailbox.core.TestModule
-import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT
-import org.junit.jupiter.api.AfterAll
-import org.junit.jupiter.api.BeforeAll
 import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.TestInstance
-import org.junit.jupiter.api.TestInstance.Lifecycle
-import org.junit.jupiter.api.io.TempDir
-import java.io.File
 import kotlin.test.assertEquals
 
-@TestInstance(Lifecycle.PER_CLASS)
-class WebServerIntegrationTest {
-
-    private lateinit var testComponent: TestComponent
-    private val lifecycleManager by lazy { testComponent.getLifecycleManager() }
-    private val httpClient = HttpClient(CIO) {
-        expectSuccess = false // prevents exceptions on non-success responses
-    }
-    private val baseUrl = "http://127.0.0.1:$PORT"
-
-    @BeforeAll
-    fun setUp(@TempDir tempDir: File) {
-        testComponent = DaggerTestComponent.builder().testModule(TestModule(tempDir)).build()
-        testComponent.injectCoreEagerSingletons()
-        lifecycleManager.startServices()
-        lifecycleManager.waitForStartup()
-    }
-
-    @AfterAll
-    fun tearDown() {
-        lifecycleManager.stopServices()
-        lifecycleManager.waitForShutdown()
-    }
+class WebServerIntegrationTest : IntegrationTest() {
 
     @Test
     fun routeRespondsWithHelloWorldString(): Unit = runBlocking {