diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0073604340d0ece1c6b89a2fe5cca197ca93b9fa..e27d9d69f119af88e859ad8e7cbf9115c0663263 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -185,6 +185,7 @@ target_sources(matrix_client
 	lib/structs/events/name.cpp
 	lib/structs/events/pinned_events.cpp
 	lib/structs/events/power_levels.cpp
+	lib/structs/events/reaction.cpp
 	lib/structs/events/redaction.cpp
 	lib/structs/events/tag.cpp
 	lib/structs/events/tombstone.cpp
diff --git a/include/mtx/events.hpp b/include/mtx/events.hpp
index 1f2034d3e69a454083ca602078dd16569b8cf44d..692540dc5d19528015f7224f16121ccbffb0e52d 100644
--- a/include/mtx/events.hpp
+++ b/include/mtx/events.hpp
@@ -24,6 +24,8 @@ enum class EventType
         KeyVerificationKey,
         /// m.key.verification.mac
         KeyVerificationMac,
+        /// m.reaction,
+        Reaction,
         /// m.room_key_request
         RoomKeyRequest,
         /// m.room.aliases
@@ -117,6 +119,9 @@ to_json(json &obj, const Event<Content> &event)
         case EventType::KeyVerificationRequest:
                 obj["type"] = "m.key.verification.request";
                 break;
+        case EventType::Reaction:
+                obj["type"] = "m.reaction";
+                break;
         case EventType::RoomKeyRequest:
                 obj["type"] = "m.room_key_request";
                 break;
diff --git a/include/mtx/events/collections.hpp b/include/mtx/events/collections.hpp
index 384a422bf1b6206c3cf35f5c50d92b0c78e454ed..1af77cb1bc6853a52ee5deddcfc1650adef24685 100644
--- a/include/mtx/events/collections.hpp
+++ b/include/mtx/events/collections.hpp
@@ -16,6 +16,7 @@
 #include "mtx/events/name.hpp"
 #include "mtx/events/pinned_events.hpp"
 #include "mtx/events/power_levels.hpp"
+#include "mtx/events/reaction.hpp"
 #include "mtx/events/redaction.hpp"
 #include "mtx/events/tag.hpp"
 #include "mtx/events/tombstone.hpp"
@@ -95,6 +96,7 @@ using TimelineEvents = std::variant<events::StateEvent<states::Aliases>,
                                     events::EncryptedEvent<msgs::Encrypted>,
                                     events::RedactionEvent<msgs::Redaction>,
                                     events::Sticker,
+                                    events::RoomEvent<msgs::Reaction>,
                                     events::RoomEvent<msgs::Redacted>,
                                     events::RoomEvent<msgs::Audio>,
                                     events::RoomEvent<msgs::Emote>,
diff --git a/include/mtx/events/common.hpp b/include/mtx/events/common.hpp
index 31023690a5094f754db4d1de51f1f0dd7b684db4..71a19ee7b6688952751e89e308f31b9d084cbc06 100644
--- a/include/mtx/events/common.hpp
+++ b/include/mtx/events/common.hpp
@@ -155,8 +155,46 @@ from_json(const nlohmann::json &obj, InReplyTo &in_reply_to);
 void
 to_json(nlohmann::json &obj, const InReplyTo &in_reply_to);
 
+//! Definition of rel_type for relations.
+enum class RelationType
+{
+        // m.annotation rel_type
+        Annotation,
+        // m.reference rel_type
+        Reference,
+        // m.replace rel_type
+        Replace,
+        // not one of the supported types
+        Unsupported
+};
+
+void
+from_json(const nlohmann::json &obj, RelationType &type);
+
+void
+to_json(nlohmann::json &obj, const RelationType &type);
+
+//! Relates to for reactions
+struct ReactionRelatesTo
+{
+        // Type of relation
+        RelationType rel_type;
+        // event id being reacted to
+        std::string event_id;
+        // key is the reaction itself
+        std::string key;
+};
+
+//! Deserialization method needed by @p nlohmann::json.
+void
+from_json(const nlohmann::json &obj, ReactionRelatesTo &relates_to);
+
+//! Serialization method needed by @p nlohmann::json.
+void
+to_json(nlohmann::json &obj, const ReactionRelatesTo &relates_to);
+
 //! Relates to data for rich replies (notice and text events)
-struct RelatesTo
+struct ReplyRelatesTo
 {
         //! What the message is in reply to
         InReplyTo in_reply_to;
@@ -164,11 +202,11 @@ struct RelatesTo
 
 //! Deserialization method needed by @p nlohmann::json.
 void
-from_json(const nlohmann::json &obj, RelatesTo &relates_to);
+from_json(const nlohmann::json &obj, ReplyRelatesTo &relates_to);
 
 //! Serialization method needed by @p nlohmann::json.
 void
-to_json(nlohmann::json &obj, const RelatesTo &relates_to);
+to_json(nlohmann::json &obj, const ReplyRelatesTo &relates_to);
 
 } // namespace common
 } // namespace mtx
diff --git a/include/mtx/events/encrypted.hpp b/include/mtx/events/encrypted.hpp
index bb75e074d8cd914de032ff7de677a67875d9f7b0..5001da4726a26abbe3ddfed8567f587a7714bdf2 100644
--- a/include/mtx/events/encrypted.hpp
+++ b/include/mtx/events/encrypted.hpp
@@ -82,7 +82,7 @@ struct Encrypted
         //! Outbound group session id.
         std::string session_id;
         //! Relates to for rich replies
-        common::RelatesTo relates_to;
+        common::ReplyRelatesTo relates_to;
 };
 
 void
diff --git a/include/mtx/events/messages/audio.hpp b/include/mtx/events/messages/audio.hpp
index 216bfcc6df187fc4a4616fd8113d84cd3d039441..501ff94e0b789d2250266b4a6b447eaaff5a1913 100644
--- a/include/mtx/events/messages/audio.hpp
+++ b/include/mtx/events/messages/audio.hpp
@@ -30,7 +30,7 @@ struct Audio
         // Encryption members. If present, they replace url.
         std::optional<crypto::EncryptedFile> file;
         //! Relates to for rich replies
-        mtx::common::RelatesTo relates_to;
+        mtx::common::ReplyRelatesTo relates_to;
 };
 
 void
diff --git a/include/mtx/events/messages/emote.hpp b/include/mtx/events/messages/emote.hpp
index d110ade4ce7cbc3db1aa350ac5ebd0eef9dd69dc..6061c7cbf7231a826d161b962e9e4aadb03091b6 100644
--- a/include/mtx/events/messages/emote.hpp
+++ b/include/mtx/events/messages/emote.hpp
@@ -25,7 +25,7 @@ struct Emote
         //! HTML formatted message.
         std::string formatted_body;
         //! Relates to for rich replies
-        mtx::common::RelatesTo relates_to;
+        mtx::common::ReplyRelatesTo relates_to;
 };
 
 void
diff --git a/include/mtx/events/messages/file.hpp b/include/mtx/events/messages/file.hpp
index 07c2554253abb93098f96dcd78b2c971d41fe947..c029e9cd8eded757033ef00672bda8e8ac79a911 100644
--- a/include/mtx/events/messages/file.hpp
+++ b/include/mtx/events/messages/file.hpp
@@ -33,7 +33,7 @@ struct File
         // Encryption members. If present, they replace url.
         std::optional<crypto::EncryptedFile> file;
         //! Relates to for rich replies
-        mtx::common::RelatesTo relates_to;
+        mtx::common::ReplyRelatesTo relates_to;
 };
 
 void
diff --git a/include/mtx/events/messages/image.hpp b/include/mtx/events/messages/image.hpp
index 06edd72687e0a6b88560dc69aadf66fe45788a19..00619d953583103451b9e40912cb7f6a8221be41 100644
--- a/include/mtx/events/messages/image.hpp
+++ b/include/mtx/events/messages/image.hpp
@@ -31,7 +31,7 @@ struct Image
         // Encryption members. If present, they replace url.
         std::optional<crypto::EncryptedFile> file;
         //! Relates to for rich replies
-        mtx::common::RelatesTo relates_to;
+        mtx::common::ReplyRelatesTo relates_to;
 };
 
 struct StickerImage
@@ -47,7 +47,7 @@ struct StickerImage
         // Encryption members. If present, they replace url.
         std::optional<crypto::EncryptedFile> file;
         //! Relates to for rich replies
-        mtx::common::RelatesTo relates_to;
+        mtx::common::ReplyRelatesTo relates_to;
 };
 
 void
diff --git a/include/mtx/events/messages/notice.hpp b/include/mtx/events/messages/notice.hpp
index fb41b39a1f69467b64d9e46ab756ededcb3072e6..7e89ee3caee39523e2820ea83f15b8048fd12f24 100644
--- a/include/mtx/events/messages/notice.hpp
+++ b/include/mtx/events/messages/notice.hpp
@@ -25,7 +25,7 @@ struct Notice
         //! HTML formatted message.
         std::string formatted_body;
         // Relates to for rich replies
-        mtx::common::RelatesTo relates_to;
+        mtx::common::ReplyRelatesTo relates_to;
 };
 
 void
diff --git a/include/mtx/events/messages/text.hpp b/include/mtx/events/messages/text.hpp
index fbf8ec843795e87e2dcbac9629c6ddf741440462..1a929f3535c3ec540a77bc6b4b7630606285da47 100644
--- a/include/mtx/events/messages/text.hpp
+++ b/include/mtx/events/messages/text.hpp
@@ -25,7 +25,7 @@ struct Text
         //! HTML formatted message.
         std::string formatted_body;
         //! Relates to for rich replies
-        mtx::common::RelatesTo relates_to;
+        mtx::common::ReplyRelatesTo relates_to;
 };
 
 void
diff --git a/include/mtx/events/messages/video.hpp b/include/mtx/events/messages/video.hpp
index 69877681f69aa11ba867ecda3761e657eed7a0cd..2af4b1eb8f666876eaff29c08ebea38c0ad87c0b 100644
--- a/include/mtx/events/messages/video.hpp
+++ b/include/mtx/events/messages/video.hpp
@@ -30,7 +30,7 @@ struct Video
         // Encryption members. If present, they replace url.
         std::optional<crypto::EncryptedFile> file;
         //! Relates to for rich replies
-        mtx::common::RelatesTo relates_to;
+        mtx::common::ReplyRelatesTo relates_to;
 };
 
 void
diff --git a/include/mtx/events/reaction.hpp b/include/mtx/events/reaction.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..ec7987b0ba97809eb7d517e9f8b5395010ee4eb4
--- /dev/null
+++ b/include/mtx/events/reaction.hpp
@@ -0,0 +1,32 @@
+#pragma once
+
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
+#include <nlohmann/json.hpp>
+#endif
+
+#include <string>
+
+#include "mtx/events/common.hpp"
+
+namespace mtx {
+namespace events {
+namespace msg {
+
+//! Content for the `m.reaction` event.
+struct Reaction
+{
+        //! The event being reacted to
+        mtx::common::ReactionRelatesTo relates_to;
+};
+
+void
+from_json(const nlohmann::json &obj, Reaction &event);
+
+void
+to_json(nlohmann::json &obj, const Reaction &event);
+
+} // namespace msg
+} // namespace events
+} // namespace mtx
diff --git a/lib/structs/events.cpp b/lib/structs/events.cpp
index c5dc3bc2d1a061a2fb5426bfaa8a21d5cf445f90..b0f05cf77f6ae60ae4dda0763eeaee0b42ec086b 100644
--- a/lib/structs/events.cpp
+++ b/lib/structs/events.cpp
@@ -20,6 +20,8 @@ getEventType(const std::string &type)
                 return EventType::KeyVerificationMac;
         else if (type == "m.key.verification.cancel")
                 return EventType::KeyVerificationCancel;
+        else if (type == "m.reaction")
+                return EventType::Reaction;
         else if (type == "m.room_key_request")
                 return EventType::RoomKeyRequest;
         else if (type == "m.room.aliases")
@@ -82,6 +84,8 @@ to_string(EventType type)
                 return "m.key.verification.key";
         case EventType::KeyVerificationMac:
                 return "m.key.verification.mac";
+        case EventType::Reaction:
+                return "m.reaction";
         case EventType::RoomKeyRequest:
                 return "m.room_key_request";
         case EventType::RoomAliases:
diff --git a/lib/structs/events/collections.cpp b/lib/structs/events/collections.cpp
index 33578e06c909b9591f0795275e3010bac8c68870..de087f5e16f7894e67bd87a93e541c6bc1be8f7e 100644
--- a/lib/structs/events/collections.cpp
+++ b/lib/structs/events/collections.cpp
@@ -10,6 +10,10 @@ from_json(const json &obj, TimelineEvent &e)
         using namespace mtx::events::msg;
 
         switch (type) {
+        case events::EventType::Reaction: {
+                e.data = events::RoomEvent<Reaction>(obj);
+                break;
+        }
         case events::EventType::RoomAliases: {
                 e.data = events::StateEvent<Aliases>(obj);
                 break;
diff --git a/lib/structs/events/common.cpp b/lib/structs/events/common.cpp
index eccdf6c7c73b68035c21cc9332d8aa678f00b9c4..2babec5af7576cfd7131bdfe20adbe3d9b378a81 100644
--- a/lib/structs/events/common.cpp
+++ b/lib/structs/events/common.cpp
@@ -183,14 +183,66 @@ to_json(json &obj, const InReplyTo &in_reply_to)
 }
 
 void
-from_json(const json &obj, RelatesTo &relates_to)
+to_json(json &obj, const RelationType &type)
+{
+        switch (type) {
+        case RelationType::Annotation:
+                obj = "m.annotation";
+                break;
+        case RelationType::Reference:
+                obj = "m.reference";
+                break;
+        case RelationType::Replace:
+                obj = "m.replace";
+                break;
+        case RelationType::Unsupported:
+        default:
+                obj = "unsupported";
+                break;
+        }
+}
+
+void
+from_json(const json &obj, RelationType &type)
+{
+        if (obj.get<std::string>() == "m.annotation")
+                type = RelationType::Annotation;
+        else if (obj.get<std::string>() == "m.reference")
+                type = RelationType::Reference;
+        else if (obj.get<std::string>() == "m.replace")
+                type = RelationType::Replace;
+        else
+                type = RelationType::Unsupported;
+}
+
+void
+from_json(const json &obj, ReactionRelatesTo &relates_to)
+{
+        if (obj.find("rel_type") != obj.end())
+                relates_to.rel_type = obj.at("rel_type").get<RelationType>();
+        if (obj.find("event_id") != obj.end())
+                relates_to.event_id = obj.at("event_id").get<std::string>();
+        if (obj.find("key") != obj.end())
+                relates_to.key = obj.at("key").get<std::string>();
+}
+
+void
+to_json(json &obj, const ReactionRelatesTo &relates_to)
+{
+        obj["rel_type"] = relates_to.rel_type;
+        obj["event_id"] = relates_to.event_id;
+        obj["key"]      = relates_to.key;
+}
+
+void
+from_json(const json &obj, ReplyRelatesTo &relates_to)
 {
         if (obj.find("m.in_reply_to") != obj.end())
                 relates_to.in_reply_to = obj.at("m.in_reply_to").get<InReplyTo>();
 }
 
 void
-to_json(json &obj, const RelatesTo &relates_to)
+to_json(json &obj, const ReplyRelatesTo &relates_to)
 {
         obj["m.in_reply_to"] = relates_to.in_reply_to;
 }
diff --git a/lib/structs/events/encrypted.cpp b/lib/structs/events/encrypted.cpp
index fdbc8291de99054d227299b17c5d20680065a9dc..bda3466e7cf1b4c7720858bf0e2e25c18b532f80 100644
--- a/lib/structs/events/encrypted.cpp
+++ b/lib/structs/events/encrypted.cpp
@@ -100,7 +100,7 @@ from_json(const json &obj, Encrypted &content)
         content.session_id = obj.at("session_id").get<std::string>();
 
         if (obj.count("m.relates_to") != 0)
-                content.relates_to = obj.at("m.relates_to").get<common::RelatesTo>();
+                content.relates_to = obj.at("m.relates_to").get<common::ReplyRelatesTo>();
 }
 
 void
diff --git a/lib/structs/events/messages/audio.cpp b/lib/structs/events/messages/audio.cpp
index 0140c36f426f189ccbf9ea55964ed69bc8febf10..7e40720e9d185a126fc49e76400cdade8db8bde8 100644
--- a/lib/structs/events/messages/audio.cpp
+++ b/lib/structs/events/messages/audio.cpp
@@ -27,7 +27,7 @@ from_json(const json &obj, Audio &content)
                 content.file = obj.at("file").get<crypto::EncryptedFile>();
 
         if (obj.count("m.relates_to") != 0)
-                content.relates_to = obj.at("m.relates_to").get<common::RelatesTo>();
+                content.relates_to = obj.at("m.relates_to").get<common::ReplyRelatesTo>();
 }
 
 void
diff --git a/lib/structs/events/messages/emote.cpp b/lib/structs/events/messages/emote.cpp
index 383e73d905c3e1a2c958eb18e1e38e9a16c9743d..7a15fd5c54b5228d19250fee855396ca5d297ae1 100644
--- a/lib/structs/events/messages/emote.cpp
+++ b/lib/structs/events/messages/emote.cpp
@@ -23,7 +23,7 @@ from_json(const json &obj, Emote &content)
                 content.formatted_body = obj.at("formatted_body").get<std::string>();
 
         if (obj.count("m.relates_to") != 0)
-                content.relates_to = obj.at("m.relates_to").get<common::RelatesTo>();
+                content.relates_to = obj.at("m.relates_to").get<common::ReplyRelatesTo>();
 }
 
 void
diff --git a/lib/structs/events/messages/file.cpp b/lib/structs/events/messages/file.cpp
index b188409e55ecb2580699b13a53b818358950d862..4c65e901960f76ff4665ea10faf074a40a49bd89 100644
--- a/lib/structs/events/messages/file.cpp
+++ b/lib/structs/events/messages/file.cpp
@@ -30,7 +30,7 @@ from_json(const json &obj, File &content)
                 content.file = obj.at("file").get<crypto::EncryptedFile>();
 
         if (obj.count("m.relates_to") != 0)
-                content.relates_to = obj.at("m.relates_to").get<common::RelatesTo>();
+                content.relates_to = obj.at("m.relates_to").get<common::ReplyRelatesTo>();
 }
 
 void
diff --git a/lib/structs/events/messages/image.cpp b/lib/structs/events/messages/image.cpp
index ca9331333a4257e4945120e95abdf8f4d0e11a20..cdaa3a410f24a71a5e95b256d16f865def6c93e1 100644
--- a/lib/structs/events/messages/image.cpp
+++ b/lib/structs/events/messages/image.cpp
@@ -26,7 +26,7 @@ from_json(const json &obj, Image &content)
                 content.file = obj.at("file").get<crypto::EncryptedFile>();
 
         if (obj.count("m.relates_to") != 0)
-                content.relates_to = obj.at("m.relates_to").get<common::RelatesTo>();
+                content.relates_to = obj.at("m.relates_to").get<common::ReplyRelatesTo>();
 }
 
 void
@@ -58,7 +58,7 @@ from_json(const json &obj, StickerImage &content)
                 content.file = obj.at("file").get<crypto::EncryptedFile>();
 
         if (obj.count("m.relates_to") != 0)
-                content.relates_to = obj.at("m.relates_to").get<common::RelatesTo>();
+                content.relates_to = obj.at("m.relates_to").get<common::ReplyRelatesTo>();
 }
 
 void
diff --git a/lib/structs/events/messages/notice.cpp b/lib/structs/events/messages/notice.cpp
index 83942ec853a5abde3a88e7d9a452e8cc2ec0dea0..cd59cf38629db3fb1cdbbc95072d1a2e74b45643 100644
--- a/lib/structs/events/messages/notice.cpp
+++ b/lib/structs/events/messages/notice.cpp
@@ -23,7 +23,7 @@ from_json(const json &obj, Notice &content)
                 content.formatted_body = obj.at("formatted_body").get<std::string>();
 
         if (obj.count("m.relates_to") != 0)
-                content.relates_to = obj.at("m.relates_to").get<common::RelatesTo>();
+                content.relates_to = obj.at("m.relates_to").get<common::ReplyRelatesTo>();
 }
 
 void
diff --git a/lib/structs/events/messages/text.cpp b/lib/structs/events/messages/text.cpp
index 90beaf67862e5185f72657b73e2381c285013dfa..5c0bf7a75fcd6c08219f5ca78ab3604c821676ba 100644
--- a/lib/structs/events/messages/text.cpp
+++ b/lib/structs/events/messages/text.cpp
@@ -23,7 +23,7 @@ from_json(const json &obj, Text &content)
                 content.formatted_body = obj.at("formatted_body").get<std::string>();
 
         if (obj.count("m.relates_to") != 0)
-                content.relates_to = obj.at("m.relates_to").get<common::RelatesTo>();
+                content.relates_to = obj.at("m.relates_to").get<common::ReplyRelatesTo>();
 }
 
 void
diff --git a/lib/structs/events/messages/video.cpp b/lib/structs/events/messages/video.cpp
index 54cbc81119f6d45e8df2e26c81adfa0dab70f9a8..e427fabb799dddd99e9e778d3dfbadb96cc11ef7 100644
--- a/lib/structs/events/messages/video.cpp
+++ b/lib/structs/events/messages/video.cpp
@@ -28,7 +28,7 @@ from_json(const json &obj, Video &content)
                 content.file = obj.at("file").get<crypto::EncryptedFile>();
 
         if (obj.count("m.relates_to") != 0)
-                content.relates_to = obj.at("m.relates_to").get<common::RelatesTo>();
+                content.relates_to = obj.at("m.relates_to").get<common::ReplyRelatesTo>();
 }
 
 void
diff --git a/lib/structs/events/reaction.cpp b/lib/structs/events/reaction.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..098bde95d5906252beb29e7dcfc83b50ccee5f19
--- /dev/null
+++ b/lib/structs/events/reaction.cpp
@@ -0,0 +1,27 @@
+#include <nlohmann/json.hpp>
+#include <string>
+
+#include "mtx/events/reaction.hpp"
+
+using json = nlohmann::json;
+
+namespace mtx {
+namespace events {
+namespace msg {
+
+void
+from_json(const json &obj, Reaction &event)
+{
+        if (obj.count("m.relates_to") != 0)
+                event.relates_to = obj.at("m.relates_to").get<common::ReactionRelatesTo>();
+}
+
+void
+to_json(json &obj, const Reaction &event)
+{
+        obj["m.relates_to"] = event.relates_to;
+}
+
+} // namespace msg
+} // namespace events
+} // namespace mtx
diff --git a/lib/structs/responses/common.cpp b/lib/structs/responses/common.cpp
index 75328f8dbb6a1ff4de3f367449d23ac31e1c24a1..d067f6f882a24757a190f89bdd0eff0dfd3ba339 100644
--- a/lib/structs/responses/common.cpp
+++ b/lib/structs/responses/common.cpp
@@ -12,6 +12,7 @@
 #include "mtx/events/name.hpp"
 #include "mtx/events/pinned_events.hpp"
 #include "mtx/events/power_levels.hpp"
+#include "mtx/events/reaction.hpp"
 #include "mtx/events/redaction.hpp"
 #include "mtx/events/tag.hpp"
 #include "mtx/events/topic.hpp"
@@ -148,6 +149,12 @@ struct TimelineEventVisitor
                 mtx::events::to_json(j, stickEv);
                 return j;
         };
+        json operator()(const events::RoomEvent<msgs::Reaction> &reactEv) const
+        {
+                json j;
+                mtx::events::to_json(j, reactEv);
+                return j;
+        };
         json operator()(const events::RoomEvent<msgs::Redacted> &redEv) const
         {
                 json j;
@@ -249,6 +256,7 @@ parse_room_account_data_events(
                 case events::EventType::KeyVerificationAccept:
                 case events::EventType::KeyVerificationKey:
                 case events::EventType::KeyVerificationMac:
+                case events::EventType::Reaction:
                 case events::EventType::RoomKeyRequest:
                 case events::EventType::RoomAliases:
                 case events::EventType::RoomAvatar:
@@ -293,6 +301,15 @@ parse_timeline_events(const json &events,
                 const auto type = mtx::events::getEventType(e);
 
                 switch (type) {
+                case events::EventType::Reaction: {
+                        try {
+                                container.emplace_back(events::RoomEvent<events::msg::Reaction>(e));
+                        } catch (json::exception &err) {
+                                log_error(err, e);
+                        }
+
+                        break;
+                }
                 case events::EventType::RoomAliases: {
                         try {
                                 container.emplace_back(events::StateEvent<Aliases>(e));
@@ -688,6 +705,7 @@ parse_state_events(const json &events,
                         break;
                 }
                 case events::EventType::Sticker:
+                case events::EventType::Reaction:
                 case events::EventType::RoomEncrypted:  /* Does this need to be here? */
                 case events::EventType::RoomKeyRequest: // Not part of the timeline or state
                 case events::EventType::RoomMessage:
@@ -827,6 +845,7 @@ parse_stripped_events(const json &events,
                         break;
                 }
                 case events::EventType::Sticker:
+                case events::EventType::Reaction:
                 case events::EventType::RoomEncrypted:
                 case events::EventType::RoomEncryption:
                 case events::EventType::RoomMessage:
diff --git a/tests/messages.cpp b/tests/messages.cpp
index f2c6fba45ab392e535477f3ed5d13915edc3ac12..6a85f0667022380a088cc129b5fb4d64798ae3b4 100644
--- a/tests/messages.cpp
+++ b/tests/messages.cpp
@@ -7,6 +7,42 @@ using json = nlohmann::json;
 
 using namespace mtx::events;
 
+TEST(RoomEvents, Reaction)
+{
+        json data = R"({
+  "type": "m.reaction",
+  "room_id": "!CYvyeleADEeDAsndMom:localhost",
+  "sender": "@example:localhost",
+  "content": {
+    "m.relates_to": {
+      "rel_type": "m.annotation",
+      "event_id": "$oGKg0tfsnDamWPsGxUptGLWR5b8Xq6QNFFsysQNSnake",
+      "key": "👀"
+    }
+  },
+  "origin_server_ts": 1588536414112,
+  "unsigned": {
+    "age": 1905609
+  },
+  "event_id": "$ujXAq1WXebS-vcpA4yBIZPvCeqGvnrMFP1c1qn8_wJump"
+  })"_json;
+
+        RoomEvent<msg::Reaction> event = data;
+
+        EXPECT_EQ(event.type, EventType::Reaction);
+        EXPECT_EQ(event.event_id, "$ujXAq1WXebS-vcpA4yBIZPvCeqGvnrMFP1c1qn8_wJump");
+        EXPECT_EQ(event.room_id, "!CYvyeleADEeDAsndMom:localhost");
+        EXPECT_EQ(event.sender, "@example:localhost");
+        EXPECT_EQ(event.origin_server_ts, 1588536414112L);
+        EXPECT_EQ(event.unsigned_data.age, 1905609L);
+        EXPECT_EQ(event.content.relates_to.event_id,
+                  "$oGKg0tfsnDamWPsGxUptGLWR5b8Xq6QNFFsysQNSnake");
+        EXPECT_EQ(event.content.relates_to.key, "👀");
+        EXPECT_EQ(event.content.relates_to.rel_type, mtx::common::RelationType::Annotation);
+
+        EXPECT_EQ(data.dump(), json(event).dump());
+};
+
 TEST(RoomEvents, Redacted)
 {
         json data = R"({
@@ -459,6 +495,8 @@ TEST(RoomEvents, TextMessage)
         EXPECT_EQ(event.content.msgtype, "m.text");
         EXPECT_EQ(event.content.relates_to.in_reply_to.event_id,
                   "$6GKhAfJOcwNd69lgSizdcTob8z2pWQgBOZPrnsWMA1E");
+
+        EXPECT_EQ(data.dump(), json(event).dump());
 }
 
 TEST(RoomEvents, VideoMessage)