diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/model/i11_portal/api/HiwiTrackerMonthData.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/model/i11_portal/api/HiwiTrackerMonthData.kt
index ea07693f48ce06de25e68409a18e9858f2a58478..58be99f037d0a212de795d3cb661178347fc91ff 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/model/i11_portal/api/HiwiTrackerMonthData.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/model/i11_portal/api/HiwiTrackerMonthData.kt
@@ -22,13 +22,13 @@ import kotlin.time.times
 
 @Serializable
 data class HiwiTrackerMonthData(
-    override val session: I11PortalData.Session,
+    override val session: PortalAPIResponse.Session,
     override val infos: List<String>,
     override val warnings: List<String>,
     override val errors: List<String>,
     override val navigation: JsonElement,
     @SerialName("ajaxui")
-    override val ajaxUI: I11PortalData.AjaxUI? = null,
+    override val ajaxUI: PortalAPIResponse.AjaxUI? = null,
     val calendar: Map<ISODate, CalendarEntry>,
     @SerialName("monthname")
     val monthName: String,
@@ -51,7 +51,7 @@ data class HiwiTrackerMonthData(
     val reset: Boolean,
     @SerialName("hidedatecaption")
     val hideDateCaption: Boolean
-) : I11PortalData {
+) : PortalAPIResponse {
     @Serializable
     data class CalendarEntry(
         val disabled: Boolean,
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/model/i11_portal/api/I11PortalData.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/model/i11_portal/api/I11PortalData.kt
deleted file mode 100644
index c68d11c26466d8ff371a0327d9319516ed69f7c9..0000000000000000000000000000000000000000
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/model/i11_portal/api/I11PortalData.kt
+++ /dev/null
@@ -1,353 +0,0 @@
-package net.novagamestudios.kaffeekasse.model.i11_portal.api
-
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.JsonElement
-
-interface I11PortalData {
-    val session: Session
-    val infos: List<String>
-    val warnings: List<String>
-    val errors: List<String>
-    val navigation: JsonElement
-    @SerialName("ajaxui")
-    val ajaxUI: AjaxUI?
-    @Serializable
-    data class Session(
-        val login: Boolean,
-        val user: String? = null,
-        @SerialName("displayname")
-        val displayName: String? = null
-    )
-    @Serializable
-    data class AjaxUI(
-        val reload: Boolean?
-    )
-}
-
-
-/*
-{
-    "session": {
-        "login": true,
-        "user": "broeckmann",
-        "displayname": "Jonas Broeckmann"
-    },
-    "calendar": {
-        "2024-02-26": {
-            "disabled": true,
-            "day": 26,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-02-27": {
-            "disabled": true,
-            "day": 27,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-02-28": {
-            "disabled": true,
-            "day": 28,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-02-29": {
-            "disabled": true,
-            "day": 29,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-01": {
-            "disabled": false,
-            "day": 1,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-02": {
-            "disabled": false,
-            "day": 2,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-03": {
-            "disabled": false,
-            "day": 3,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-04": {
-            "disabled": false,
-            "day": 4,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-05": {
-            "disabled": false,
-            "day": 5,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-06": {
-            "disabled": false,
-            "day": 6,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-07": {
-            "disabled": false,
-            "day": 7,
-            "hasentry": true,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-08": {
-            "disabled": false,
-            "day": 8,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-09": {
-            "disabled": false,
-            "day": 9,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-10": {
-            "disabled": false,
-            "day": 10,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-11": {
-            "disabled": false,
-            "day": 11,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-12": {
-            "disabled": false,
-            "day": 12,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-13": {
-            "disabled": false,
-            "day": 13,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-14": {
-            "disabled": false,
-            "day": 14,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-15": {
-            "disabled": false,
-            "day": 15,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-16": {
-            "disabled": false,
-            "day": 16,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-17": {
-            "disabled": false,
-            "day": 17,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-18": {
-            "disabled": false,
-            "day": 18,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-19": {
-            "disabled": false,
-            "day": 19,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-20": {
-            "disabled": false,
-            "day": 20,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-21": {
-            "disabled": false,
-            "day": 21,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-22": {
-            "disabled": false,
-            "day": 22,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-23": {
-            "disabled": false,
-            "day": 23,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-24": {
-            "disabled": false,
-            "day": 24,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-25": {
-            "disabled": false,
-            "day": 25,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-26": {
-            "disabled": false,
-            "day": 26,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-27": {
-            "disabled": false,
-            "day": 27,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-28": {
-            "disabled": false,
-            "day": 28,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-29": {
-            "disabled": false,
-            "day": 29,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": "Karfreitag"
-        },
-        "2024-03-30": {
-            "disabled": false,
-            "day": 30,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        },
-        "2024-03-31": {
-            "disabled": false,
-            "day": 31,
-            "hasentry": false,
-            "vacation": false,
-            "holiday": false
-        }
-    },
-    "monthname": "März",
-    "today": "2024-03-08",
-    "nextmonthdate": "2024-04-01",
-    "prevmonthdate": "2024-02-01",
-    "selecteddate": null,
-    "entries": [
-        {
-            "timetableentry_id": 71889,
-            "timetable_id": 463,
-            "date": "2024-03-07",
-            "begin": "10:00:00",
-            "end": "17:00:00",
-            "breaktime": "00:30:00",
-            "note": "psp_course_materials",
-            "hoursperday": "01:54:00.0000",
-            "name": "Broeckmann, Jonas",
-            "hrsweek": "09:30:00",
-            "hours_worked": "06:30:00.000000"
-        }
-    ],
-    "total_month": {
-        "timetable_id": 463,
-        "name": "Broeckmann, Jonas",
-        "date": "2024-03-01",
-        "hoursperweek": "09:30:00",
-        "hoursperday": "01:54:00.0000",
-        "workdays": 20,
-        "seconds_worked": 23400,
-        "holidays": 1,
-        "vacation_days": null,
-        "hours_worked": "06:30:00",
-        "hoursthismonth": "38:00:00.000000",
-        "hours_balance": "-31:30:00.000000",
-        "hours_balance_percent": -0.828947368,
-        "hours_balance_total_percent": -0.83
-    },
-    "total": {
-        "timetable_id": 463,
-        "hoursperweek": "08:30:00",
-        "hoursperday": "01:42:00.0000",
-        "workdays": 692,
-        "seconds_worked": 4492800,
-        "holidays": 26,
-        "last_reset": "03/2021",
-        "vacation_days": 43,
-        "date": "2024-02-29",
-        "hours_worked": "838:59:59",
-        "hours_balance": "04:48:00",
-        "hours_balance_percent": 1.403921568
-    },
-    "last_reset": {
-        "date": "2021-04-01"
-    },
-    "overtime_expire": "00:00:00",
-    "holiday": false,
-    "reset": false,
-    "hidedatecaption": false,
-    "infos": [],
-    "warnings": [],
-    "errors": [],
-    "navigation": {
-        "Urlaub eintragen": {
-            "type": "link",
-            "action": "?vacations",
-            "subitems": []
-        }
-    }
-}
- */
-
-
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/model/i11_portal/api/PortalAPIResponse.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/model/i11_portal/api/PortalAPIResponse.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d701f992dd9ac0a88578f0aef86e789e1427bc25
--- /dev/null
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/model/i11_portal/api/PortalAPIResponse.kt
@@ -0,0 +1,41 @@
+package net.novagamestudios.kaffeekasse.model.i11_portal.api
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonNull
+import net.novagamestudios.kaffeekasse.model.i11_portal.api.PortalAPIResponse.*
+
+interface PortalAPIResponse {
+    @SerialName("session")
+    val session: Session
+    val infos: List<String>
+    val warnings: List<String>
+    val errors: List<String>
+    val navigation: JsonElement
+    @SerialName("ajaxui")
+    val ajaxUI: AjaxUI?
+    @Serializable
+    data class Session(
+        @SerialName("login")
+        val login: Boolean = false,
+        @SerialName("user")
+        val user: String? = null,
+        @SerialName("displayname")
+        val displayName: String? = null
+    )
+    @Serializable
+    data class AjaxUI(
+        val reload: Boolean?
+    )
+}
+
+abstract class PortalAPIResponseBase : PortalAPIResponse {
+    override val session: Session = Session()
+    override val infos: List<String> = emptyList()
+    override val warnings: List<String> = emptyList()
+    override val errors: List<String> = emptyList()
+    override val navigation: JsonElement = JsonNull
+    override val ajaxUI: AjaxUI? = null
+}
+
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/DeviceAuthResponse.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/DeviceAuthResponse.kt
new file mode 100644
index 0000000000000000000000000000000000000000..43faadc32e8fea8d15f109ca96d5b493cf56f64b
--- /dev/null
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/DeviceAuthResponse.kt
@@ -0,0 +1,18 @@
+package net.novagamestudios.kaffeekasse.model.kaffeekasse.api
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class DeviceAuthResponse(
+    val challenge: String? = null,
+    val name: String? = null,
+    @SerialName("item_type_id")
+    val itemTypeId: Int? = null
+)
+
+
+
+
+
+
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/InfoResponse.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/InfoResponse.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d580a67213d54090985e74b803e2b4bd2af59dde
--- /dev/null
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/InfoResponse.kt
@@ -0,0 +1,66 @@
+package net.novagamestudios.kaffeekasse.model.kaffeekasse.api
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+
+@Serializable
+data class UserListResponse(
+    @SerialName("user_list")
+    val userList: List<User>
+)
+
+@Serializable
+data class UserInfoResponse(
+    @SerialName("user_id")
+    val userId: Int,
+    val name: String,
+    val balance: Double,
+    val favorites: JsonArray = JsonArray(emptyList()),
+    val blacklist: List<User>? = null,
+    val whitelist: List<User>? = null
+)
+
+@Serializable
+data class User(
+    @SerialName("user_id")
+    val userId: Int,
+    val name: String,
+    @SerialName("empty_pin")
+    val noPinSet: Boolean? = null
+) {
+    val firstName by lazy { name.split(", ")[1] }
+    val lastName by lazy { name.split(", ")[0] }
+}
+
+@Serializable
+data class ItemListResponse(
+    @SerialName("item_list")
+    val itemList: List<Item>
+) {
+    @Serializable
+    data class Item(
+        @SerialName("item_id")
+        val itemId: Int,
+        val name: String,
+        val gtin: String? = null,
+        val price: Double,
+        @SerialName("image_url")
+        val imageUrl: String? = null,
+        val sort: Int,
+        @SerialName("sort_p")
+        val sortP: Int,
+        @SerialName("itemtype_id")
+        val itemTypeId: ItemTypeId,
+        val enabled: Boolean,
+        @SerialName("has_condition_reports")
+        val hasConditionReports: JsonElement
+    )
+}
+
+@Serializable
+@JvmInline
+value class ItemTypeId(val id: Int)
+
+
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/KaffeekasseAPI.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/KaffeekasseAPI.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f4606e0324f2e5aa8b016b02c17be445a13be15e
--- /dev/null
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/KaffeekasseAPI.kt
@@ -0,0 +1,121 @@
+package net.novagamestudios.kaffeekasse.model.kaffeekasse.api
+
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.request.forms.FormDataContent
+import io.ktor.client.request.post
+import io.ktor.client.request.setBody
+import io.ktor.client.statement.HttpResponse
+import io.ktor.http.Parameters
+import io.ktor.http.URLBuilder
+import io.ktor.http.Url
+import io.ktor.util.sha1
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.decodeFromJsonElement
+import net.novagamestudios.kaffeekasse.model.kaffeekasse.api.KaffeekasseAPIResponse.Error.Code as ErrorCode
+
+class KaffeekasseAPI(
+    private val apiUrl: Url,
+    private val client: HttpClient
+) {
+    private val jsonFormat = Json {
+        ignoreUnknownKeys = true
+        isLenient = true
+    }
+    private inline fun <reified T> JsonElement.decode() = jsonFormat.decodeFromJsonElement<T>(this)
+
+    private suspend fun invokeFunction(name: String, arguments: List<Pair<String, String>>): HttpResponse {
+        val functionUrl = URLBuilder(apiUrl).apply {
+            parameters.append(name, "")
+        }.build()
+        return client.post(functionUrl) {
+            setBody(FormDataContent(Parameters.build { arguments.forEach { (key, value) -> append(key, value) } }))
+        }
+    }
+
+    private suspend inline fun <reified R> HttpResponse.parseResult(
+        validateLoggedIn: Boolean = true
+    ): Result<R> {
+        val raw = body<JsonElement>()
+        val response = raw.decode<KaffeekasseAPIResponse>()
+        return when {
+            response.hasError -> Result.Error(response)
+            validateLoggedIn && !response.session.login -> Result.NotLoggedIn(response)
+            else -> Result.Success(response, raw.decode<R>())
+        }
+    }
+
+
+    suspend fun userList() = invokeFunction(
+        "user_list",
+        emptyList()
+    ).parseResult<UserListResponse>()
+
+    suspend fun userInfo(userId: Int) = invokeFunction(
+        "user_info",
+        listOf("user_id" to "$userId")
+    ).parseResult<UserInfoResponse>()
+
+    suspend fun itemList(itemTypeId: ItemTypeId? = null) = invokeFunction(
+        "item_list",
+        listOfNotNull(itemTypeId?.let { "item_type_id" to "${it.id}" })
+    ).parseResult<ItemListResponse>()
+
+
+
+
+    private suspend fun loginDevice(deviceId: String, challengeResponse: String? = null) = invokeFunction(
+        "login_device",
+        listOfNotNull(
+            "id" to deviceId,
+            challengeResponse?.let { "response" to it }
+        )
+    ).parseResult<DeviceAuthResponse>(validateLoggedIn = false)
+
+    suspend fun performLogin(deviceId: String, apiKey: String): LoginResult {
+        // Phase 1
+        val challenge = when (val response = loginDevice(deviceId)) {
+            is Result.Error -> when (response.errorCode) {
+                ErrorCode.InvalidFieldValue -> return LoginResult.Failure.InvalidDeviceId
+                ErrorCode.AccessDenied -> return LoginResult.Failure.AccessDenied
+                else -> return LoginResult.Failure.UnknownError(response.response)
+            }
+            is Result.NotLoggedIn -> throw AssertionError("Not logged in")
+            is Result.Success -> response.result.challenge
+                ?: return LoginResult.Failure.UnknownError(response.response)
+        }
+
+        // Phase 2 (useless, but yolo)
+        val hash = sha1((challenge + apiKey).encodeToByteArray()).toString()
+        return when (val response = loginDevice(deviceId, hash)) {
+            is Result.Error -> when (response.errorCode) {
+                ErrorCode.AuthenticationFailure -> LoginResult.Failure.AuthenticationFailure
+                else -> LoginResult.Failure.UnknownError(response.response)
+            }
+
+            is Result.NotLoggedIn -> throw AssertionError("Not logged in")
+            is Result.Success -> LoginResult.LoggedIn(response.result)
+        }
+    }
+
+    sealed interface LoginResult {
+        sealed interface Failure : LoginResult {
+            data object InvalidDeviceId : LoginResult
+            data object AccessDenied : LoginResult
+            data object AuthenticationFailure : LoginResult
+            class UnknownError(val response: KaffeekasseAPIResponse) : LoginResult
+        }
+        class LoggedIn(val response: DeviceAuthResponse) : LoginResult
+    }
+
+    sealed class Result<T>(
+        val response: KaffeekasseAPIResponse
+    ) {
+        val errorCode: ErrorCode? get() = response.error?.code
+        class Error<T>(response: KaffeekasseAPIResponse) : Result<T>(response)
+        class NotLoggedIn<T>(response: KaffeekasseAPIResponse) : Result<T>(response)
+        class Success<T>(response: KaffeekasseAPIResponse, val result: T) : Result<T>(response)
+    }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/KaffeekasseAPIResponse.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/KaffeekasseAPIResponse.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fa49d60dfbf84ae1ee149cf7a183cbc070af609d
--- /dev/null
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/KaffeekasseAPIResponse.kt
@@ -0,0 +1,42 @@
+package net.novagamestudios.kaffeekasse.model.kaffeekasse.api
+
+import kotlinx.serialization.Serializable
+import net.novagamestudios.kaffeekasse.model.i11_portal.api.PortalAPIResponseBase
+
+
+@Serializable
+data class KaffeekasseAPIResponse(
+    val error: Error? = null
+) : PortalAPIResponseBase() {
+    @Serializable
+    data class Error(
+        val code: Code,
+        val string: String
+    ) {
+        @Serializable
+        @JvmInline
+        value class Code(val value: Long) {
+            companion object {
+                val NoError = Code(0)
+                val Unknown = Code(1)
+                val AccessDenied = Code(3)
+                val UnsupportedAuthMethod = Code(4)
+                val AuthenticationFailure = Code(5)
+                val MethodDisabled = Code(6)
+                val AuthenticationRequired = Code(7)
+                val InvalidFieldValue = Code(207)
+                val MissingFieldValue = Code(208)
+                val NotFound = Code(209)
+                val InvalidAction = Code(210)
+                val NotImplemented = Code(0xDEADBEEF)
+                val NoCodeYet = Code(0xEA7DEADBEEF)
+            }
+        }
+        val isError get() = code != Code.NoError
+    }
+    val hasError get() = error?.takeIf { it.isError } != null || errors.isNotEmpty()
+}
+
+
+
+
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/model/i11_portal/api/KaffeekasseData.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/KaffeekasseData.kt
similarity index 85%
rename from app/src/main/java/net/novagamestudios/kaffeekasse/model/i11_portal/api/KaffeekasseData.kt
rename to app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/KaffeekasseData.kt
index 34a8dc79fcc43a90d66dcae69e517f9d133ed417..72f50268eb0447dd96c7f91c8d8f74bacb4fe9a8 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/model/i11_portal/api/KaffeekasseData.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/model/kaffeekasse/api/KaffeekasseData.kt
@@ -1,8 +1,9 @@
-package net.novagamestudios.kaffeekasse.model.i11_portal.api
+package net.novagamestudios.kaffeekasse.model.kaffeekasse.api
 
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.json.JsonElement
+import net.novagamestudios.kaffeekasse.model.i11_portal.api.PortalAPIResponse
 
 /*
 {"session":{"login":false},"error":{"code":0,"string":"No error"},"infos":[],"warnings":[],"errors":["Login fehlgeschlagen."],"navigation":[]}
@@ -22,22 +23,16 @@ import kotlinx.serialization.json.JsonElement
 */
 @Serializable
 data class KaffeekasseData(
-    override val session: I11PortalData.Session,
+    override val session: PortalAPIResponse.Session,
     override val infos: List<String>,
     override val warnings: List<String>,
     override val errors: List<String>,
     override val navigation: JsonElement,
-    @SerialName("ajaxui")
-    override val ajaxUI: I11PortalData.AjaxUI? = null,
-    val error: Error? = null,
+    override val ajaxUI: PortalAPIResponse.AjaxUI? = null,
+    override val error: KaffeekasseAPIResponse.Error? = null,
     @SerialName("Kontostand")
     val balance: Balance? = null
-) : I11PortalData {
-    @Serializable
-    data class Error(
-        val code: Int,
-        val string: String
-    )
+) : KaffeekasseAPIResponse {
     @Serializable
     data class Balance(
         @SerialName("mybalance")
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/I11Client.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/I11Client.kt
index b4d8f26271c50d27cf380e314d3afb0d0ec1ba43..ad0b2c20ee5c03968bce2d716f1724bebcd3cce5 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/I11Client.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/repositories/I11Client.kt
@@ -60,9 +60,9 @@ import net.novagamestudios.common_utils.debug
 import net.novagamestudios.common_utils.info
 import net.novagamestudios.common_utils.warn
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.Cart
-import net.novagamestudios.kaffeekasse.model.i11_portal.api.I11PortalData
+import net.novagamestudios.kaffeekasse.model.i11_portal.api.PortalAPIResponse
 import net.novagamestudios.kaffeekasse.model.i11_portal.api.HiwiTrackerMonthData
-import net.novagamestudios.kaffeekasse.model.i11_portal.api.KaffeekasseData
+import net.novagamestudios.kaffeekasse.model.kaffeekasse.api.KaffeekasseData
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.KnownItem
 import net.novagamestudios.kaffeekasse.model.i11_portal.Login
 import net.novagamestudios.kaffeekasse.model.hiwi_tracker.MonthKey
@@ -76,7 +76,6 @@ import net.novagamestudios.kaffeekasse.model.kaffeekasse.ManualBillDetails.ItemG
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.Transaction
 import net.novagamestudios.kaffeekasse.model.kaffeekasse.isEmpty
 import net.novagamestudios.kaffeekasse.model.i11_portal.isValid
-import net.novagamestudios.kaffeekasse.ui.hiwi_tracker.formatHiwiTracker
 import net.novagamestudios.kaffeekasse.util.RichData
 import net.novagamestudios.kaffeekasse.util.RichDataState
 import java.time.LocalDateTime
@@ -180,7 +179,7 @@ class I11Client(
     }
 
 
-    abstract inner class Module<T : I11PortalData>(
+    abstract inner class Module<T : PortalAPIResponse>(
         val baseUrl: Url
     ) {
         val isLoggedIn get() = this@I11Client.isLoggedIn
@@ -212,21 +211,21 @@ class I11Client(
         }
     }
 
-    inner class Portal : Module<I11PortalData>(Url("https://portal.embedded.rwth-aachen.de")) {
-        override suspend fun HttpResponse.toData(): I11PortalData {
+    inner class Portal : Module<PortalAPIResponse>(Url("https://portal.embedded.rwth-aachen.de")) {
+        override suspend fun HttpResponse.toData(): PortalAPIResponse {
             return body<I11PortalDataImpl>()
         }
     }
     @Serializable
     private data class I11PortalDataImpl(
-        override val session: I11PortalData.Session,
+        override val session: PortalAPIResponse.Session,
         override val infos: List<String>,
         override val warnings: List<String>,
         override val errors: List<String>,
         override val navigation: JsonElement,
         @SerialName("ajaxui")
-        override val ajaxUI: I11PortalData.AjaxUI? = null
-    ) : I11PortalData
+        override val ajaxUI: PortalAPIResponse.AjaxUI? = null
+    ) : PortalAPIResponse
 
     inner class Kaffeekasse : Module<KaffeekasseData>(Url("https://kaffeekasse.embedded.rwth-aachen.de")) {
         val manuaBillUrl = URLBuilder(baseUrl).apply {
diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/Account.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/Account.kt
index 382f50d31df46bce40df0813189f8942f87e8ed7..80bff50e0ec66deef67903987de50ecc3440faeb 100644
--- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/Account.kt
+++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/Account.kt
@@ -38,7 +38,7 @@ import net.novagamestudios.common_utils.compose.state.collectAsStateIn
 import net.novagamestudios.common_utils.format
 import net.novagamestudios.common_utils.warn
 import net.novagamestudios.kaffeekasse.App
-import net.novagamestudios.kaffeekasse.model.i11_portal.api.KaffeekasseData
+import net.novagamestudios.kaffeekasse.model.kaffeekasse.api.KaffeekasseData
 import net.novagamestudios.kaffeekasse.repositories.I11Client
 import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.FailureRetryScreen
 import net.novagamestudios.kaffeekasse.ui.util.PullToRefreshBox