diff --git a/CMakeLists.txt b/CMakeLists.txt
index 18c197ed5a9b773e01c7b9b9c9d37773fe8048b3..554bcbe13e983e0d603e0649227d1f9af5db2d97 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -133,6 +133,7 @@ add_library(matrix_client
             lib/structs/events/power_levels.cpp
             lib/structs/events/redaction.cpp
             lib/structs/events/tag.cpp
+            lib/structs/events/tombstone.cpp
             lib/structs/events/topic.cpp
             lib/structs/events/messages/audio.cpp
             lib/structs/events/messages/emote.cpp
diff --git a/include/mtx/events.hpp b/include/mtx/events.hpp
index 83bff9dc1a781d94685022854bbd33fb29d26770..c0e2b843f00808412f77292ef0799f46d167c5de 100644
--- a/include/mtx/events.hpp
+++ b/include/mtx/events.hpp
@@ -48,6 +48,8 @@ enum class EventType
         RoomRedaction,
         /// m.room.pinned_events
         RoomPinnedEvents,
+        /// m.room.tombstone
+        RoomTombstone,
         // m.sticker
         Sticker,
         // m.tag
@@ -136,6 +138,9 @@ to_json(json &obj, const Event<Content> &event)
         case EventType::RoomPinnedEvents:
                 obj["type"] = "m.room.pinned_events";
                 break;
+        case EventType::RoomTombstone:
+                obj["type"] = "m.room.tombstone";
+                break;
         case EventType::Sticker:
                 obj["type"] = "m.sticker";
                 break;
diff --git a/include/mtx/events/collections.hpp b/include/mtx/events/collections.hpp
index a2e46266bf47ddd4bdfce69f74c8d94d696078c3..13adb68af62c114046ca997e5c7ee6f22696e79e 100644
--- a/include/mtx/events/collections.hpp
+++ b/include/mtx/events/collections.hpp
@@ -18,6 +18,7 @@
 #include "mtx/events/power_levels.hpp"
 #include "mtx/events/redaction.hpp"
 #include "mtx/events/tag.hpp"
+#include "mtx/events/tombstone.hpp"
 #include "mtx/events/topic.hpp"
 
 #include "mtx/events/messages/audio.hpp"
@@ -54,6 +55,7 @@ using StateEvents = boost::variant<events::StateEvent<states::Aliases>,
                                    events::StateEvent<states::Name>,
                                    events::StateEvent<states::PinnedEvents>,
                                    events::StateEvent<states::PowerLevels>,
+                                   events::StateEvent<states::Tombstone>,
                                    events::StateEvent<states::Topic>,
                                    events::StateEvent<msgs::Redacted>>;
 
@@ -69,6 +71,7 @@ using StrippedEvents = boost::variant<events::StrippedEvent<states::Aliases>,
                                       events::StrippedEvent<states::Name>,
                                       events::StrippedEvent<states::PinnedEvents>,
                                       events::StrippedEvent<states::PowerLevels>,
+                                      events::StrippedEvent<states::Tombstone>,
                                       events::StrippedEvent<states::Topic>>;
 
 //! Collection of @p StateEvent and @p RoomEvent. Those events would be
@@ -85,6 +88,7 @@ using TimelineEvents = boost::variant<events::StateEvent<states::Aliases>,
                                       events::StateEvent<states::Name>,
                                       events::StateEvent<states::PinnedEvents>,
                                       events::StateEvent<states::PowerLevels>,
+                                      events::StateEvent<states::Tombstone>,
                                       events::StateEvent<states::Topic>,
                                       events::EncryptedEvent<msgs::Encrypted>,
                                       events::RedactionEvent<msgs::Redaction>,
@@ -164,6 +168,10 @@ from_json(const json &obj, TimelineEvent &e)
                 e.data = events::RedactionEvent<mtx::events::msg::Redaction>(obj);
                 break;
         }
+        case events::EventType::RoomTombstone: {
+                e.data = events::StateEvent<Tombstone>(obj);
+                break;
+        }
         case events::EventType::RoomTopic: {
                 e.data = events::StateEvent<Topic>(obj);
                 break;
diff --git a/include/mtx/events/tombstone.hpp b/include/mtx/events/tombstone.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4d1c7a0d43b345a94b8b828cbe1d108124e03271
--- /dev/null
+++ b/include/mtx/events/tombstone.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <nlohmann/json.hpp>
+
+using json = nlohmann::json;
+
+namespace mtx {
+namespace events {
+namespace state {
+
+//! Content for the `m.room.tombstone` event.
+//
+//! A state event signifying that a room has been
+//! upgraded to a different room version, and
+//! that clients should go there.
+struct Tombstone
+{
+        //! Required. A server-defined message.
+        std::string body;
+        //! Required. The new room the client should be visiting.
+        std::string replacement_room;
+};
+
+//! Deserialization method needed by @p nlohmann::json.
+void
+from_json(const json &obj, Tombstone &content);
+
+//! Serialization method needed by @p nlohmann::json.
+void
+to_json(json &obj, const Tombstone &content);
+
+} // namespace state
+} // namespace events
+} // namespace mtx
diff --git a/lib/structs/events.cpp b/lib/structs/events.cpp
index bdbbe00ddb0405d6d8eb1e7496a868ba656bef7d..5d1a89cd1e3b1c0c447d2b427652b1815b989579 100644
--- a/lib/structs/events.cpp
+++ b/lib/structs/events.cpp
@@ -42,6 +42,8 @@ getEventType(const std::string &type)
                 return EventType::RoomRedaction;
         else if (type == "m.room.pinned_events")
                 return EventType::RoomPinnedEvents;
+        else if (type == "m.room.tombstone")
+                return EventType::RoomTombstone;
         else if (type == "m.sticker")
                 return EventType::Sticker;
         else if (type == "m.tag")
@@ -88,6 +90,8 @@ to_string(EventType type)
                 return "m.room.redaction";
         case EventType::RoomPinnedEvents:
                 return "m.room.pinned_events";
+        case EventType::RoomTombstone:
+                return "m.room.tombstone";
         case EventType::Sticker:
                 return "m.sticker";
         case EventType::Tag:
diff --git a/lib/structs/events/tombstone.cpp b/lib/structs/events/tombstone.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b862632a4658278234d33324dd66308e20b7215a
--- /dev/null
+++ b/lib/structs/events/tombstone.cpp
@@ -0,0 +1,27 @@
+#include <nlohmann/json.hpp>
+
+using json = nlohmann::json;
+
+#include "mtx/events/tombstone.hpp"
+
+namespace mtx {
+namespace events {
+namespace state {
+
+void
+from_json(const json &obj, Tombstone &content)
+{
+        content.body             = obj.at("body");
+        content.replacement_room = obj.at("replacement_room");
+}
+
+void
+to_json(json &obj, const Tombstone &content)
+{
+        obj["body"]             = content.body;
+        obj["replacement_room"] = content.replacement_room;
+}
+
+} // namespace state
+} // namespace events
+} // namespace mtx
diff --git a/lib/structs/responses/common.cpp b/lib/structs/responses/common.cpp
index e3e2dbd22f7fcd71b7b3a26784a9071d95569d94..67ce418b7f9831cdb874b2e2ffd7ccfe2f9d891f 100644
--- a/lib/structs/responses/common.cpp
+++ b/lib/structs/responses/common.cpp
@@ -93,6 +93,7 @@ parse_room_account_data_events(
                 case events::EventType::RoomMessage:
                 case events::EventType::RoomName:
                 case events::EventType::RoomPowerLevels:
+                case events::EventType::RoomTombstone:
                 case events::EventType::RoomTopic:
                 case events::EventType::RoomRedaction:
                 case events::EventType::RoomPinnedEvents:
@@ -233,6 +234,15 @@ parse_timeline_events(const json &events,
 
                         break;
                 }
+                case events::EventType::RoomTombstone: {
+                        try {
+                                container.emplace_back(events::StateEvent<Tombstone>(e));
+                        } catch (json::exception &err) {
+                                log_error(err, e);
+                        }
+
+                        break;
+                }
                 case events::EventType::RoomTopic: {
                         try {
                                 container.emplace_back(events::StateEvent<Topic>(e));
@@ -474,6 +484,15 @@ parse_state_events(const json &events,
 
                         break;
                 }
+                case events::EventType::RoomTombstone: {
+                        try {
+                                container.emplace_back(events::StateEvent<Tombstone>(e));
+                        } catch (json::exception &err) {
+                                log_error(err, e);
+                        }
+
+                        break;
+                }
                 case events::EventType::RoomTopic: {
                         try {
                                 container.emplace_back(events::StateEvent<Topic>(e));
@@ -597,6 +616,15 @@ parse_stripped_events(const json &events,
 
                         break;
                 }
+                case events::EventType::RoomTombstone: {
+                        try {
+                                container.emplace_back(events::StrippedEvent<Tombstone>(e));
+                        } catch (json::exception &err) {
+                                log_error(err, e);
+                        }
+
+                        break;
+                }
                 case events::EventType::RoomTopic: {
                         try {
                                 container.emplace_back(events::StrippedEvent<Topic>(e));
diff --git a/tests/events.cpp b/tests/events.cpp
index 574a07271406d7ce389d7ea7d88bf4ef13d66f21..234426ea2d7821d0fba8798202e9befe45f5e7ca 100644
--- a/tests/events.cpp
+++ b/tests/events.cpp
@@ -51,6 +51,7 @@ TEST(Events, Conversions)
         EXPECT_EQ("m.room.name", ns::to_string(ns::EventType::RoomName));
         EXPECT_EQ("m.room.power_levels", ns::to_string(ns::EventType::RoomPowerLevels));
         EXPECT_EQ("m.room.topic", ns::to_string(ns::EventType::RoomTopic));
+        EXPECT_EQ("m.room.tombstone", ns::to_string(ns::EventType::RoomTombstone));
         EXPECT_EQ("m.room.redaction", ns::to_string(ns::EventType::RoomRedaction));
         EXPECT_EQ("m.room.pinned_events", ns::to_string(ns::EventType::RoomPinnedEvents));
         EXPECT_EQ("m.tag", ns::to_string(ns::EventType::Tag));
@@ -547,6 +548,37 @@ TEST(StateEvents, PowerLevels)
         EXPECT_EQ(event.content.user_level("@not:matrix.org"), event.content.users_default);
 }
 
+TEST(StateEvents, Tombstone)
+{
+        json data = R"({
+            "content": {
+                "body": "This room has been replaced",
+                "replacement_room": "!newroom:example.org"
+            },
+            "event_id": "$143273582443PhrSn:example.org",
+            "origin_server_ts": 1432735824653,
+            "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+            "sender": "@example:example.org",
+            "state_key": "",
+            "type": "m.room.tombstone",
+            "unsigned": {
+                "age": 1234
+            }
+        })"_json;
+
+        ns::StateEvent<ns::state::Tombstone> event = data;
+
+        EXPECT_EQ(event.type, ns::EventType::RoomTombstone);
+        EXPECT_EQ(event.event_id, "$143273582443PhrSn:example.org");
+        EXPECT_EQ(event.room_id, "!jEsUZKDJdhlrceRyVU:example.org");
+        EXPECT_EQ(event.sender, "@example:example.org");
+        EXPECT_EQ(event.origin_server_ts, 1432735824653);
+        EXPECT_EQ(event.unsigned_data.age, 1234);
+        EXPECT_EQ(event.state_key, "");
+        EXPECT_EQ(event.content.body, "This room has been replaced");
+        EXPECT_EQ(event.content.replacement_room, "!newroom:example.org");
+}
+
 TEST(StateEvents, Topic)
 {
         json data = R"({