diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5496bb1a261f993acb6275c4477e7cb3b0f44509..a8e64ed70dcf49cf394b14af55e1d580dbd3194e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -232,6 +232,7 @@ target_sources(matrix_client
 	lib/structs/responses/common.cpp
 	lib/structs/responses/create_room.cpp
 	lib/structs/responses/crypto.cpp
+	lib/structs/responses/device.cpp
 	lib/structs/responses/empty.cpp
 	lib/structs/responses/groups.cpp
 	lib/structs/responses/login.cpp
diff --git a/include/mtx/responses.hpp b/include/mtx/responses.hpp
index 8d844cc9e2a8abd773364d54bd76057169575ebf..176c5973a8c5c9fa511dc5f4bd3d6be067492450 100644
--- a/include/mtx/responses.hpp
+++ b/include/mtx/responses.hpp
@@ -7,6 +7,7 @@
 
 #include "responses/create_room.hpp"
 #include "responses/crypto.hpp"
+#include "responses/device.hpp"
 #include "responses/empty.hpp"
 #include "responses/groups.hpp"
 #include "responses/login.hpp"
diff --git a/include/mtx/responses/device.hpp b/include/mtx/responses/device.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e795395728901032d7f98e9eb2306baf209c391f
--- /dev/null
+++ b/include/mtx/responses/device.hpp
@@ -0,0 +1,54 @@
+#pragma once
+
+/// @file
+/// @brief device related endpoints.
+
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
+#include <nlohmann/json.hpp>
+#endif
+
+#include "mtx/common.hpp"
+#include "mtx/lightweight_error.hpp"
+
+#include <string>
+#include <vector>
+
+namespace mtx {
+namespace responses {
+
+struct Device
+{
+    //! **Required.** Identifier of this device.
+    std::string device_id;
+
+    //! Display name set by the user for this device. Absent if no name has been set.
+    std::string display_name;
+
+    //! The IP address where this device was last seen. (May be a few minutes out of date, for
+    //! efficiency reasons).
+    std::string last_seen_ip;
+
+    //! The timestamp (in milliseconds since the unix epoch) when this devices was last seen. (May
+    //! be a few minutes out of date, for efficiency reasons).
+    size_t last_seen_ts;
+};
+
+void
+from_json(const nlohmann::json &obj, Device &res);
+
+//! Response from the `GET /_matrix/client/r0/devices` endpoint.
+struct QueryDevices
+{
+    //! Gets information about all devices for the current user.
+    //! A list of all registered devices for this user.
+    //
+    std::vector<Device> devices;
+};
+
+void
+from_json(const nlohmann::json &obj, QueryDevices &response);
+
+}
+}
diff --git a/include/mtxclient/http/client.hpp b/include/mtxclient/http/client.hpp
index 9f99b2adb47e00481cb1e25e637b2752a2ad7b18..f52c65df7f2f252c2642ca5e737412f76a5129a8 100644
--- a/include/mtxclient/http/client.hpp
+++ b/include/mtxclient/http/client.hpp
@@ -79,6 +79,7 @@ struct Versions;
 struct WellKnown;
 struct PublicRoomVisibility;
 struct PublicRooms;
+struct QueryDevices;
 namespace backup {
 struct SessionBackup;
 struct RoomKeysBackup;
@@ -581,6 +582,18 @@ public:
                            Callback<nlohmann::json> cb);
     void add_room_to_group(const std::string &room_id, const std::string &group_id, ErrCallback cb);
 
+    //
+    // Device related endpoints.
+    //
+
+    //! List devices
+    void query_devices(Callback<mtx::responses::QueryDevices> cb);
+
+    /////! Rename device
+    // void rename_device(const mtx::requests::DeviceSigningUpload,
+    //                           UIAHandler uia_handler,
+    //                           ErrCallback cb);
+
     //
     // Encryption related endpoints.
     //
diff --git a/lib/http/client.cpp b/lib/http/client.cpp
index 0d55eb2539a2d784be7d78fbe13c5fe5611ba522..dc5a9fb6c809737f7a5f24d077bf707f812acfaf 100644
--- a/lib/http/client.cpp
+++ b/lib/http/client.cpp
@@ -1082,6 +1082,19 @@ Client::add_room_to_group(const std::string &room_id, const std::string &group_i
       "/client/r0/groups/" + group_id + "/admin/rooms/" + room_id, json::object(), cb);
 }
 
+//
+// Device related endpoints
+//
+
+void
+Client::query_devices(Callback<mtx::responses::QueryDevices> cb)
+{
+    get<mtx::responses::QueryDevices>("/client/r0/devices",
+                                      [cb](const mtx::responses::QueryDevices &res,
+                                           HeaderFields,
+                                           RequestErr err) { cb(res, err); });
+}
+
 //
 // Encryption related endpoints
 //
diff --git a/lib/structs/responses/device.cpp b/lib/structs/responses/device.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1973c3770d797692f1540a4379eebb8c48c01864
--- /dev/null
+++ b/lib/structs/responses/device.cpp
@@ -0,0 +1,35 @@
+#include "mtx/responses/device.hpp"
+
+#include <nlohmann/json.hpp>
+
+namespace mtx {
+namespace responses {
+
+void
+from_json(const nlohmann::json &obj, Device &res)
+{
+    res.device_id = obj.at("device_id").get<std::string>();
+
+    // This is needed because synapse sometimes sends null instead -_-
+    if (obj.contains("display_name") && obj["display_name"].is_string()) {
+        res.display_name = obj.value("display_name", std::string{});
+    }
+
+    // This is needed because synapse sometimes sends null instead -_-
+    if (obj.contains("last_seen_ip") && obj["last_seen_ip"].is_string()) {
+        res.last_seen_ip = obj.value("last_seen_ip", std::string{});
+    }
+
+    // This is needed because synapse sometimes sends null instead -_-
+    if (obj.contains("last_seen_ts") && obj["last_seen_ts"].is_number()) {
+        res.last_seen_ts = obj.value("last_seen_ts", size_t{});
+    }
+}
+
+void
+from_json(const nlohmann::json &obj, QueryDevices &response)
+{
+    response.devices = obj.at("devices").get<std::vector<Device>>();
+}
+}
+}
diff --git a/meson.build b/meson.build
index e2d16607671df50ec7fc33ac533949c56235106d..33db9e5a671e00c3bbef4409ab475d36b3c47903 100644
--- a/meson.build
+++ b/meson.build
@@ -97,6 +97,7 @@ src = [
 	'lib/structs/responses/common.cpp',
 	'lib/structs/responses/create_room.cpp',
 	'lib/structs/responses/crypto.cpp',
+	'lib/structs/responses/device.cpp',
 	'lib/structs/responses/empty.cpp',
 	'lib/structs/responses/groups.cpp',
 	'lib/structs/responses/login.cpp',