diff --git a/cmake/Hunter/config.cmake b/cmake/Hunter/config.cmake
index 7c53e0ea57096a3372bf3779dfdf86e8958e114e..4cdeee97ba09d684988a241aa5af943d5793550d 100644
--- a/cmake/Hunter/config.cmake
+++ b/cmake/Hunter/config.cmake
@@ -3,3 +3,7 @@ hunter_config(
     VERSION  "1.70.0-p1"
     CMAKE_ARGS IOSTREAMS_NO_BZIP2=1
 )
+hunter_config(
+    nlohmann_json
+    CMAKE_ARGS JSON_MultipleHeaders=ON
+)
diff --git a/examples/crypto_bot.cpp b/examples/crypto_bot.cpp
index eb4f2736ed5be28d6bf51d2e834778c4d709f19f..5c4b94247deee60f3780a5ba9dda0b4bfb3847eb 100644
--- a/examples/crypto_bot.cpp
+++ b/examples/crypto_bot.cpp
@@ -677,7 +677,7 @@ parse_messages(const mtx::responses::Sync &res)
                 auto room_id = room.first;
 
                 console->info("joining room {}", room_id);
-                client->join_room(room_id, [room_id](const nlohmann::json &, RequestErr e) {
+                client->join_room(room_id, [room_id](const mtx::responses::RoomId &, RequestErr e) {
                         if (e) {
                                 print_errors(e);
                                 console->error("failed to join room {}", room_id);
@@ -911,10 +911,10 @@ handle_to_device_msgs(const mtx::responses::ToDevice &msgs)
                 console->info("inspecting {} to_device messages", msgs.events.size());
 
         for (const auto &msg : msgs.events) {
-                console->info(std::visit(mtx::events::DeviceEventVisitor{}, msg).dump(2));
+                console->info(std::visit([](const auto &e) { return json(e); }, msg).dump(2));
 
                 try {
-                        OlmMessage olm_msg = std::visit(DeviceEventVisitor{}, msg);
+                        OlmMessage olm_msg = std::visit([](const auto &e) { return json(e); }, msg);
                         decrypt_olm_message(std::move(olm_msg));
                 } catch (const nlohmann::json::exception &e) {
                         console->warn("parsing error for olm message: {}", e.what());
@@ -961,15 +961,13 @@ login_cb(const mtx::responses::Login &, RequestErr err)
 }
 
 void
-join_room_cb(const nlohmann::json &obj, RequestErr err)
+join_room_cb(const mtx::responses::RoomId &, RequestErr err)
 {
         if (err) {
                 print_errors(err);
                 return;
         }
 
-        (void)obj;
-
         // Fetch device list for all users.
 }
 
diff --git a/examples/media_downloader.cpp b/examples/media_downloader.cpp
index 17b354a301cbd319c9b6ef49cfd3e56b869c38e2..42ddc2f2559be79f4b98536597e0df4c02b07f1f 100644
--- a/examples/media_downloader.cpp
+++ b/examples/media_downloader.cpp
@@ -150,7 +150,7 @@ message_handler(const mtx::responses::Messages &res, RequestErr err)
                 return;
         }
 
-        for (const auto& msg : res.chunk)
+        for (const auto &msg : res.chunk)
                 print_message(msg);
 
         if (res.chunk.empty()) {
diff --git a/examples/room_feed.cpp b/examples/room_feed.cpp
index 4fc826756024b9d639de724c16f87026d584ec5b..a81c03e2595f0d3de1c0ffd52b468b501c3a2ab2 100644
--- a/examples/room_feed.cpp
+++ b/examples/room_feed.cpp
@@ -96,8 +96,8 @@ sync_handler(const mtx::responses::Sync &res, RequestErr err)
                 return;
         }
 
-        for (const auto& room : res.rooms.join) {
-                for (const auto& msg : room.second.timeline.events)
+        for (const auto &room : res.rooms.join) {
+                for (const auto &msg : room.second.timeline.events)
                         print_message(msg);
         }
 
diff --git a/examples/simple_bot.cpp b/examples/simple_bot.cpp
index 5ed3f839d58e1c54b5048236a5c70e40d467b50e..0e2a47490146d03aafc9dd78a7ad66a36b8cb162 100644
--- a/examples/simple_bot.cpp
+++ b/examples/simple_bot.cpp
@@ -83,38 +83,41 @@ get_sender(const TimelineEvent &event)
 void
 parse_messages(const mtx::responses::Sync &res, bool parse_repeat_cmd = false)
 {
-        for (const auto& room : res.rooms.invite) {
+        for (const auto &room : res.rooms.invite) {
                 auto room_id = room.first;
 
                 printf("joining room %s\n", room_id.c_str());
-                client->join_room(room_id, [room_id](const nlohmann::json &obj, RequestErr e) {
-                        if (e) {
-                                print_errors(e);
-                                printf("failed to join room %s\n", room_id.c_str());
-                                return;
-                        }
-
-                        printf("joined room \n%s\n", obj.dump(2).c_str());
-
-                        mtx::events::msg::Text text;
-                        text.body = "Thanks for the invitation!";
-
-                        client->send_room_message<mtx::events::msg::Text>(
-                          room_id, text, [room_id](const mtx::responses::EventId &, RequestErr e) {
-                                  if (e) {
-                                          print_errors(e);
-                                          return;
-                                  }
-
-                                  printf("sent message to %s\n", room_id.c_str());
-                          });
-                });
+                client->join_room(
+                  room_id, [room_id](const mtx::responses::RoomId &obj, RequestErr e) {
+                          if (e) {
+                                  print_errors(e);
+                                  printf("failed to join room %s\n", room_id.c_str());
+                                  return;
+                          }
+
+                          printf("joined room \n%s\n", obj.room_id.c_str());
+
+                          mtx::events::msg::Text text;
+                          text.body = "Thanks for the invitation!";
+
+                          client->send_room_message<mtx::events::msg::Text>(
+                            room_id,
+                            text,
+                            [room_id](const mtx::responses::EventId &, RequestErr e) {
+                                    if (e) {
+                                            print_errors(e);
+                                            return;
+                                    }
+
+                                    printf("sent message to %s\n", room_id.c_str());
+                            });
+                  });
         }
 
         if (!parse_repeat_cmd)
                 return;
 
-        for (const auto& room : res.rooms.join) {
+        for (const auto &room : res.rooms.join) {
                 const std::string repeat_cmd = "!repeat";
                 const std::string room_id    = room.first;
 
diff --git a/include/mtx/common.hpp b/include/mtx/common.hpp
index 452765427fb35c287377676de7eb1d1589e7eaf8..31ca50934fa0a32fb39250cfec3dab6794537e0f 100644
--- a/include/mtx/common.hpp
+++ b/include/mtx/common.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <map>
 #include <string>
diff --git a/include/mtx/errors.hpp b/include/mtx/errors.hpp
index a4e75d76986421c6e837ea20e7a7a5da70cb5005..1dd8127f2e022031dc095dd23aa82a01c5fb7258 100644
--- a/include/mtx/errors.hpp
+++ b/include/mtx/errors.hpp
@@ -1,62 +1,10 @@
 #pragma once
 
-#include <nlohmann/json.hpp>
-#include <string>
-
+#include "lightweight_error.hpp"
 #include "user_interactive.hpp"
 
 namespace mtx {
 namespace errors {
-
-enum class ErrorCode
-{
-        M_UNRECOGNIZED,
-        //! unknown user or so
-        M_UNKNOWN,
-        //! Forbidden access, e.g. joining a room without permission, failed login.
-        M_FORBIDDEN,
-        //! The access token specified was not recognised.
-        M_UNKNOWN_TOKEN,
-        //! Request contained valid JSON, but it was malformed in some way,
-        //! e.g. missing required keys, invalid values for keys
-        M_BAD_JSON,
-        //! Request did not contain valid JSON.
-        M_NOT_JSON,
-        //! No resource was found for this request.
-        M_NOT_FOUND,
-        //! Too many requests have been sent in a short period of time.
-        M_LIMIT_EXCEEDED,
-        //! Encountered when trying to register a user ID which has been taken.
-        M_USER_IN_USE,
-        //! Encountered when trying to register a user ID which is not valid.
-        M_INVALID_USERNAME,
-        //! Sent when the room alias given to the createRoom API is already in use.
-        M_ROOM_IN_USE,
-        //! Sent when the intial state given to the createRoom API is invalid.
-        M_INVALID_ROOM_STATE,
-        //! Encountered when specifying bad pagination query parameters.
-        M_BAD_PAGINATION,
-        //! Sent when a threepid given to an API cannot be used because
-        //! the same threepid is already in use.
-        M_THREEPID_IN_USE,
-        //! Sent when a threepid given to an API cannot be used
-        //! because no record matching the threepid was found.
-        M_THREEPID_NOT_FOUND,
-        //! The client's request used a third party server,
-        //! eg. ID server, that this server does not trust.
-        M_SERVER_NOT_TRUSTED,
-        //! The access token isn't present in the request.
-        M_MISSING_TOKEN,
-        //! One of the uploaded signatures was invalid
-        M_INVALID_SIGNATURE,
-};
-
-std::string
-to_string(ErrorCode code);
-
-ErrorCode
-from_string(const std::string &code);
-
 //! Represents a Matrix related error.
 struct Error
 {
diff --git a/include/mtx/events.hpp b/include/mtx/events.hpp
index 9160d7e08ed1f6391c550a64077173bd129dfac9..36bb8384ed9c5053f9e4c179cb54df3f84bc5b83 100644
--- a/include/mtx/events.hpp
+++ b/include/mtx/events.hpp
@@ -1,7 +1,12 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
+#include "mtx/events/event_type.hpp"
 #include "mtx/events/messages/image.hpp"
 #include "mtx/events/redaction.hpp"
 #include "mtx/identifiers.hpp"
@@ -9,101 +14,6 @@
 using json = nlohmann::json;
 namespace mtx {
 namespace events {
-
-enum class EventType
-{
-        /// m.key.verification.cancel
-        KeyVerificationCancel,
-        /// m.key.verification.request
-        KeyVerificationRequest,
-        /// m.key.verification.start
-        KeyVerificationStart,
-        /// m.key.verification.accept
-        KeyVerificationAccept,
-        /// m.key.verification.key
-        KeyVerificationKey,
-        /// m.key.verification.mac
-        KeyVerificationMac,
-        /// m.key.verification.ready,
-        KeyVerificationReady,
-        /// m.key.verification.done,
-        KeyVerificationDone,
-        /// m.reaction,
-        Reaction,
-        /// m.room_key
-        RoomKey,
-        /// m.forwarded_room_key
-        ForwardedRoomKey,
-        /// m.room_key_request
-        RoomKeyRequest,
-        /// m.room.aliases
-        RoomAliases,
-        /// m.room.avatar
-        RoomAvatar,
-        /// m.room.canonical_alias
-        RoomCanonicalAlias,
-        /// m.room.create
-        RoomCreate,
-        /// m.room.encrypted.
-        RoomEncrypted,
-        /// m.room.encryption.
-        RoomEncryption,
-        /// m.room.guest_access
-        RoomGuestAccess,
-        /// m.room.history_visibility
-        RoomHistoryVisibility,
-        /// m.room.join_rules
-        RoomJoinRules,
-        /// m.room.member
-        RoomMember,
-        /// m.room.message
-        RoomMessage,
-        /// m.room.name
-        RoomName,
-        /// m.room.power_levels
-        RoomPowerLevels,
-        /// m.room.topic
-        RoomTopic,
-        /// m.room.redaction
-        RoomRedaction,
-        /// m.room.pinned_events
-        RoomPinnedEvents,
-        /// m.room.tombstone
-        RoomTombstone,
-        // m.sticker
-        Sticker,
-        // m.tag
-        Tag,
-        // m.presence
-        Presence,
-        // m.push_rules
-        PushRules,
-        // m.call.invite
-        CallInvite,
-        // m.call.candidates
-        CallCandidates,
-        // m.call.answer
-        CallAnswer,
-        // m.call.hangup
-        CallHangUp,
-
-        // custom events
-        // im.nheko.hidden_events
-        NhekoHiddenEvents,
-
-        // Unsupported event
-        Unsupported,
-};
-
-std::string
-to_string(EventType type);
-
-EventType
-getEventType(const std::string &type);
-
-EventType
-getEventType(const json &obj);
-
 //! The basic set of fields all events must have.
 template<class Content>
 struct Event
@@ -121,21 +31,11 @@ struct Event
 
 template<class Content>
 void
-to_json(json &obj, const Event<Content> &event)
-{
-        obj["content"] = event.content;
-        obj["sender"]  = event.sender;
-        obj["type"]    = ::mtx::events::to_string(event.type);
-}
+to_json(json &obj, const Event<Content> &event);
 
 template<class Content>
 void
-from_json(const json &obj, Event<Content> &event)
-{
-        event.content = obj.at("content").get<Content>();
-        event.type    = getEventType(obj.at("type").get<std::string>());
-        event.sender  = obj.value("sender", "");
-}
+from_json(const json &obj, Event<Content> &event);
 
 //! Extension of the Event type for device events.
 template<class Content>
@@ -146,24 +46,11 @@ struct DeviceEvent : public Event<Content>
 
 template<class Content>
 void
-from_json(const json &obj, DeviceEvent<Content> &event)
-{
-        Event<Content> base_event = event;
-        from_json(obj, base_event);
-        event.content = base_event.content;
-        event.type    = base_event.type;
-        event.sender  = obj.at("sender");
-}
+from_json(const json &obj, DeviceEvent<Content> &event);
 
 template<class Content>
 void
-to_json(json &obj, const DeviceEvent<Content> &event)
-{
-        Event<Content> base_event = event;
-        to_json(obj, base_event);
-
-        obj["sender"] = event.sender;
-}
+to_json(json &obj, const DeviceEvent<Content> &event);
 
 struct UnsignedData
 {
@@ -184,50 +71,11 @@ struct UnsignedData
         std::optional<Event<mtx::events::msg::Redaction>> redacted_because;
 };
 
-inline void
-from_json(const json &obj, UnsignedData &data)
-{
-        if (obj.find("age") != obj.end())
-                data.age = obj.at("age").get<uint64_t>();
-
-        if (obj.find("transaction_id") != obj.end())
-                data.transaction_id = obj.at("transaction_id").get<std::string>();
-
-        if (obj.find("prev_sender") != obj.end())
-                data.prev_sender = obj.at("prev_sender").get<std::string>();
-
-        if (obj.find("replaces_state") != obj.end())
-                data.replaces_state = obj.at("replaces_state").get<std::string>();
-
-        if (obj.find("redacted_by") != obj.end())
-                data.redacted_by = obj.at("redacted_by").get<std::string>();
-
-        if (obj.find("redacted_because") != obj.end())
-                data.redacted_because =
-                  obj.at("redacted_because").get<Event<mtx::events::msg::Redaction>>();
-}
-
-inline void
-to_json(json &obj, const UnsignedData &event)
-{
-        if (!event.prev_sender.empty())
-                obj["prev_sender"] = event.prev_sender;
-
-        if (!event.transaction_id.empty())
-                obj["transaction_id"] = event.transaction_id;
-
-        if (!event.replaces_state.empty())
-                obj["replaces_state"] = event.replaces_state;
-
-        if (event.age != 0)
-                obj["age"] = event.age;
-
-        if (!event.redacted_by.empty())
-                obj["redacted_by"] = event.redacted_by;
+void
+from_json(const json &obj, UnsignedData &data);
 
-        if (event.redacted_because)
-                obj["redacted_because"] = *event.redacted_because;
-}
+void
+to_json(json &obj, const UnsignedData &event);
 
 template<class Content>
 struct StrippedEvent : public Event<Content>
@@ -237,23 +85,11 @@ struct StrippedEvent : public Event<Content>
 
 template<class Content>
 void
-from_json(const json &obj, StrippedEvent<Content> &event)
-{
-        Event<Content> &base = event;
-        from_json(obj, base);
-
-        event.state_key = obj.at("state_key");
-}
+from_json(const json &obj, StrippedEvent<Content> &event);
 
 template<class Content>
 void
-to_json(json &obj, const StrippedEvent<Content> &event)
-{
-        Event<Content> base_event = event;
-        to_json(obj, base_event);
-
-        obj["state_key"] = event.state_key;
-}
+to_json(json &obj, const StrippedEvent<Content> &event);
 
 //! RoomEvent.
 template<class Content>
@@ -273,36 +109,11 @@ struct RoomEvent : public Event<Content>
 
 template<class Content>
 void
-from_json(const json &obj, RoomEvent<Content> &event)
-{
-        Event<Content> &base = event;
-        from_json(obj, base);
-
-        event.event_id         = obj.at("event_id");
-        event.origin_server_ts = obj.at("origin_server_ts");
-
-        // SPEC_BUG: Not present in the state array returned by /sync.
-        if (obj.find("room_id") != obj.end())
-                event.room_id = obj.at("room_id");
-
-        if (obj.find("unsigned") != obj.end())
-                event.unsigned_data = obj.at("unsigned");
-}
+from_json(const json &obj, RoomEvent<Content> &event);
 
 template<class Content>
 void
-to_json(json &obj, const RoomEvent<Content> &event)
-{
-        Event<Content> base_event = event;
-        to_json(obj, base_event);
-
-        if (!event.room_id.empty())
-                obj["room_id"] = event.room_id;
-
-        obj["event_id"]         = event.event_id;
-        obj["unsigned"]         = event.unsigned_data;
-        obj["origin_server_ts"] = event.origin_server_ts;
-}
+to_json(json &obj, const RoomEvent<Content> &event);
 
 //! Extension of the RoomEvent.
 template<class Content>
@@ -315,23 +126,11 @@ struct StateEvent : public RoomEvent<Content>
 
 template<class Content>
 void
-to_json(json &obj, const StateEvent<Content> &event)
-{
-        RoomEvent<Content> base_event = event;
-        to_json(obj, base_event);
-
-        obj["state_key"] = event.state_key;
-}
+to_json(json &obj, const StateEvent<Content> &event);
 
 template<class Content>
 void
-from_json(const json &obj, StateEvent<Content> &event)
-{
-        RoomEvent<Content> &base = event;
-        from_json(obj, base);
-
-        event.state_key = obj.at("state_key").get<std::string>();
-}
+from_json(const json &obj, StateEvent<Content> &event);
 
 //! Extension of the RoomEvent.
 template<class Content>
@@ -343,23 +142,11 @@ struct RedactionEvent : public RoomEvent<Content>
 
 template<class Content>
 void
-to_json(json &obj, const RedactionEvent<Content> &event)
-{
-        RoomEvent<Content> base_event = event;
-        to_json(obj, base_event);
-
-        obj["redacts"] = event.redacts;
-}
+to_json(json &obj, const RedactionEvent<Content> &event);
 
 template<class Content>
 void
-from_json(const json &obj, RedactionEvent<Content> &event)
-{
-        RoomEvent<Content> &base = event;
-        from_json(obj, base);
-
-        event.redacts = obj.at("redacts").get<std::string>();
-}
+from_json(const json &obj, RedactionEvent<Content> &event);
 
 //! Extension of the RoomEvent.
 template<class Content>
@@ -368,19 +155,11 @@ struct EncryptedEvent : public RoomEvent<Content>
 
 template<class Content>
 void
-to_json(json &obj, const EncryptedEvent<Content> &event)
-{
-        RoomEvent<Content> base_event = event;
-        to_json(obj, base_event);
-}
+to_json(json &obj, const EncryptedEvent<Content> &event);
 
 template<class Content>
 void
-from_json(const json &obj, EncryptedEvent<Content> &event)
-{
-        RoomEvent<Content> &base = event;
-        from_json(obj, base);
-}
+from_json(const json &obj, EncryptedEvent<Content> &event);
 
 enum class MessageType
 {
diff --git a/include/mtx/events/aliases.hpp b/include/mtx/events/aliases.hpp
index 6e680bf99102293729f57de9c7a705c6c5656cae..64a68c4c302d908b29cba2bf005f28cc51364acb 100644
--- a/include/mtx/events/aliases.hpp
+++ b/include/mtx/events/aliases.hpp
@@ -3,7 +3,11 @@
 #include <string>
 #include <vector>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx {
 namespace events {
diff --git a/include/mtx/events/avatar.hpp b/include/mtx/events/avatar.hpp
index a9071ce2376cc9f6205df9791ad0e44cf1bb48ae..1a7c7e1a6b7547c5487b468453e705f11812813f 100644
--- a/include/mtx/events/avatar.hpp
+++ b/include/mtx/events/avatar.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include "mtx/events/common.hpp"
 
diff --git a/include/mtx/events/canonical_alias.hpp b/include/mtx/events/canonical_alias.hpp
index d9dcb8308a2f959f2fd713f14efcef9373304670..ed19abd558bd64753b8ff2d86395f59f986929a8 100644
--- a/include/mtx/events/canonical_alias.hpp
+++ b/include/mtx/events/canonical_alias.hpp
@@ -2,7 +2,11 @@
 
 #include <string>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx {
 namespace events {
diff --git a/include/mtx/events/common.hpp b/include/mtx/events/common.hpp
index 269f7a06bf191694f2c3f1695b75052bea3917b8..ea6cd4afa212f83f17f0059f488bb3d3ff6d8198 100644
--- a/include/mtx/events/common.hpp
+++ b/include/mtx/events/common.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <optional>
 
diff --git a/include/mtx/events/create.hpp b/include/mtx/events/create.hpp
index 8e831305f5c69f2d8b348358255b9342d10a456e..69cf3831fae7729439e5e5183c3eb7d4276de577 100644
--- a/include/mtx/events/create.hpp
+++ b/include/mtx/events/create.hpp
@@ -2,7 +2,11 @@
 
 #include <optional>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx {
 namespace events {
diff --git a/include/mtx/events/encrypted.hpp b/include/mtx/events/encrypted.hpp
index 3180c250db4de462a2d8b1134a8fe24c479b954f..625bc747d6fb7760f1dcf72ceb227c71fa0d10a9 100644
--- a/include/mtx/events/encrypted.hpp
+++ b/include/mtx/events/encrypted.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include "mtx/events.hpp"
 #include "mtx/events/common.hpp"
@@ -392,94 +396,5 @@ void
 to_json(nlohmann::json &obj, const KeyVerificationMac &event);
 
 } // namespace msg
-struct DeviceEventVisitor
-{
-        nlohmann::json operator()(const DeviceEvent<mtx::events::msg::RoomKey> &roomKey)
-        {
-                json j;
-                mtx::events::to_json(j, roomKey);
-                return j;
-        }
-        nlohmann::json operator()(const DeviceEvent<mtx::events::msg::ForwardedRoomKey> &roomKey)
-        {
-                json j;
-                mtx::events::to_json(j, roomKey);
-                return j;
-        }
-        nlohmann::json operator()(const DeviceEvent<mtx::events::msg::KeyRequest> &keyReq)
-        {
-                json j;
-                mtx::events::to_json(j, keyReq);
-                return j;
-        }
-        nlohmann::json operator()(const DeviceEvent<mtx::events::msg::OlmEncrypted> &olmEnc)
-        {
-                json j;
-                mtx::events::to_json(j, olmEnc);
-                return j;
-        }
-        nlohmann::json operator()(const DeviceEvent<mtx::events::msg::Encrypted> &enc)
-        {
-                json j;
-                mtx::events::to_json(j, enc);
-                return j;
-        }
-        nlohmann::json operator()(
-          const DeviceEvent<mtx::events::msg::KeyVerificationRequest> &keyVerificationRequest)
-        {
-                json j;
-                mtx::events::to_json(j, keyVerificationRequest);
-                return j;
-        }
-        nlohmann::json operator()(
-          const DeviceEvent<mtx::events::msg::KeyVerificationAccept> &keyVerificationAccept)
-        {
-                json j;
-                mtx::events::to_json(j, keyVerificationAccept);
-                return j;
-        }
-        nlohmann::json operator()(
-          const DeviceEvent<mtx::events::msg::KeyVerificationStart> &keyVerificationStart)
-        {
-                json j;
-                mtx::events::to_json(j, keyVerificationStart);
-                return j;
-        }
-        nlohmann::json operator()(
-          const DeviceEvent<mtx::events::msg::KeyVerificationCancel> &KeyVerificationCancel)
-        {
-                json j;
-                mtx::events::to_json(j, KeyVerificationCancel);
-                return j;
-        }
-        nlohmann::json operator()(
-          const DeviceEvent<mtx::events::msg::KeyVerificationKey> &keyVerificationKey)
-        {
-                json j;
-                mtx::events::to_json(j, keyVerificationKey);
-                return j;
-        }
-        nlohmann::json operator()(
-          const DeviceEvent<mtx::events::msg::KeyVerificationMac> &keyVerificationMac)
-        {
-                json j;
-                mtx::events::to_json(j, keyVerificationMac);
-                return j;
-        }
-        nlohmann::json operator()(
-          const DeviceEvent<mtx::events::msg::KeyVerificationReady> &keyVerificationReady)
-        {
-                json j;
-                mtx::events::to_json(j, keyVerificationReady);
-                return j;
-        }
-        nlohmann::json operator()(
-          const DeviceEvent<mtx::events::msg::KeyVerificationDone> &keyVerificationDone)
-        {
-                json j;
-                mtx::events::to_json(j, keyVerificationDone);
-                return j;
-        }
-};
 } // namespace events
 } // namespace mtx
diff --git a/include/mtx/events/encryption.hpp b/include/mtx/events/encryption.hpp
index f7e86ba592a05f42ee4c38b417be41872687f273..b8c021647e65d4fc6bc18b86c2a03c645de47d78 100644
--- a/include/mtx/events/encryption.hpp
+++ b/include/mtx/events/encryption.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx {
 namespace events {
diff --git a/include/mtx/events/event_type.hpp b/include/mtx/events/event_type.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8ef332721bd57a8cba3313b8a95be7692d82a159
--- /dev/null
+++ b/include/mtx/events/event_type.hpp
@@ -0,0 +1,107 @@
+#pragma once
+
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
+#include <nlohmann/json.hpp>
+#endif
+
+#include <string>
+namespace mtx {
+namespace events {
+
+enum class EventType
+{
+        /// m.key.verification.cancel
+        KeyVerificationCancel,
+        /// m.key.verification.request
+        KeyVerificationRequest,
+        /// m.key.verification.start
+        KeyVerificationStart,
+        /// m.key.verification.accept
+        KeyVerificationAccept,
+        /// m.key.verification.key
+        KeyVerificationKey,
+        /// m.key.verification.mac
+        KeyVerificationMac,
+        /// m.key.verification.ready,
+        KeyVerificationReady,
+        /// m.key.verification.done,
+        KeyVerificationDone,
+        /// m.reaction,
+        Reaction,
+        /// m.room_key
+        RoomKey,
+        /// m.forwarded_room_key
+        ForwardedRoomKey,
+        /// m.room_key_request
+        RoomKeyRequest,
+        /// m.room.aliases
+        RoomAliases,
+        /// m.room.avatar
+        RoomAvatar,
+        /// m.room.canonical_alias
+        RoomCanonicalAlias,
+        /// m.room.create
+        RoomCreate,
+        /// m.room.encrypted.
+        RoomEncrypted,
+        /// m.room.encryption.
+        RoomEncryption,
+        /// m.room.guest_access
+        RoomGuestAccess,
+        /// m.room.history_visibility
+        RoomHistoryVisibility,
+        /// m.room.join_rules
+        RoomJoinRules,
+        /// m.room.member
+        RoomMember,
+        /// m.room.message
+        RoomMessage,
+        /// m.room.name
+        RoomName,
+        /// m.room.power_levels
+        RoomPowerLevels,
+        /// m.room.topic
+        RoomTopic,
+        /// m.room.redaction
+        RoomRedaction,
+        /// m.room.pinned_events
+        RoomPinnedEvents,
+        /// m.room.tombstone
+        RoomTombstone,
+        // m.sticker
+        Sticker,
+        // m.tag
+        Tag,
+        // m.presence
+        Presence,
+        // m.push_rules
+        PushRules,
+        // m.call.invite
+        CallInvite,
+        // m.call.candidates
+        CallCandidates,
+        // m.call.answer
+        CallAnswer,
+        // m.call.hangup
+        CallHangUp,
+
+        // custom events
+        // im.nheko.hidden_events
+        NhekoHiddenEvents,
+
+        // Unsupported event
+        Unsupported,
+};
+
+std::string
+to_string(EventType type);
+
+EventType
+getEventType(const std::string &type);
+
+EventType
+getEventType(const nlohmann::json &obj);
+}
+}
diff --git a/include/mtx/events/guest_access.hpp b/include/mtx/events/guest_access.hpp
index 4f3d31051d78f90f4fcc13c6bf9d7020603317ce..2718f82431683c75d7bb4552fb1d79f4de464dbc 100644
--- a/include/mtx/events/guest_access.hpp
+++ b/include/mtx/events/guest_access.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 
diff --git a/include/mtx/events/history_visibility.hpp b/include/mtx/events/history_visibility.hpp
index 30bf911b4cfe035c81af07bd39e1b369ce33b64e..ddc8cc4a5838c42685bbf6d0801f6d4762f655df 100644
--- a/include/mtx/events/history_visibility.hpp
+++ b/include/mtx/events/history_visibility.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 
diff --git a/include/mtx/events/join_rules.hpp b/include/mtx/events/join_rules.hpp
index e8aab20cfbeb16cfb99c410364745fd90fbcbc5f..63473fd28197c1e95e7257cf8ce4465a429153af 100644
--- a/include/mtx/events/join_rules.hpp
+++ b/include/mtx/events/join_rules.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 
diff --git a/include/mtx/events/member.hpp b/include/mtx/events/member.hpp
index 80ed56c588f465d8e189b5fe0ff17cd90e085681..25fb66a9bf5423477f629ca59212294c9194aba2 100644
--- a/include/mtx/events/member.hpp
+++ b/include/mtx/events/member.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 
diff --git a/include/mtx/events/messages/audio.hpp b/include/mtx/events/messages/audio.hpp
index c16802337f427480ceef17b265e05c0d58e2fb2a..501ff94e0b789d2250266b4a6b447eaaff5a1913 100644
--- a/include/mtx/events/messages/audio.hpp
+++ b/include/mtx/events/messages/audio.hpp
@@ -3,7 +3,11 @@
 #include <optional>
 #include <string>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include "mtx/common.hpp"
 #include "mtx/events/common.hpp"
diff --git a/include/mtx/events/messages/emote.hpp b/include/mtx/events/messages/emote.hpp
index 67f4e98c3be15b1969583d8e1108ce16702ac1ce..6061c7cbf7231a826d161b962e9e4aadb03091b6 100644
--- a/include/mtx/events/messages/emote.hpp
+++ b/include/mtx/events/messages/emote.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 
diff --git a/include/mtx/events/messages/file.hpp b/include/mtx/events/messages/file.hpp
index 4cbe285f387a4a343d06cb147093f8f25885c4af..c029e9cd8eded757033ef00672bda8e8ac79a911 100644
--- a/include/mtx/events/messages/file.hpp
+++ b/include/mtx/events/messages/file.hpp
@@ -3,7 +3,11 @@
 #include <optional>
 #include <string>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include "mtx/common.hpp"
 #include "mtx/events/common.hpp"
diff --git a/include/mtx/events/messages/image.hpp b/include/mtx/events/messages/image.hpp
index 3188566a5173e1e28d4d55d0215ad2d439deb6cb..00619d953583103451b9e40912cb7f6a8221be41 100644
--- a/include/mtx/events/messages/image.hpp
+++ b/include/mtx/events/messages/image.hpp
@@ -3,7 +3,11 @@
 #include <optional>
 #include <string>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include "mtx/common.hpp"
 #include "mtx/events/common.hpp"
diff --git a/include/mtx/events/messages/notice.hpp b/include/mtx/events/messages/notice.hpp
index b1783deb819313abc5eef0f986d2d1371d34bf65..7e89ee3caee39523e2820ea83f15b8048fd12f24 100644
--- a/include/mtx/events/messages/notice.hpp
+++ b/include/mtx/events/messages/notice.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 
diff --git a/include/mtx/events/messages/text.hpp b/include/mtx/events/messages/text.hpp
index 2b55014f1e402ba26a6e0bbbd12830c10f0d63ca..1a929f3535c3ec540a77bc6b4b7630606285da47 100644
--- a/include/mtx/events/messages/text.hpp
+++ b/include/mtx/events/messages/text.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 
diff --git a/include/mtx/events/messages/video.hpp b/include/mtx/events/messages/video.hpp
index d7009307f8dbb63d6856c2d5520f4317841a5b44..2af4b1eb8f666876eaff29c08ebea38c0ad87c0b 100644
--- a/include/mtx/events/messages/video.hpp
+++ b/include/mtx/events/messages/video.hpp
@@ -3,7 +3,11 @@
 #include <optional>
 #include <string>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include "mtx/common.hpp"
 #include "mtx/events/common.hpp"
diff --git a/include/mtx/events/name.hpp b/include/mtx/events/name.hpp
index 292f9d16c97b4d34f5759cd63840965e0710d0bd..67fdbadec5252e6d767924814b466e3553fe6c91 100644
--- a/include/mtx/events/name.hpp
+++ b/include/mtx/events/name.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 
diff --git a/include/mtx/events/nheko_extensions/hidden_events.hpp b/include/mtx/events/nheko_extensions/hidden_events.hpp
index ef69854c9f86cd957bac564e5a1e76708cfb0d1c..7eb92578a505e03a821f218179ea24bd3ea8b92d 100644
--- a/include/mtx/events/nheko_extensions/hidden_events.hpp
+++ b/include/mtx/events/nheko_extensions/hidden_events.hpp
@@ -2,7 +2,11 @@
 
 #include <vector>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include "mtx/events.hpp"
 
diff --git a/include/mtx/events/pinned_events.hpp b/include/mtx/events/pinned_events.hpp
index 51a58bb0eca0ce105000363173f96cde7c8c2f59..c5e64606a08eb401d1eb3e4914d032e887e598bc 100644
--- a/include/mtx/events/pinned_events.hpp
+++ b/include/mtx/events/pinned_events.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 
diff --git a/include/mtx/events/power_levels.hpp b/include/mtx/events/power_levels.hpp
index ea6c7703404eced1f99e3c377e7f5dcbaf055a74..375e67032304bd362208db3dc23eb79904d532ba 100644
--- a/include/mtx/events/power_levels.hpp
+++ b/include/mtx/events/power_levels.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 
diff --git a/include/mtx/events/presence.hpp b/include/mtx/events/presence.hpp
index 4ce9d1490f363a53c015ed1f9036a113fe384c45..34fbc191d365cbff3c90eea7f1bcf58d69266a79 100644
--- a/include/mtx/events/presence.hpp
+++ b/include/mtx/events/presence.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <optional>
 #include <string>
diff --git a/include/mtx/events/reaction.hpp b/include/mtx/events/reaction.hpp
index 742f8ff8266d63d24eaa85da3064b453e5ee3413..1ba22a1fc59ce598e59447808c8c32fb54031c3d 100644
--- a/include/mtx/events/reaction.hpp
+++ b/include/mtx/events/reaction.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 
diff --git a/include/mtx/events/redaction.hpp b/include/mtx/events/redaction.hpp
index 077c55dfce3ddff2092ce7a05068ba32e0ba5937..c7bd339b69d4c6f8d192cf5c05363be5a096352f 100644
--- a/include/mtx/events/redaction.hpp
+++ b/include/mtx/events/redaction.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 
diff --git a/include/mtx/events/tag.hpp b/include/mtx/events/tag.hpp
index 728bb4729ed011d2ac25e5c6859c1925184b2d6c..1eb78a02fde44ff8dfa06a4b68b7e21934e8b7ca 100644
--- a/include/mtx/events/tag.hpp
+++ b/include/mtx/events/tag.hpp
@@ -3,7 +3,11 @@
 #include <optional>
 #include <string>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx {
 namespace events {
diff --git a/include/mtx/events/tombstone.hpp b/include/mtx/events/tombstone.hpp
index 0243a8efdc1cfe90acc8e421c59400567157a866..f5d3de25bdc74a0efe05b7bd96fe3b4705759801 100644
--- a/include/mtx/events/tombstone.hpp
+++ b/include/mtx/events/tombstone.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx {
 namespace events {
diff --git a/include/mtx/events/topic.hpp b/include/mtx/events/topic.hpp
index 34a89098d61e5937e8c33757c2c5718440544b70..5a1f2f04ce50277dd3012babe2d1b492a0875db2 100644
--- a/include/mtx/events/topic.hpp
+++ b/include/mtx/events/topic.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 
diff --git a/include/mtx/events/voip.hpp b/include/mtx/events/voip.hpp
index 54c4c0aced3c6c7702b6a36f47f9bbdb6f93bf42..7ed17936602a4ce4213a489ad560a2cb2ef76689 100644
--- a/include/mtx/events/voip.hpp
+++ b/include/mtx/events/voip.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 #include <vector>
diff --git a/include/mtx/events_impl.hpp b/include/mtx/events_impl.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..5f574a6b1f6f28796803c96ae9f5af7c50ea4b36
--- /dev/null
+++ b/include/mtx/events_impl.hpp
@@ -0,0 +1,199 @@
+#include "events.hpp"
+
+#include <nlohmann/json.hpp>
+
+namespace mtx::events {
+template<class Content>
+void
+to_json(json &obj, const Event<Content> &event)
+{
+        obj["content"] = event.content;
+        obj["sender"]  = event.sender;
+        obj["type"]    = ::mtx::events::to_string(event.type);
+}
+
+template<class Content>
+void
+from_json(const json &obj, Event<Content> &event)
+{
+        event.content = obj.at("content").get<Content>();
+        event.type    = getEventType(obj.at("type").get<std::string>());
+        event.sender  = obj.value("sender", "");
+}
+
+template<class Content>
+void
+from_json(const json &obj, DeviceEvent<Content> &event)
+{
+        Event<Content> base_event = event;
+        from_json(obj, base_event);
+        event.content = base_event.content;
+        event.type    = base_event.type;
+        event.sender  = obj.at("sender");
+}
+
+template<class Content>
+void
+to_json(json &obj, const DeviceEvent<Content> &event)
+{
+        Event<Content> base_event = event;
+        to_json(obj, base_event);
+
+        obj["sender"] = event.sender;
+}
+
+void
+from_json(const json &obj, UnsignedData &data)
+{
+        if (obj.find("age") != obj.end())
+                data.age = obj.at("age").get<uint64_t>();
+
+        if (obj.find("transaction_id") != obj.end())
+                data.transaction_id = obj.at("transaction_id").get<std::string>();
+
+        if (obj.find("prev_sender") != obj.end())
+                data.prev_sender = obj.at("prev_sender").get<std::string>();
+
+        if (obj.find("replaces_state") != obj.end())
+                data.replaces_state = obj.at("replaces_state").get<std::string>();
+
+        if (obj.find("redacted_by") != obj.end())
+                data.redacted_by = obj.at("redacted_by").get<std::string>();
+
+        if (obj.find("redacted_because") != obj.end())
+                data.redacted_because =
+                  obj.at("redacted_because").get<Event<mtx::events::msg::Redaction>>();
+}
+
+void
+to_json(json &obj, const UnsignedData &event)
+{
+        if (!event.prev_sender.empty())
+                obj["prev_sender"] = event.prev_sender;
+
+        if (!event.transaction_id.empty())
+                obj["transaction_id"] = event.transaction_id;
+
+        if (!event.replaces_state.empty())
+                obj["replaces_state"] = event.replaces_state;
+
+        if (event.age != 0)
+                obj["age"] = event.age;
+
+        if (!event.redacted_by.empty())
+                obj["redacted_by"] = event.redacted_by;
+
+        if (event.redacted_because)
+                obj["redacted_because"] = *event.redacted_because;
+}
+
+template<class Content>
+void
+from_json(const json &obj, StrippedEvent<Content> &event)
+{
+        Event<Content> &base = event;
+        from_json(obj, base);
+
+        event.state_key = obj.at("state_key");
+}
+
+template<class Content>
+void
+to_json(json &obj, const StrippedEvent<Content> &event)
+{
+        Event<Content> base_event = event;
+        to_json(obj, base_event);
+
+        obj["state_key"] = event.state_key;
+}
+
+template<class Content>
+void
+from_json(const json &obj, RoomEvent<Content> &event)
+{
+        Event<Content> &base = event;
+        from_json(obj, base);
+
+        event.event_id         = obj.at("event_id");
+        event.origin_server_ts = obj.at("origin_server_ts");
+
+        // SPEC_BUG: Not present in the state array returned by /sync.
+        if (obj.find("room_id") != obj.end())
+                event.room_id = obj.at("room_id");
+
+        if (obj.find("unsigned") != obj.end())
+                event.unsigned_data = obj.at("unsigned");
+}
+
+template<class Content>
+void
+to_json(json &obj, const RoomEvent<Content> &event)
+{
+        Event<Content> base_event = event;
+        to_json(obj, base_event);
+
+        if (!event.room_id.empty())
+                obj["room_id"] = event.room_id;
+
+        obj["event_id"]         = event.event_id;
+        obj["unsigned"]         = event.unsigned_data;
+        obj["origin_server_ts"] = event.origin_server_ts;
+}
+
+template<class Content>
+void
+to_json(json &obj, const StateEvent<Content> &event)
+{
+        RoomEvent<Content> base_event = event;
+        to_json(obj, base_event);
+
+        obj["state_key"] = event.state_key;
+}
+
+template<class Content>
+void
+from_json(const json &obj, StateEvent<Content> &event)
+{
+        RoomEvent<Content> &base = event;
+        from_json(obj, base);
+
+        event.state_key = obj.at("state_key").get<std::string>();
+}
+
+template<class Content>
+void
+to_json(json &obj, const RedactionEvent<Content> &event)
+{
+        RoomEvent<Content> base_event = event;
+        to_json(obj, base_event);
+
+        obj["redacts"] = event.redacts;
+}
+
+template<class Content>
+void
+from_json(const json &obj, RedactionEvent<Content> &event)
+{
+        RoomEvent<Content> &base = event;
+        from_json(obj, base);
+
+        event.redacts = obj.at("redacts").get<std::string>();
+}
+
+template<class Content>
+void
+to_json(json &obj, const EncryptedEvent<Content> &event)
+{
+        RoomEvent<Content> base_event = event;
+        to_json(obj, base_event);
+}
+
+template<class Content>
+void
+from_json(const json &obj, EncryptedEvent<Content> &event)
+{
+        RoomEvent<Content> &base = event;
+        from_json(obj, base);
+}
+
+}
diff --git a/include/mtx/identifiers.hpp b/include/mtx/identifiers.hpp
index c0fa48c408a10f2272fa68cac63891efd1ae169b..b6ef59e576dd2d56055b2f4db7b0c666095708be 100644
--- a/include/mtx/identifiers.hpp
+++ b/include/mtx/identifiers.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <stdexcept>
 
diff --git a/include/mtx/lightweight_error.hpp b/include/mtx/lightweight_error.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..dc207521ad3a5993098e32ccbf2aed39e3c5d072
--- /dev/null
+++ b/include/mtx/lightweight_error.hpp
@@ -0,0 +1,74 @@
+#pragma once
+
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
+#include <nlohmann/json.hpp>
+#endif
+#include <string>
+
+namespace mtx {
+namespace errors {
+
+enum class ErrorCode
+{
+        M_UNRECOGNIZED,
+        //! unknown user or so
+        M_UNKNOWN,
+        //! Forbidden access, e.g. joining a room without permission, failed login.
+        M_FORBIDDEN,
+        //! The access token specified was not recognised.
+        M_UNKNOWN_TOKEN,
+        //! Request contained valid JSON, but it was malformed in some way,
+        //! e.g. missing required keys, invalid values for keys
+        M_BAD_JSON,
+        //! Request did not contain valid JSON.
+        M_NOT_JSON,
+        //! No resource was found for this request.
+        M_NOT_FOUND,
+        //! Too many requests have been sent in a short period of time.
+        M_LIMIT_EXCEEDED,
+        //! Encountered when trying to register a user ID which has been taken.
+        M_USER_IN_USE,
+        //! Encountered when trying to register a user ID which is not valid.
+        M_INVALID_USERNAME,
+        //! Sent when the room alias given to the createRoom API is already in use.
+        M_ROOM_IN_USE,
+        //! Sent when the intial state given to the createRoom API is invalid.
+        M_INVALID_ROOM_STATE,
+        //! Encountered when specifying bad pagination query parameters.
+        M_BAD_PAGINATION,
+        //! Sent when a threepid given to an API cannot be used because
+        //! the same threepid is already in use.
+        M_THREEPID_IN_USE,
+        //! Sent when a threepid given to an API cannot be used
+        //! because no record matching the threepid was found.
+        M_THREEPID_NOT_FOUND,
+        //! The client's request used a third party server,
+        //! eg. ID server, that this server does not trust.
+        M_SERVER_NOT_TRUSTED,
+        //! The access token isn't present in the request.
+        M_MISSING_TOKEN,
+        //! One of the uploaded signatures was invalid
+        M_INVALID_SIGNATURE,
+};
+
+std::string
+to_string(ErrorCode code);
+
+ErrorCode
+from_string(const std::string &code);
+
+//! Represents a Matrix related error.
+struct LightweightError
+{
+        //! Error code.
+        ErrorCode errcode = {};
+        //! Human readable version of the error.
+        std::string error;
+};
+
+void
+from_json(const nlohmann::json &obj, LightweightError &error);
+}
+}
diff --git a/include/mtx/pushrules.hpp b/include/mtx/pushrules.hpp
index ad73054351a0225f03606eb2605b58344b4e1bd1..f4301d96be4dfbe84574ccb0a1c3a0dda33ad4d9 100644
--- a/include/mtx/pushrules.hpp
+++ b/include/mtx/pushrules.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 #include <variant>
diff --git a/include/mtx/requests.hpp b/include/mtx/requests.hpp
index 4a4fb0f54db39c86a045816de9e0d820449f6f77..e73586b7c136624bb29a0b1901db9c7acbd4e3e9 100644
--- a/include/mtx/requests.hpp
+++ b/include/mtx/requests.hpp
@@ -5,7 +5,11 @@
 
 #include <mtx/common.hpp>
 #include <mtx/events/collections.hpp>
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 using json = nlohmann::json;
 
@@ -142,6 +146,17 @@ to_json(json &, const Empty &)
 
 using Logout = Empty;
 
+struct SignedOneTimeKey
+{
+        //! Required. The unpadded Base64-encoded 32-byte Curve25519 public key.
+        std::string key;
+        //! Required. Signatures of the key object.
+        //! The signature is calculated using the process described at Signing JSON.
+        std::map<std::string, std::map<std::string, std::string>> signatures;
+};
+void
+to_json(json &obj, const SignedOneTimeKey &);
+
 struct UploadKeys
 {
         //! Identity keys for the device.
@@ -150,7 +165,7 @@ struct UploadKeys
         //! One-time public keys for "pre-key" messages.
         //! The names of the properties should be in the format <algorithm>:<key_id>.
         //! The format of the key is determined by the key algorithm.
-        std::map<std::string, json> one_time_keys;
+        std::map<std::string, std::variant<std::string, SignedOneTimeKey>> one_time_keys;
 };
 
 void
@@ -185,12 +200,8 @@ struct ClaimKeys
         std::map<std::string, std::map<std::string, std::string>> one_time_keys;
 };
 
-inline void
-to_json(json &obj, const ClaimKeys &request)
-{
-        obj["timeout"]       = request.timeout;
-        obj["one_time_keys"] = request.one_time_keys;
-}
+void
+to_json(json &obj, const ClaimKeys &request);
 
 struct KeySignaturesUpload
 {
diff --git a/include/mtx/responses/common.hpp b/include/mtx/responses/common.hpp
index b1422ad2b5ce552f122f3ef51e621b9382e38d79..574679157b94b11559cf4f7a73abad0507db7e7d 100644
--- a/include/mtx/responses/common.hpp
+++ b/include/mtx/responses/common.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 #include <vector>
@@ -26,6 +30,14 @@ struct GroupId
 void
 from_json(const nlohmann::json &obj, GroupId &response);
 
+struct RoomId
+{
+        std::string room_id;
+};
+
+void
+from_json(const nlohmann::json &obj, RoomId &response);
+
 struct FilterId
 {
         std::string filter_id;
@@ -46,7 +58,7 @@ namespace states = mtx::events::state;
 namespace msgs   = mtx::events::msg;
 
 void
-log_error(nlohmann::json::exception &err, const nlohmann::json &event);
+log_error(std::exception &err, const nlohmann::json &event);
 
 void
 log_error(std::string err, const nlohmann::json &event);
diff --git a/include/mtx/responses/create_room.hpp b/include/mtx/responses/create_room.hpp
index d460785dd9675b4b6b509ad6a04215e88d1aec7d..e1fe6e10d364103f23d0944188880c21a680264b 100644
--- a/include/mtx/responses/create_room.hpp
+++ b/include/mtx/responses/create_room.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx {
 namespace responses {
diff --git a/include/mtx/responses/crypto.hpp b/include/mtx/responses/crypto.hpp
index 0d72b8952c72f34e53b2eb96317bbb3694a3caca..358886bc64db82bf9ac6968ca01d8f617415f62c 100644
--- a/include/mtx/responses/crypto.hpp
+++ b/include/mtx/responses/crypto.hpp
@@ -1,9 +1,13 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include "mtx/common.hpp"
-#include "mtx/errors.hpp"
+#include "mtx/lightweight_error.hpp"
 
 #include <map>
 #include <string>
@@ -51,7 +55,7 @@ from_json(const nlohmann::json &obj, QueryKeys &response);
 
 struct KeySignaturesUpload
 {
-        std::map<std::string, std::map<std::string, mtx::errors::Error>> errors;
+        std::map<std::string, std::map<std::string, mtx::errors::LightweightError>> errors;
 };
 
 void
@@ -158,7 +162,7 @@ struct BackupVersion
         std::string algorithm;
         //! Required. Algorithm-dependent data. See the documentation for the backup algorithms in
         //! Server-side key backups for more information on the expected format of the data.
-        nlohmann::json auth_data;
+        std::string auth_data;
         //! Required. The number of keys stored in the backup.
         int64_t count;
         //! Required. An opaque string representing stored keys in the backup. Clients can
diff --git a/include/mtx/responses/empty.hpp b/include/mtx/responses/empty.hpp
index 72d680926d199494a9352605df93ea3288b56e5d..cf742761afa8f9143c67355e23963e366ee4f3c9 100644
--- a/include/mtx/responses/empty.hpp
+++ b/include/mtx/responses/empty.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include "mtx/identifiers.hpp"
 
diff --git a/include/mtx/responses/groups.hpp b/include/mtx/responses/groups.hpp
index 7872e274ec3b244d53602d868590fcd43fdcdc4c..b7012ac0cb4c1ed6cd456f8eb39ab0502f77df61 100644
--- a/include/mtx/responses/groups.hpp
+++ b/include/mtx/responses/groups.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx {
 namespace responses {
diff --git a/include/mtx/responses/login.hpp b/include/mtx/responses/login.hpp
index 5b973c249b8573b212cc46ef57d4866bb563dd86..e38f30e9e81761724ee18633816ed159f5570dad 100644
--- a/include/mtx/responses/login.hpp
+++ b/include/mtx/responses/login.hpp
@@ -3,7 +3,11 @@
 #include <optional>
 #include <string>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include "mtx/identifiers.hpp"
 #include "mtx/user_interactive.hpp"
diff --git a/include/mtx/responses/media.hpp b/include/mtx/responses/media.hpp
index 3853923bbf9bc066187f90c9bf0ec4ff31762153..c40e7d0f889497ccbde31454e3543340fdd33f21 100644
--- a/include/mtx/responses/media.hpp
+++ b/include/mtx/responses/media.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <string>
 
diff --git a/include/mtx/responses/messages.hpp b/include/mtx/responses/messages.hpp
index 3b09ef4c489439b6598bd3aedee974d3a351f1c7..1bf7da48ebe0a9269324304228323fae0435fae1 100644
--- a/include/mtx/responses/messages.hpp
+++ b/include/mtx/responses/messages.hpp
@@ -2,7 +2,11 @@
 
 #include <string>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include "mtx/events/collections.hpp"
 
diff --git a/include/mtx/responses/notifications.hpp b/include/mtx/responses/notifications.hpp
index 7c7804a1b1cc44c38127214f5927bcb8d51e4164..afefcab7cca2222c45291a0f2625062cfd3fb737 100644
--- a/include/mtx/responses/notifications.hpp
+++ b/include/mtx/responses/notifications.hpp
@@ -1,8 +1,13 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include "mtx/events/collections.hpp"
+#include "mtx/pushrules.hpp"
 
 namespace mtx {
 namespace responses {
@@ -10,7 +15,7 @@ namespace responses {
 struct Notification
 {
         //! The action to perform when the conditions for this rule are met.
-        nlohmann::json actions;
+        std::vector<mtx::pushrules::actions::Action> actions;
         //! The Event object for the event that triggered the notification.
         mtx::events::collections::TimelineEvents event;
         //! Indicates whether the user has sent a read receipt indicating
diff --git a/include/mtx/responses/profile.hpp b/include/mtx/responses/profile.hpp
index 47c3d1474cf20d066fd38df7ab1f77de656219fb..d1a046b610576bd7aa7584ed9bf3bdc87363631a 100644
--- a/include/mtx/responses/profile.hpp
+++ b/include/mtx/responses/profile.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx {
 namespace responses {
diff --git a/include/mtx/responses/register.hpp b/include/mtx/responses/register.hpp
index fd0780999e3376d1e532d982da53f82eae38ecc9..4a1c1654674643ed311cc4f02bf28b526bf5f4ea 100644
--- a/include/mtx/responses/register.hpp
+++ b/include/mtx/responses/register.hpp
@@ -2,7 +2,11 @@
 
 #include <string>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include "mtx/identifiers.hpp"
 
diff --git a/include/mtx/responses/sync.hpp b/include/mtx/responses/sync.hpp
index 5af1fbc497ccc6480a894650a78465a1043d6f20..0c1f5fd3671dc48fc2c801cbed8f1c8af30dd4e7 100644
--- a/include/mtx/responses/sync.hpp
+++ b/include/mtx/responses/sync.hpp
@@ -6,7 +6,11 @@
 
 #include "mtx/events/collections.hpp"
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx {
 namespace responses {
diff --git a/include/mtx/responses/turn_server.hpp b/include/mtx/responses/turn_server.hpp
index 8788b95b21b6f24090a5115df3c68271cc6ffe83..c8fe103794fc76997ffac7cc9bc2c950bf7e5c44 100644
--- a/include/mtx/responses/turn_server.hpp
+++ b/include/mtx/responses/turn_server.hpp
@@ -3,7 +3,11 @@
 #include <string>
 #include <vector>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx::responses {
 
diff --git a/include/mtx/responses/version.hpp b/include/mtx/responses/version.hpp
index bc0ab5c0178e477207699f7c633a1023822da5b1..28b44f11bffd3b4676a1ead603ec9788bca5ab8a 100644
--- a/include/mtx/responses/version.hpp
+++ b/include/mtx/responses/version.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx {
 namespace responses {
diff --git a/include/mtx/responses/well-known.hpp b/include/mtx/responses/well-known.hpp
index 2c53000b1e1fbf9ed2ee209b884a4243fde222bd..98c246aa6f412275ddcbd59f5f20615d02d19277 100644
--- a/include/mtx/responses/well-known.hpp
+++ b/include/mtx/responses/well-known.hpp
@@ -2,7 +2,11 @@
 
 #include <optional>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx {
 namespace responses {
diff --git a/include/mtx/secret_storage.hpp b/include/mtx/secret_storage.hpp
index 2ab388ff357d435bf2cb051d3056f843e3aa72a9..05d0d462e9be450972313229205a52930419dd64 100644
--- a/include/mtx/secret_storage.hpp
+++ b/include/mtx/secret_storage.hpp
@@ -5,7 +5,11 @@
 #include <optional>
 #include <string>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx {
 namespace secret_storage {
diff --git a/include/mtx/user_interactive.hpp b/include/mtx/user_interactive.hpp
index fde1360d0c9e5ec12e84d3f005dc819b57ff4d6f..90c36cc6b5b8061999a33be7f175a86b1f0f9177 100644
--- a/include/mtx/user_interactive.hpp
+++ b/include/mtx/user_interactive.hpp
@@ -6,7 +6,11 @@
 #include <variant>
 #include <vector>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 namespace mtx {
 namespace user_interactive {
@@ -62,7 +66,7 @@ struct TermsParams
 void
 from_json(const nlohmann::json &obj, TermsParams &params);
 
-using Params = std::variant<OAuth2Params, TermsParams, nlohmann::json>;
+using Params = std::variant<OAuth2Params, TermsParams, std::string>;
 
 struct Unauthorized
 {
diff --git a/include/mtxclient/crypto/client.hpp b/include/mtxclient/crypto/client.hpp
index 6d32abad01fe7b0e4debb7cd9cdafb580127b8b6..1476743d3814b23d2e5b807b573ef83556ec3a9d 100644
--- a/include/mtxclient/crypto/client.hpp
+++ b/include/mtxclient/crypto/client.hpp
@@ -4,7 +4,11 @@
 #include <memory>
 #include <new>
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include <mtx/identifiers.hpp>
 #include <mtx/requests.hpp>
@@ -91,11 +95,6 @@ unpickle(const std::string &pickled, const std::string &key)
         return object;
 }
 
-using OlmSessionPtr           = std::unique_ptr<OlmSession, OlmDeleter>;
-using OutboundGroupSessionPtr = std::unique_ptr<OlmOutboundGroupSession, OlmDeleter>;
-using InboundGroupSessionPtr  = std::unique_ptr<OlmInboundGroupSession, OlmDeleter>;
-using SASPtr                  = std::unique_ptr<OlmSAS, OlmDeleter>;
-
 struct GroupPlaintext
 {
         BinaryBuf data;
@@ -125,7 +124,7 @@ public:
         {}
 
         using Base64String      = std::string;
-        using SignedOneTimeKeys = std::map<std::string, json>;
+        using SignedOneTimeKeys = std::map<std::string, requests::SignedOneTimeKey>;
 
         void set_device_id(std::string device_id) { device_id_ = std::move(device_id); }
         void set_user_id(std::string user_id) { user_id_ = std::move(user_id); }
@@ -154,7 +153,8 @@ public:
         //! Sign one_time_keys and generate the appropriate structure for the /keys/upload request.
         SignedOneTimeKeys sign_one_time_keys(const OneTimeKeys &keys);
         //! Generate the json structure for the signed one time key.
-        json signed_one_time_key_json(const std::string &key, const std::string &signature);
+        requests::SignedOneTimeKey signed_one_time_key(const std::string &key,
+                                                       const std::string &signature);
 
         //! Marks the current set of one time keys as being published.
         void mark_keys_as_published() { olm_account_mark_keys_as_published(account_.get()); }
diff --git a/include/mtxclient/crypto/objects.hpp b/include/mtxclient/crypto/objects.hpp
index 8da33e242d3a568bf6713114cbfef481cca27050..a0fa155a27b8e5ff206dc38d3da3d4ef28d35b42 100644
--- a/include/mtxclient/crypto/objects.hpp
+++ b/include/mtxclient/crypto/objects.hpp
@@ -184,5 +184,10 @@ create_olm_object()
 {
         return std::unique_ptr<typename T::olm_type, OlmDeleter>(T::allocate());
 }
+
+using OlmSessionPtr           = std::unique_ptr<OlmSession, OlmDeleter>;
+using OutboundGroupSessionPtr = std::unique_ptr<OlmOutboundGroupSession, OlmDeleter>;
+using InboundGroupSessionPtr  = std::unique_ptr<OlmInboundGroupSession, OlmDeleter>;
+using SASPtr                  = std::unique_ptr<OlmSAS, OlmDeleter>;
 }
 }
diff --git a/include/mtxclient/crypto/types.hpp b/include/mtxclient/crypto/types.hpp
index b78b6f2f8d28ade1358f5fc613c8c06280b97617..455b98dfb4be8db3e375d085808557a6692a47cd 100644
--- a/include/mtxclient/crypto/types.hpp
+++ b/include/mtxclient/crypto/types.hpp
@@ -1,11 +1,10 @@
 #pragma once
 
-#include "mtxclient/utils.hpp"
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
-
-STRONG_TYPE(UserId, std::string)
-STRONG_TYPE(DeviceId, std::string)
-STRONG_TYPE(RoomId, std::string)
+#endif
 
 namespace mtx {
 namespace crypto {
@@ -70,5 +69,38 @@ to_json(nlohmann::json &obj, const OneTimeKeys &keys);
 void
 from_json(const nlohmann::json &obj, OneTimeKeys &keys);
 
+template<class T, class Name>
+class strong_type
+{
+public:
+        strong_type() = default;
+        explicit strong_type(const T &value)
+          : value_(value)
+        {}
+        explicit strong_type(T &&value)
+          : value_(std::forward<T>(value))
+        {}
+
+        operator T &() noexcept { return value_; }
+        constexpr operator const T &() const noexcept { return value_; }
+
+        T &get() { return value_; }
+        T const &get() const { return value_; }
+
+private:
+        T value_;
+};
+
+// Macro for concisely defining a strong type
+#define STRONG_TYPE(type_name, value_type)                                                         \
+        struct type_name : mtx::crypto::strong_type<value_type, type_name>                         \
+        {                                                                                          \
+                using strong_type::strong_type;                                                    \
+        };
+
 } // namespace crypto
 } // namespace mtx
+
+STRONG_TYPE(UserId, std::string)
+STRONG_TYPE(DeviceId, std::string)
+STRONG_TYPE(RoomId, std::string)
diff --git a/include/mtxclient/http/client.hpp b/include/mtxclient/http/client.hpp
index 0767b92704f8e27f11c08139cb2b4b006f85485b..b82e7c0df74dd30f419e96998683f617389083f7 100644
--- a/include/mtxclient/http/client.hpp
+++ b/include/mtxclient/http/client.hpp
@@ -1,6 +1,10 @@
 #pragma once
 
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
 #include <nlohmann/json.hpp>
+#endif
 
 #include "mtx/errors.hpp"             // for Error
 #include "mtx/events.hpp"             // for EventType, to_string, json
@@ -46,6 +50,7 @@ struct ClaimKeys;
 struct ContentURI;
 struct CreateRoom;
 struct EventId;
+struct RoomId;
 struct FilterId;
 struct GroupId;
 struct GroupProfile;
@@ -312,9 +317,9 @@ public:
         void create_room(const mtx::requests::CreateRoom &room_options,
                          Callback<mtx::responses::CreateRoom> cb);
         //! Join a room by an alias or a room_id.
-        void join_room(const std::string &room, Callback<nlohmann::json> cb);
+        void join_room(const std::string &room, Callback<mtx::responses::RoomId> cb);
         //! Leave a room by its room_id.
-        void leave_room(const std::string &room_id, Callback<nlohmann::json> cb);
+        void leave_room(const std::string &room_id, Callback<mtx::responses::Empty> cb);
         //! Invite a user to a room.
         void invite_user(const std::string &room_id,
                          const std::string &user_id,
@@ -439,18 +444,7 @@ public:
         void send_to_device(
           const std::string &txid,
           const std::map<mtx::identifiers::User, std::map<std::string, EventContent>> &messages,
-          ErrCallback callback)
-        {
-                constexpr auto event_type = mtx::events::to_device_content_to_type<EventContent>;
-                static_assert(event_type != mtx::events::EventType::Unsupported);
-
-                json j;
-                for (const auto &[user, deviceToMessage] : messages)
-                        for (const auto &[deviceid, message] : deviceToMessage)
-                                j["messages"][user.to_string()][deviceid] = message;
-
-                send_to_device(mtx::events::to_string(event_type), txid, j, callback);
-        }
+          ErrCallback callback);
 
         //
         // Group related endpoints.
@@ -598,119 +592,6 @@ private:
 }
 }
 
-template<class Request, class Response>
-void
-mtx::http::Client::post(const std::string &endpoint,
-                        const Request &req,
-                        Callback<Response> callback,
-                        bool requires_auth,
-                        const std::string &content_type)
-{
-        post(
-          endpoint,
-          client::utils::serialize(req),
-          prepare_callback<Response>(
-            [callback](const Response &res, HeaderFields, RequestErr err) { callback(res, err); }),
-          requires_auth,
-          content_type);
-}
-
-// put function for the PUT HTTP requests that send responses
-template<class Request, class Response>
-void
-mtx::http::Client::put(const std::string &endpoint,
-                       const Request &req,
-                       Callback<Response> callback,
-                       bool requires_auth)
-{
-        put(
-          endpoint,
-          client::utils::serialize(req),
-          prepare_callback<Response>(
-            [callback](const Response &res, HeaderFields, RequestErr err) { callback(res, err); }),
-          requires_auth);
-}
-
-// provides PUT functionality for the endpoints which dont respond with a body
-template<class Request>
-void
-mtx::http::Client::put(const std::string &endpoint,
-                       const Request &req,
-                       ErrCallback callback,
-                       bool requires_auth)
-{
-        mtx::http::Client::put<Request, mtx::responses::Empty>(
-          endpoint,
-          req,
-          [callback](const mtx::responses::Empty, RequestErr err) { callback(err); },
-          requires_auth);
-}
-
-template<class Response>
-void
-mtx::http::Client::get(const std::string &endpoint,
-                       HeadersCallback<Response> callback,
-                       bool requires_auth,
-                       const std::string &endpoint_namespace)
-{
-        get(endpoint, prepare_callback<Response>(callback), requires_auth, endpoint_namespace);
-}
-
-template<class Response>
-mtx::http::TypeErasedCallback
-mtx::http::Client::prepare_callback(HeadersCallback<Response> callback)
-{
-        auto type_erased_cb = [callback](HeaderFields headers,
-                                         const std::string &body,
-                                         const boost::system::error_code &err_code,
-                                         boost::beast::http::status status_code) {
-                Response response_data;
-                mtx::http::ClientError client_error;
-
-                if (err_code) {
-                        client_error.error_code = err_code;
-                        return callback(response_data, headers, client_error);
-                }
-
-                // We only count 2xx status codes as success.
-                if (static_cast<int>(status_code) < 200 || static_cast<int>(status_code) >= 300) {
-                        client_error.status_code = status_code;
-
-                        // Try to parse the response in case we have an endpoint that
-                        // doesn't return an error struct for non 200 requests.
-                        try {
-                                response_data = client::utils::deserialize<Response>(body);
-                        } catch (const nlohmann::json::exception &e) {
-                        }
-
-                        // The homeserver should return an error struct.
-                        try {
-                                nlohmann::json json_error       = json::parse(body);
-                                mtx::errors::Error matrix_error = json_error;
-
-                                client_error.matrix_error = matrix_error;
-                                return callback(response_data, headers, client_error);
-                        } catch (const nlohmann::json::exception &e) {
-                                client_error.parse_error = std::string(e.what()) + ": " + body;
-
-                                return callback(response_data, headers, client_error);
-                        }
-                }
-
-                // If we reach that point we most likely have a valid output from the
-                // homeserver.
-                try {
-                        auto res = client::utils::deserialize<Response>(body);
-                        callback(std::move(res), headers, {});
-                } catch (const nlohmann::json::exception &e) {
-                        client_error.parse_error = std::string(e.what()) + ": " + body;
-                        callback(response_data, headers, client_error);
-                }
-        };
-
-        return type_erased_cb;
-}
-
 template<class Payload>
 void
 mtx::http::Client::send_room_message(const std::string &room_id,
diff --git a/include/mtxclient/http/client_impl.hpp b/include/mtxclient/http/client_impl.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..067877fe998ef3b7d875704f878e9cd0d3d01d09
--- /dev/null
+++ b/include/mtxclient/http/client_impl.hpp
@@ -0,0 +1,170 @@
+#pragma once
+
+#include "client.hpp"
+#include "mtxclient/utils.hpp" // for random_token, url_encode, des...
+
+#include <nlohmann/json.hpp>
+
+namespace mtx {
+namespace client {
+namespace utils {
+template<class T>
+inline T
+deserialize(const std::string &data)
+{
+        return nlohmann::json::parse(data);
+}
+
+template<>
+inline std::string
+deserialize<std::string>(const std::string &data)
+{
+        return data;
+}
+
+template<class T>
+inline std::string
+serialize(const T &obj)
+{
+        return nlohmann::json(obj).dump();
+}
+
+template<>
+inline std::string
+serialize<std::string>(const std::string &obj)
+{
+        return obj;
+}
+}
+}
+}
+template<class Request, class Response>
+void
+mtx::http::Client::post(const std::string &endpoint,
+                        const Request &req,
+                        Callback<Response> callback,
+                        bool requires_auth,
+                        const std::string &content_type)
+{
+        post(
+          endpoint,
+          client::utils::serialize(req),
+          prepare_callback<Response>(
+            [callback](const Response &res, HeaderFields, RequestErr err) { callback(res, err); }),
+          requires_auth,
+          content_type);
+}
+
+// put function for the PUT HTTP requests that send responses
+template<class Request, class Response>
+void
+mtx::http::Client::put(const std::string &endpoint,
+                       const Request &req,
+                       Callback<Response> callback,
+                       bool requires_auth)
+{
+        put(
+          endpoint,
+          client::utils::serialize(req),
+          prepare_callback<Response>(
+            [callback](const Response &res, HeaderFields, RequestErr err) { callback(res, err); }),
+          requires_auth);
+}
+
+// provides PUT functionality for the endpoints which dont respond with a body
+template<class Request>
+void
+mtx::http::Client::put(const std::string &endpoint,
+                       const Request &req,
+                       ErrCallback callback,
+                       bool requires_auth)
+{
+        mtx::http::Client::put<Request, mtx::responses::Empty>(
+          endpoint,
+          req,
+          [callback](const mtx::responses::Empty, RequestErr err) { callback(err); },
+          requires_auth);
+}
+
+template<class Response>
+void
+mtx::http::Client::get(const std::string &endpoint,
+                       HeadersCallback<Response> callback,
+                       bool requires_auth,
+                       const std::string &endpoint_namespace)
+{
+        get(endpoint, prepare_callback<Response>(callback), requires_auth, endpoint_namespace);
+}
+
+template<class Response>
+mtx::http::TypeErasedCallback
+mtx::http::Client::prepare_callback(HeadersCallback<Response> callback)
+{
+        auto type_erased_cb = [callback](HeaderFields headers,
+                                         const std::string &body,
+                                         const boost::system::error_code &err_code,
+                                         boost::beast::http::status status_code) {
+                Response response_data;
+                mtx::http::ClientError client_error;
+
+                if (err_code) {
+                        client_error.error_code = err_code;
+                        return callback(response_data, headers, client_error);
+                }
+
+                // We only count 2xx status codes as success.
+                if (static_cast<int>(status_code) < 200 || static_cast<int>(status_code) >= 300) {
+                        client_error.status_code = status_code;
+
+                        // Try to parse the response in case we have an endpoint that
+                        // doesn't return an error struct for non 200 requests.
+                        try {
+                                response_data = client::utils::deserialize<Response>(body);
+                        } catch (const nlohmann::json::exception &e) {
+                        }
+
+                        // The homeserver should return an error struct.
+                        try {
+                                nlohmann::json json_error       = json::parse(body);
+                                mtx::errors::Error matrix_error = json_error;
+
+                                client_error.matrix_error = matrix_error;
+                                return callback(response_data, headers, client_error);
+                        } catch (const nlohmann::json::exception &e) {
+                                client_error.parse_error = std::string(e.what()) + ": " + body;
+
+                                return callback(response_data, headers, client_error);
+                        }
+                }
+
+                // If we reach that point we most likely have a valid output from the
+                // homeserver.
+                try {
+                        auto res = client::utils::deserialize<Response>(body);
+                        callback(std::move(res), headers, {});
+                } catch (const nlohmann::json::exception &e) {
+                        client_error.parse_error = std::string(e.what()) + ": " + body;
+                        callback(response_data, headers, client_error);
+                }
+        };
+
+        return type_erased_cb;
+}
+
+template<typename EventContent>
+void
+mtx::http::Client::send_to_device(
+  const std::string &txid,
+  const std::map<mtx::identifiers::User, std::map<std::string, EventContent>> &messages,
+  ErrCallback callback)
+{
+        constexpr auto event_type = mtx::events::to_device_content_to_type<EventContent>;
+        static_assert(event_type != mtx::events::EventType::Unsupported);
+
+        json j;
+        for (const auto &[user, deviceToMessage] : messages)
+                for (const auto &[deviceid, message] : deviceToMessage)
+                        j["messages"][user.to_string()][deviceid] = message;
+
+        send_to_device(mtx::events::to_string(event_type), txid, j, callback);
+}
diff --git a/include/mtxclient/http/session.hpp b/include/mtxclient/http/session.hpp
index b5175b10a35cc28f6bbf77e1dead987302422644..928de06b97d34ee646e9cbe59d8fb842f29470ab 100644
--- a/include/mtxclient/http/session.hpp
+++ b/include/mtxclient/http/session.hpp
@@ -4,6 +4,8 @@
 #include <boost/asio/ssl.hpp>
 #include <boost/beast.hpp>
 
+#include <nlohmann/json.hpp>
+
 #include "mtxclient/http/errors.hpp"
 #include "mtxclient/utils.hpp"
 
diff --git a/include/mtxclient/utils.hpp b/include/mtxclient/utils.hpp
index 3af32d8a86299f0eb70bd3db20aba765651cb718..ee98310b11f29f8574e1ca70a259d22000e0dc05 100644
--- a/include/mtxclient/utils.hpp
+++ b/include/mtxclient/utils.hpp
@@ -3,8 +3,7 @@
 #include <boost/iostreams/device/array.hpp>
 #include <iosfwd>
 #include <map>
-
-#include <nlohmann/json.hpp>
+#include <string>
 
 namespace mtx {
 namespace client {
@@ -42,63 +41,6 @@ decompress(const boost::iostreams::array_source &src, const std::string &type) n
 //! URL-encode the input string.
 std::string
 url_encode(const std::string &s) noexcept;
-
-template<class T>
-inline T
-deserialize(const std::string &data)
-{
-        return nlohmann::json::parse(data);
-}
-
-template<>
-inline std::string
-deserialize<std::string>(const std::string &data)
-{
-        return data;
-}
-
-template<class T>
-inline std::string
-serialize(const T &obj)
-{
-        return nlohmann::json(obj).dump();
-}
-
-template<>
-inline std::string
-serialize<std::string>(const std::string &obj)
-{
-        return obj;
-}
-
-template<class T, class Name>
-class strong_type
-{
-public:
-        strong_type() = default;
-        explicit strong_type(const T &value)
-          : value_(value)
-        {}
-        explicit strong_type(T &&value)
-          : value_(std::forward<T>(value))
-        {}
-
-        operator T &() noexcept { return value_; }
-        constexpr operator const T &() const noexcept { return value_; }
-
-        T &get() { return value_; }
-        T const &get() const { return value_; }
-
-private:
-        T value_;
-};
-
-// Macro for concisely defining a strong type
-#define STRONG_TYPE(type_name, value_type)                                                         \
-        struct type_name : mtx::client::utils::strong_type<value_type, type_name>                  \
-        {                                                                                          \
-                using strong_type::strong_type;                                                    \
-        };
 }
 }
 }
diff --git a/lib/crypto/client.cpp b/lib/crypto/client.cpp
index efef3e56cf998c8f0c7f491bced2cde44adb7b0c..baed4e35b201abeace1b505923eff3013ee411a1 100644
--- a/lib/crypto/client.cpp
+++ b/lib/crypto/client.cpp
@@ -1,5 +1,7 @@
 #include <iostream>
 
+#include <nlohmann/json.hpp>
+
 #include <openssl/aes.h>
 #include <openssl/sha.h>
 
@@ -107,11 +109,11 @@ OlmClient::sign_one_time_key(const std::string &key)
         return sign_message(j.dump());
 }
 
-std::map<std::string, json>
+std::map<std::string, mtx::requests::SignedOneTimeKey>
 OlmClient::sign_one_time_keys(const OneTimeKeys &keys)
 {
         // Sign & append the one time keys.
-        std::map<std::string, json> signed_one_time_keys;
+        std::map<std::string, mtx::requests::SignedOneTimeKey> signed_one_time_keys;
         for (const auto &elem : keys.curve25519) {
                 const auto key_id       = elem.first;
                 const auto one_time_key = elem.second;
@@ -119,17 +121,19 @@ OlmClient::sign_one_time_keys(const OneTimeKeys &keys)
                 auto sig = sign_one_time_key(one_time_key);
 
                 signed_one_time_keys["signed_curve25519:" + key_id] =
-                  signed_one_time_key_json(one_time_key, sig);
+                  signed_one_time_key(one_time_key, sig);
         }
 
         return signed_one_time_keys;
 }
 
-json
-OlmClient::signed_one_time_key_json(const std::string &key, const std::string &signature)
+mtx::requests::SignedOneTimeKey
+OlmClient::signed_one_time_key(const std::string &key, const std::string &signature)
 {
-        return json{{"key", key},
-                    {"signatures", {{user_id_, {{"ed25519:" + device_id_, signature}}}}}};
+        mtx::requests::SignedOneTimeKey sign{};
+        sign.key        = key;
+        sign.signatures = {{user_id_, {{"ed25519:" + device_id_, signature}}}};
+        return sign;
 }
 
 mtx::requests::UploadKeys
@@ -159,7 +163,9 @@ OlmClient::create_upload_keys_request(const mtx::crypto::OneTimeKeys &one_time_k
                 return req;
 
         // Sign & append the one time keys.
-        req.one_time_keys = sign_one_time_keys(one_time_keys);
+        auto temp = sign_one_time_keys(one_time_keys);
+        for (const auto &[key_id, key] : temp)
+                req.one_time_keys[key_id] = key;
 
         return req;
 }
@@ -639,8 +645,6 @@ mtx::crypto::verify_identity_signature(const DeviceKeys &device_keys,
                                        const DeviceId &device_id,
                                        const UserId &user_id)
 {
-        using namespace client::utils;
-
         try {
                 const auto sign_key_id = "ed25519:" + device_id.get();
                 const auto signing_key = device_keys.keys.at(sign_key_id);
@@ -665,8 +669,6 @@ mtx::crypto::ed25519_verify_signature(std::string signing_key,
                                       nlohmann::json obj,
                                       std::string signature)
 {
-        using namespace client::utils;
-
         try {
                 if (signature.empty())
                         return false;
diff --git a/lib/crypto/types.cpp b/lib/crypto/types.cpp
index 3543b5e6964ae63eb1aed550ac1cbe975b2e328e..7e324d0c1b9eb06e0f78bd86446c22d137ea6a38 100644
--- a/lib/crypto/types.cpp
+++ b/lib/crypto/types.cpp
@@ -1,5 +1,7 @@
 #include "mtxclient/crypto/types.hpp"
 
+#include <nlohmann/json.hpp>
+
 namespace mtx {
 namespace crypto {
 
diff --git a/lib/crypto/utils.cpp b/lib/crypto/utils.cpp
index 1c211c8f56dbc47f0d35daf7aad8d31dbd7d803b..0913c7732776be78d8cee3677b4c56c0100756e0 100644
--- a/lib/crypto/utils.cpp
+++ b/lib/crypto/utils.cpp
@@ -1,5 +1,7 @@
 #include "mtxclient/crypto/utils.hpp"
 
+#include <nlohmann/json.hpp>
+
 #include <openssl/aes.h>
 #include <openssl/evp.h>
 #include <openssl/hmac.h>
diff --git a/lib/http/client.cpp b/lib/http/client.cpp
index 8e8d0ff52fdd1a725801a0a0eb23d08e1fc410bf..d3f29975aba97e6f03a392c44dd3d955bb25562b 100644
--- a/lib/http/client.cpp
+++ b/lib/http/client.cpp
@@ -1,17 +1,18 @@
 #include "mtxclient/http/client.hpp"
+#include "mtxclient/http/client_impl.hpp"
 
 #include <mutex>
 #include <thread>
 
+#include <nlohmann/json.hpp>
+
 #include <boost/algorithm/string.hpp>
-#include <boost/bind.hpp>
 #include <boost/utility/typed_in_place_factory.hpp>
 
-#include <boost/asio.hpp>
-#include <boost/asio/ssl.hpp>
-#include <boost/beast.hpp>
+#include <boost/asio/ssl/context.hpp>
+#include <boost/beast/http/message.hpp>
 #include <boost/iostreams/stream.hpp>
-#include <boost/signals2.hpp>
+#include <boost/signals2/signal.hpp>
 #include <boost/signals2/signal_type.hpp>
 #include <boost/thread/thread.hpp>
 
@@ -45,7 +46,7 @@ Client::Client(const std::string &server, uint16_t port)
   , p{new ClientPrivate}
 {
         using namespace boost::asio;
-        const auto threads_num = std::max(1U, std::thread::hardware_concurrency());
+        const auto threads_num = std::min(8U, std::max(1U, std::thread::hardware_concurrency()));
 
         for (unsigned int i = 0; i < threads_num; ++i)
                 p->thread_group_.add_thread(new boost::thread([this]() { p->ios_.run(); }));
@@ -599,19 +600,19 @@ Client::create_room(const mtx::requests::CreateRoom &room_options,
 }
 
 void
-Client::join_room(const std::string &room, Callback<nlohmann::json> callback)
+Client::join_room(const std::string &room, Callback<mtx::responses::RoomId> callback)
 {
         auto api_path = "/client/r0/join/" + mtx::client::utils::url_encode(room);
 
-        post<std::string, nlohmann::json>(api_path, "{}", callback);
+        post<std::string, mtx::responses::RoomId>(api_path, "{}", callback);
 }
 
 void
-Client::leave_room(const std::string &room_id, Callback<nlohmann::json> callback)
+Client::leave_room(const std::string &room_id, Callback<mtx::responses::Empty> callback)
 {
         auto api_path = "/client/r0/rooms/" + mtx::client::utils::url_encode(room_id) + "/leave";
 
-        post<std::string, nlohmann::json>(api_path, "{}", callback);
+        post<std::string, mtx::responses::Empty>(api_path, "{}", callback);
 }
 
 void
@@ -1171,3 +1172,79 @@ Client::get_turn_server(Callback<mtx::responses::TurnServer> cb)
                                              HeaderFields,
                                              RequestErr err) { cb(res, err); });
 }
+
+// Template instantiations for the various send functions
+
+#define MTXCLIENT_SEND_STATE_EVENT(Content)                                                        \
+        template void mtx::http::Client::send_state_event<mtx::events::state::Content>(            \
+          const std::string &,                                                                     \
+          const std::string &state_key,                                                            \
+          const mtx::events::state::Content &,                                                     \
+          Callback<mtx::responses::EventId> cb);
+
+MTXCLIENT_SEND_STATE_EVENT(Aliases)
+MTXCLIENT_SEND_STATE_EVENT(Avatar)
+MTXCLIENT_SEND_STATE_EVENT(CanonicalAlias)
+MTXCLIENT_SEND_STATE_EVENT(Create)
+MTXCLIENT_SEND_STATE_EVENT(Encryption)
+MTXCLIENT_SEND_STATE_EVENT(GuestAccess)
+MTXCLIENT_SEND_STATE_EVENT(HistoryVisibility)
+MTXCLIENT_SEND_STATE_EVENT(JoinRules)
+MTXCLIENT_SEND_STATE_EVENT(Member)
+MTXCLIENT_SEND_STATE_EVENT(Name)
+MTXCLIENT_SEND_STATE_EVENT(PinnedEvents)
+MTXCLIENT_SEND_STATE_EVENT(PowerLevels)
+MTXCLIENT_SEND_STATE_EVENT(Tombstone)
+MTXCLIENT_SEND_STATE_EVENT(Topic)
+
+#define MTXCLIENT_SEND_ROOM_MESSAGE(Content)                                                       \
+        template void mtx::http::Client::send_room_message<Content>(                               \
+          const std::string &,                                                                     \
+          const std::string &,                                                                     \
+          const Content &,                                                                         \
+          Callback<mtx::responses::EventId> cb);                                                   \
+        template void mtx::http::Client::send_room_message<Content>(                               \
+          const std::string &, const Content &, Callback<mtx::responses::EventId> cb);
+
+MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::Encrypted)
+MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::StickerImage)
+MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::Reaction)
+MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::Audio)
+MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::Emote)
+MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::File)
+MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::Image)
+MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::Notice)
+MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::Text)
+MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::Video)
+// MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::KeyVerificationRequest)
+// MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::KeyVerificationStart)
+// MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::KeyVerificationReady)
+// MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::KeyVerificationDone)
+// MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::KeyVerificationAccept)
+// MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::KeyVerificationCancel)
+// MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::KeyVerificationKey)
+// MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::KeyVerificationMac)
+MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::CallInvite)
+MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::CallCandidates)
+MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::CallAnswer)
+MTXCLIENT_SEND_ROOM_MESSAGE(mtx::events::msg::CallHangUp)
+
+#define MTXCLIENT_SEND_TO_DEVICE(Content)                                                          \
+        template void mtx::http::Client::send_to_device<Content>(                                  \
+          const std::string &txid,                                                                 \
+          const std::map<mtx::identifiers::User, std::map<std::string, Content>> &messages,        \
+          ErrCallback callback);
+
+MTXCLIENT_SEND_TO_DEVICE(mtx::events::msg::RoomKey)
+MTXCLIENT_SEND_TO_DEVICE(mtx::events::msg::ForwardedRoomKey)
+MTXCLIENT_SEND_TO_DEVICE(mtx::events::msg::KeyRequest)
+MTXCLIENT_SEND_TO_DEVICE(mtx::events::msg::OlmEncrypted)
+MTXCLIENT_SEND_TO_DEVICE(mtx::events::msg::Encrypted)
+MTXCLIENT_SEND_TO_DEVICE(mtx::events::msg::KeyVerificationRequest)
+MTXCLIENT_SEND_TO_DEVICE(mtx::events::msg::KeyVerificationStart)
+MTXCLIENT_SEND_TO_DEVICE(mtx::events::msg::KeyVerificationReady)
+MTXCLIENT_SEND_TO_DEVICE(mtx::events::msg::KeyVerificationDone)
+MTXCLIENT_SEND_TO_DEVICE(mtx::events::msg::KeyVerificationAccept)
+MTXCLIENT_SEND_TO_DEVICE(mtx::events::msg::KeyVerificationCancel)
+MTXCLIENT_SEND_TO_DEVICE(mtx::events::msg::KeyVerificationKey)
+MTXCLIENT_SEND_TO_DEVICE(mtx::events::msg::KeyVerificationMac)
diff --git a/lib/structs/errors.cpp b/lib/structs/errors.cpp
index 0fa246ca9e494f3a6127404e4ad0e6d8fc24efb5..b9ac9dcd74297a9e632474dacdd3244c8dfc91eb 100644
--- a/lib/structs/errors.cpp
+++ b/lib/structs/errors.cpp
@@ -91,6 +91,13 @@ from_string(const std::string &code)
                 return ErrorCode::M_UNRECOGNIZED;
 }
 
+void
+from_json(const nlohmann::json &obj, LightweightError &error)
+{
+        error.errcode = from_string(obj.value("errcode", ""));
+        error.error   = obj.value("error", "");
+}
+
 void
 from_json(const nlohmann::json &obj, Error &error)
 {
diff --git a/lib/structs/events.cpp b/lib/structs/events.cpp
index 21fefbaf4233556fdc0a72e9492a69bb567fd1e5..9be4a2241fe29eb958e2ebdb2969124c7e9358f8 100644
--- a/lib/structs/events.cpp
+++ b/lib/structs/events.cpp
@@ -1,5 +1,7 @@
 #include "mtx/events.hpp"
 
+#include <nlohmann/json.hpp>
+
 using json = nlohmann::json;
 
 namespace mtx {
diff --git a/lib/structs/events/collections.cpp b/lib/structs/events/collections.cpp
index 23eb6e5ca70b6e48029636b48aca1385fcb14cc7..5c942216a5744577c74bdfd18108d807ec791e18 100644
--- a/lib/structs/events/collections.cpp
+++ b/lib/structs/events/collections.cpp
@@ -1,6 +1,93 @@
 #include "mtx/events/collections.hpp"
+#include "mtx/events_impl.hpp"
 #include "mtx/log.hpp"
 
+#include <nlohmann/json.hpp>
+
+namespace mtx::events {
+using namespace mtx::events::collections;
+
+#define MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(EventType, Content)                                   \
+        template void to_json<Content>(nlohmann::json &, const EventType<Content> &);              \
+        template void from_json<Content>(const nlohmann::json &, EventType<Content> &);
+
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::Aliases)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::Avatar)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::CanonicalAlias)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::Create)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::Encryption)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::GuestAccess)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::HistoryVisibility)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::JoinRules)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::Member)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::Name)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::PinnedEvents)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::PowerLevels)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::Tombstone)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::Topic)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, msgs::Redacted)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::EncryptedEvent, msgs::Encrypted)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::EncryptedEvent, msgs::OlmEncrypted)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::StickerImage)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::Reaction)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::Redacted)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::Audio)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::Emote)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::File)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::Image)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::Notice)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::Text)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::Video)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::KeyVerificationRequest)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::KeyVerificationStart)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::KeyVerificationReady)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::KeyVerificationDone)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::KeyVerificationAccept)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::KeyVerificationCancel)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::KeyVerificationKey)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::KeyVerificationMac)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::CallInvite)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::CallCandidates)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::CallAnswer)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RoomEvent, msgs::CallHangUp)
+
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::Aliases)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::Avatar)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::CanonicalAlias)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::Create)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::Encryption)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::GuestAccess)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::HistoryVisibility)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::JoinRules)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::Member)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::Name)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::PinnedEvents)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::PowerLevels)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::Tombstone)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::Topic)
+
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::DeviceEvent, msgs::Encrypted)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::DeviceEvent, msgs::OlmEncrypted)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::DeviceEvent, msgs::KeyVerificationRequest)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::DeviceEvent, msgs::KeyVerificationStart)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::DeviceEvent, msgs::KeyVerificationReady)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::DeviceEvent, msgs::KeyVerificationDone)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::DeviceEvent, msgs::KeyVerificationAccept)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::DeviceEvent, msgs::KeyVerificationCancel)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::DeviceEvent, msgs::KeyVerificationKey)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::DeviceEvent, msgs::KeyVerificationMac)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::DeviceEvent, msgs::RoomKey)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::DeviceEvent, msgs::ForwardedRoomKey)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::DeviceEvent, msgs::KeyRequest)
+
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::Event, account_data::Tags)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::Event, pushrules::GlobalRuleset)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::Event, account_data::nheko_extensions::HiddenEvents)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::Event, presence::Presence)
+
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::RedactionEvent, msg::Redaction)
+}
+
 namespace mtx::events::collections {
 void
 from_json(const json &obj, TimelineEvent &e)
diff --git a/lib/structs/events/encrypted.cpp b/lib/structs/events/encrypted.cpp
index 964d36429598b3f7ddbbafb146f0318ffe12309b..ade1abd6cfc7b804d63a781b9ac5fd143777b204 100644
--- a/lib/structs/events/encrypted.cpp
+++ b/lib/structs/events/encrypted.cpp
@@ -1,5 +1,7 @@
 #include <string>
 
+#include <nlohmann/json.hpp>
+
 #include "mtx/events/encrypted.hpp"
 
 static constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2";
diff --git a/lib/structs/events/nheko_extensions/hidden_events.cpp b/lib/structs/events/nheko_extensions/hidden_events.cpp
index 7f40629ba90601d1bdb35ce7f057ea5a0784fb0d..03cb68cee46ac24978c833a3bf1e5852fa94c38c 100644
--- a/lib/structs/events/nheko_extensions/hidden_events.cpp
+++ b/lib/structs/events/nheko_extensions/hidden_events.cpp
@@ -1,5 +1,7 @@
 #include "mtx/events/nheko_extensions/hidden_events.hpp"
 
+#include <nlohmann/json.hpp>
+
 namespace mtx {
 namespace events {
 namespace account_data {
diff --git a/lib/structs/requests.cpp b/lib/structs/requests.cpp
index ebeda73d151d0925f508581298a5f6ae398d5183..c36ee42b3bb14442819a219b349707ba37168bf3 100644
--- a/lib/structs/requests.cpp
+++ b/lib/structs/requests.cpp
@@ -2,6 +2,7 @@
 #include "mtx/events/collections.hpp"
 #include "mtx/events/encrypted.hpp"
 #include <iostream>
+#include <nlohmann/json.hpp>
 
 using json = nlohmann::json;
 using namespace mtx::events::collections;
@@ -110,6 +111,13 @@ to_json(json &obj, const TypingNotification &request)
         obj["timeout"] = request.timeout;
 }
 
+void
+to_json(json &obj, const SignedOneTimeKey &request)
+{
+        obj["key"]        = request.key;
+        obj["signatures"] = request.signatures;
+}
+
 void
 to_json(json &obj, const UploadKeys &request)
 {
@@ -118,8 +126,17 @@ to_json(json &obj, const UploadKeys &request)
         if (!request.device_keys.user_id.empty())
                 obj["device_keys"] = request.device_keys;
 
-        if (!request.one_time_keys.empty())
-                obj["one_time_keys"] = request.one_time_keys;
+        for (const auto &[key_id, key] : request.one_time_keys) {
+                obj["one_time_keys"][key_id] =
+                  std::visit([](const auto &e) { return json(e); }, key);
+        }
+}
+
+void
+to_json(json &obj, const ClaimKeys &request)
+{
+        obj["timeout"]       = request.timeout;
+        obj["one_time_keys"] = request.one_time_keys;
 }
 
 void
diff --git a/lib/structs/responses/common.cpp b/lib/structs/responses/common.cpp
index 1b7017ad9b06399bc7c74e809bed81061b3b96c8..23e9790d64451d231eef99e17ea2dda388584aa4 100644
--- a/lib/structs/responses/common.cpp
+++ b/lib/structs/responses/common.cpp
@@ -1,5 +1,7 @@
 #include "mtx/responses/common.hpp"
 
+#include <nlohmann/json.hpp>
+
 #include "mtx/events.hpp"
 #include "mtx/events/aliases.hpp"
 #include "mtx/events/avatar.hpp"
@@ -33,6 +35,12 @@ from_json(const nlohmann::json &obj, GroupId &response)
         response.group_id = obj.at("group_id");
 }
 
+void
+from_json(const nlohmann::json &obj, RoomId &response)
+{
+        response.room_id = obj.at("room_id");
+}
+
 void
 from_json(const nlohmann::json &obj, EventId &response)
 {
@@ -48,7 +56,7 @@ from_json(const nlohmann::json &obj, FilterId &response)
 namespace utils {
 
 void
-log_error(json::exception &err, const json &event)
+log_error(std::exception &err, const json &event)
 {
         std::cout << err.what() << std::endl;
         std::cout << event.dump(2) << std::endl;
diff --git a/lib/structs/responses/crypto.cpp b/lib/structs/responses/crypto.cpp
index 067aa6be868e2e8d09f1c1191770502ee75c0bd6..9cd1475191b147f8c86d07f3602c92d405a409e8 100644
--- a/lib/structs/responses/crypto.cpp
+++ b/lib/structs/responses/crypto.cpp
@@ -117,7 +117,7 @@ void
 from_json(const nlohmann::json &obj, BackupVersion &response)
 {
         response.algorithm = obj.at("algorithm");
-        response.auth_data = obj.at("auth_data");
+        response.auth_data = obj.at("auth_data").dump();
         response.count     = obj.at("count");
         response.etag =
           obj.at("etag").dump(); // workaround, since synapse 1.15.1 and older sends this as integer
@@ -127,7 +127,7 @@ void
 to_json(nlohmann::json &obj, const BackupVersion &response)
 {
         obj["algorithm"] = response.algorithm;
-        obj["auth_data"] = response.auth_data;
+        obj["auth_data"] = nlohmann::json::parse(response.auth_data);
         obj["count"]     = response.count;
         obj["etag"]      = response.etag;
         obj["version"]   = response.version;
diff --git a/lib/structs/responses/empty.cpp b/lib/structs/responses/empty.cpp
index c7bb3ff5a370b012ac343c79a272897f6b0a5ce3..0508dc8b397e7c7247e53531bc69c473c74c5d4b 100644
--- a/lib/structs/responses/empty.cpp
+++ b/lib/structs/responses/empty.cpp
@@ -1,13 +1,11 @@
 #include "mtx/responses/empty.hpp"
 
-using json = nlohmann::json;
-
 namespace mtx {
 namespace responses {
 
 // Provides a deserialization function to use when empty responses are returned from the server
 void
-from_json(const json &, Empty &)
+from_json(const nlohmann::json &, Empty &)
 {}
 }
 }
diff --git a/lib/structs/responses/messages.cpp b/lib/structs/responses/messages.cpp
index a116f2327de2cd93927a3027f2a3f500feb9ce3b..3f6a464bd7304c9679a3195c3031114351589df7 100644
--- a/lib/structs/responses/messages.cpp
+++ b/lib/structs/responses/messages.cpp
@@ -1,6 +1,8 @@
 #include "mtx/responses/messages.hpp"
 #include "mtx/responses/common.hpp"
 
+#include <nlohmann/json.hpp>
+
 using json = nlohmann::json;
 
 namespace mtx {
diff --git a/lib/structs/responses/notifications.cpp b/lib/structs/responses/notifications.cpp
index a692c85a17ca0735cd1834489cd21c74e172b15a..ec13c00fde0b5d6b29e9fc6996460606ff45c675 100644
--- a/lib/structs/responses/notifications.cpp
+++ b/lib/structs/responses/notifications.cpp
@@ -1,6 +1,8 @@
 #include "mtx/responses/notifications.hpp"
 #include "mtx/responses/common.hpp"
 
+#include <nlohmann/json.hpp>
+
 using json = nlohmann::json;
 
 namespace mtx {
@@ -9,7 +11,7 @@ namespace responses {
 void
 from_json(const json &obj, Notification &res)
 {
-        res.actions = obj.at("actions");
+        res.actions = obj.at("actions").get<decltype(res.actions)>();
         res.read    = obj.at("read");
         res.room_id = obj.at("room_id");
         res.ts      = obj.at("ts");
diff --git a/lib/structs/responses/sync.cpp b/lib/structs/responses/sync.cpp
index 66d0fb04964c38031895f5bd0464b3298b10222b..8ba836a4942eea485b65b893520cfedeabc17c41 100644
--- a/lib/structs/responses/sync.cpp
+++ b/lib/structs/responses/sync.cpp
@@ -3,6 +3,8 @@
 #include "mtx/log.hpp"
 #include "mtx/responses/common.hpp"
 
+#include <nlohmann/json.hpp>
+
 #include <variant>
 
 using json = nlohmann::json;
diff --git a/lib/structs/user_interactive.cpp b/lib/structs/user_interactive.cpp
index 93ccc93db9358d3467b970f0542b33b064fe37c1..af6861065616db90e981166f63842a8a5edd8b2d 100644
--- a/lib/structs/user_interactive.cpp
+++ b/lib/structs/user_interactive.cpp
@@ -1,5 +1,7 @@
 #include "mtx/user_interactive.hpp"
 
+#include <nlohmann/json.hpp>
+
 namespace mtx::user_interactive {
 void
 from_json(const nlohmann::json &obj, OAuth2Params &params)
@@ -50,7 +52,7 @@ from_json(const nlohmann::json &obj, Unauthorized &u)
                         else if (e.key() == auth_types::oauth2)
                                 u.params.emplace(e.key(), e.value().get<OAuth2Params>());
                         else
-                                u.params.emplace(e.key(), e.value());
+                                u.params.emplace(e.key(), e.value().dump());
                 }
         }
 }
diff --git a/tests/client_api.cpp b/tests/client_api.cpp
index 4913952c41760d51fea4ac087f41aacd30fb98c3..1d4dab26ceee7464d6398b7540502eb8ef444876 100644
--- a/tests/client_api.cpp
+++ b/tests/client_api.cpp
@@ -5,6 +5,8 @@
 
 #include <gtest/gtest.h>
 
+#include <nlohmann/json.hpp>
+
 #include "mtx/events/collections.hpp"
 #include "mtx/events/encrypted.hpp"
 #include "mtx/requests.hpp"
@@ -444,11 +446,13 @@ TEST(ClientAPI, CreateRoomInvites)
                 check_error(err);
                 auto room_id = res.room_id.to_string();
 
-                bob->join_room(room_id,
-                               [](const nlohmann::json &, RequestErr err) { check_error(err); });
+                bob->join_room(room_id, [](const mtx::responses::RoomId &, RequestErr err) {
+                        check_error(err);
+                });
 
-                carl->join_room(room_id,
-                                [](const nlohmann::json &, RequestErr err) { check_error(err); });
+                carl->join_room(room_id, [](const mtx::responses::RoomId &, RequestErr err) {
+                        check_error(err);
+                });
         });
 
         alice->close();
@@ -486,20 +490,23 @@ TEST(ClientAPI, JoinRoom)
                   check_error(err);
                   auto room_id = res.room_id.to_string();
 
-                  bob->join_room(room_id,
-                                 [](const nlohmann::json &, RequestErr err) { check_error(err); });
+                  bob->join_room(room_id, [](const mtx::responses::RoomId &, RequestErr err) {
+                          check_error(err);
+                  });
 
                   using namespace mtx::identifiers;
-                  bob->join_room(
-                    "!random_room_id:localhost", [](const nlohmann::json &, RequestErr err) {
-                            ASSERT_TRUE(err);
-                            EXPECT_EQ(mtx::errors::to_string(err->matrix_error.errcode),
-                                      "M_UNKNOWN");
-                    });
+                  bob->join_room("!random_room_id:localhost",
+                                 [](const mtx::responses::RoomId &, RequestErr err) {
+                                         ASSERT_TRUE(err);
+                                         EXPECT_EQ(
+                                           mtx::errors::to_string(err->matrix_error.errcode),
+                                           "M_UNKNOWN");
+                                 });
 
                   // Join the room using an alias.
-                  bob->join_room("#" + alias + ":localhost",
-                                 [](const nlohmann::json &, RequestErr err) { check_error(err); });
+                  bob->join_room(
+                    "#" + alias + ":localhost",
+                    [](const mtx::responses::RoomId &, RequestErr err) { check_error(err); });
           });
 
         alice->close();
@@ -531,18 +538,18 @@ TEST(ClientAPI, LeaveRoom)
                 auto room_id = res.room_id;
 
                 bob->join_room(res.room_id.to_string(),
-                               [room_id, bob](const nlohmann::json &, RequestErr err) {
+                               [room_id, bob](const mtx::responses::RoomId &, RequestErr err) {
                                        check_error(err);
 
                                        bob->leave_room(room_id.to_string(),
-                                                       [](const nlohmann::json &, RequestErr err) {
+                                                       [](mtx::responses::Empty, RequestErr err) {
                                                                check_error(err);
                                                        });
                                });
         });
 
         // Trying to leave a non-existent room should fail.
-        bob->leave_room("!random_room_id:localhost", [](const nlohmann::json &, RequestErr err) {
+        bob->leave_room("!random_room_id:localhost", [](mtx::responses::Empty, RequestErr err) {
                 ASSERT_TRUE(err);
                 EXPECT_EQ(mtx::errors::to_string(err->matrix_error.errcode), "M_UNKNOWN");
                 EXPECT_EQ(err->matrix_error.error, "Not a known room");
@@ -583,7 +590,8 @@ TEST(ClientAPI, InviteRoom)
                                              check_error(err);
 
                                              bob->join_room(
-                                               room_id, [](const nlohmann::json &, RequestErr err) {
+                                               room_id,
+                                               [](const mtx::responses::RoomId &, RequestErr err) {
                                                        check_error(err);
                                                });
                                      });
@@ -625,7 +633,8 @@ TEST(ClientAPI, KickRoom)
                             check_error(err);
 
                             bob->join_room(
-                              room_id, [alice, room_id](const nlohmann::json &, RequestErr err) {
+                              room_id,
+                              [alice, room_id](const mtx::responses::RoomId &, RequestErr err) {
                                       check_error(err);
 
                                       alice->kick_user(room_id,
@@ -672,7 +681,8 @@ TEST(ClientAPI, BanRoom)
                             check_error(err);
 
                             bob->join_room(
-                              room_id, [alice, room_id](const nlohmann::json &, RequestErr err) {
+                              room_id,
+                              [alice, room_id](const mtx::responses::RoomId &, RequestErr err) {
                                       check_error(err);
 
                                       alice->ban_user(
@@ -912,7 +922,7 @@ TEST(ClientAPI, PresenceOverSync)
                   auto room_id = res.room_id.to_string();
 
                   bob->join_room(
-                    room_id, [alice, bob, room_id](const nlohmann::json &, RequestErr err) {
+                    room_id, [alice, bob, room_id](const mtx::responses::RoomId &, RequestErr err) {
                             check_error(err);
                             alice->put_presence_status(
                               mtx::presence::unavailable,
@@ -984,7 +994,7 @@ TEST(ClientAPI, SendMessages)
                   auto room_id = res.room_id.to_string();
 
                   bob->join_room(
-                    room_id, [alice, bob, room_id](const nlohmann::json &, RequestErr err) {
+                    room_id, [alice, bob, room_id](const mtx::responses::RoomId &, RequestErr err) {
                             check_error(err);
 
                             // Flag to indicate when those messages would be ready to be read by
diff --git a/tests/e2ee.cpp b/tests/e2ee.cpp
index 9ca3009f4d80b086a573bef657b9a49dcbf13318..f2d08d2fb759c10030dae4ce2681008cae10c9b9 100644
--- a/tests/e2ee.cpp
+++ b/tests/e2ee.cpp
@@ -4,6 +4,8 @@
 
 #include <gtest/gtest.h>
 
+#include <nlohmann/json.hpp>
+
 #include "mtxclient/crypto/client.hpp"
 #include "mtxclient/crypto/types.hpp"
 #include "mtxclient/http/client.hpp"
@@ -116,18 +118,12 @@ TEST(Encryption, UploadOneTimeKeys)
         auto nkeys = olm_account->generate_one_time_keys(5);
         EXPECT_EQ(nkeys, 5);
 
-        json otks = olm_account->one_time_keys();
+        auto otks = olm_account->one_time_keys();
 
         mtx::requests::UploadKeys req;
 
-        // Create the proper structure for uploading.
-        std::map<std::string, json> unsigned_keys;
-
-        auto obj = otks.at("curve25519");
-        for (auto it = obj.begin(); it != obj.end(); ++it)
-                unsigned_keys["curve25519:" + it.key()] = it.value();
-
-        req.one_time_keys = unsigned_keys;
+        for (auto [key_id, key] : otks.curve25519)
+                req.one_time_keys["curve25519:" + key_id] = key;
 
         alice->upload_keys(req, [](const mtx::responses::UploadKeys &res, RequestErr err) {
                 check_error(err);
@@ -160,7 +156,8 @@ TEST(Encryption, UploadSignedOneTimeKeys)
         auto one_time_keys = olm_account->one_time_keys();
 
         mtx::requests::UploadKeys req;
-        req.one_time_keys = olm_account->sign_one_time_keys(one_time_keys);
+        for (const auto &[key_id, key] : olm_account->sign_one_time_keys(one_time_keys))
+                req.one_time_keys[key_id] = key;
 
         alice->upload_keys(req, [nkeys](const mtx::responses::UploadKeys &res, RequestErr err) {
                 check_error(err);
@@ -570,7 +567,7 @@ TEST(Encryption, EnableEncryption)
                     });
 
                   carl->join_room(res.room_id.to_string(),
-                                  [&responses](const nlohmann::json &, RequestErr err) {
+                                  [&responses](const mtx::responses::RoomId &, RequestErr err) {
                                           check_error(err);
                                           responses += 1;
                                   });
@@ -849,7 +846,7 @@ TEST(Encryption, OlmRoomKeyEncryption)
 
         // Alice create m.room.key request
         json payload =
-          json{{"content", {"secret", SECRET_TEXT}}, {"type", "im.nheko.custom_test_event"}};
+          json{{"content", {{"secret", SECRET_TEXT}}}, {"type", "im.nheko.custom_test_event"}};
 
         // Alice creates an outbound session.
         auto out_session = alice_olm->create_outbound_session(bob_curve25519, bob_otk);
diff --git a/tests/requests.cpp b/tests/requests.cpp
index ad4b181e64dba2342ef3f401d33adde727b545f8..b1e637431fe04cffdd401c8a971450d3676c6eb0 100644
--- a/tests/requests.cpp
+++ b/tests/requests.cpp
@@ -83,20 +83,17 @@ TEST(Requests, UploadKeys)
           "EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/"
           "a+myXS367WT6NAIcBA\"}},\"user_id\":\"@alice:example.com\"}}");
 
-        json k1 = {{"key", "zKbLg+NrIjpnagy+pIY6uPL4ZwEG2v+8F9lmgsnlZzs"},
-                   {"signatures",
-                    {{"@alice:example.com",
-                      {{"ed25519:JLAFKJWSCS",
-                        "IQeCEPb9HFk217cU9kw9EOiusC6kMIkoIRnbnfOh5Oc63S1ghgyjShBGpu34blQomoalCyXWyh"
-                        "aaT3MrLZYQ"
-                        "AA"}}}}}};
-
-        json k2 = {{"key", "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw"},
-                   {"signatures",
-                    {{"@alice:example.com",
-                      {{"ed25519:JLAFKJWSCS",
-                        "FLWxXqGbwrb8SM3Y795eB6OA8bwBcoMZFXBqnTn58AYWZSqiD45tlBVcDa2L7RwdKXebW/"
-                        "VzDlnfVJ+9jok1Bw"}}}}}};
+        mtx::requests::SignedOneTimeKey k1;
+        k1.key        = "zKbLg+NrIjpnagy+pIY6uPL4ZwEG2v+8F9lmgsnlZzs";
+        k1.signatures = {{"@alice:example.com",
+                          {{"ed25519:JLAFKJWSCS",
+                            "IQeCEPb9HFk217cU9kw9EOiusC6kMIkoIRnbnfOh5Oc63S1ghgyjShBGpu34blQomoalCy"
+                            "XWyhaaT3MrLZYQAA"}}}};
+
+        mtx::requests::SignedOneTimeKey k2;
+        k2.key = "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw";
+        k2.signatures["@alice:example.com"]["ed25519:JLAFKJWSCS"] =
+          "FLWxXqGbwrb8SM3Y795eB6OA8bwBcoMZFXBqnTn58AYWZSqiD45tlBVcDa2L7RwdKXebW/VzDlnfVJ+9jok1Bw";
 
         r3.one_time_keys.emplace("curve25519:AAAAAQ",
                                  "/qyvZvwjiTxGdGU0RCguDCLeR+nmsb3FfNG3/Ve4vU8");