diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7bfd58b3e7cb46452206e89e7388df563f48bfaf..22970b787e79e2ac7e217f85ce7ffb56eb48ed00 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -191,7 +191,8 @@ target_sources(matrix_client
 	lib/structs/responses/sync.cpp
 	lib/structs/responses/turn_server.cpp
 	lib/structs/responses/version.cpp
-	lib/structs/responses/well-known.cpp)
+	lib/structs/responses/well-known.cpp
+	lib/structs/responses/public_rooms.cpp)
 add_library(MatrixClient::MatrixClient ALIAS matrix_client)
 set_property(TARGET matrix_client  PROPERTY CXX_STANDARD 17)
 set_property(TARGET matrix_client  PROPERTY CXX_EXTENSIONS OFF)
diff --git a/include/mtx/filters.hpp b/include/mtx/filters.hpp
deleted file mode 100644
index 1950f917da7b105df2feecb4c3b016c0d6c62c55..0000000000000000000000000000000000000000
--- a/include/mtx/filters.hpp
+++ /dev/null
@@ -1,30 +0,0 @@
-#pragma once
-
-#if __has_include(<nlohmann/json_fwd.hpp>)
-#include <nlohmann/json_fwd.hpp>
-#else
-#include <nlohmann/json.hpp>
-#endif
-
-#include <string>
-
-//! Filters can be created on the server and can be
-//! passed as a parameter to APIs which return events.
-
-namespace mtx {
-namespace filters {
-
-struct Filter {
-    //! A string to search for in the room metadata,
-    //! e.g. name, topic, canonical alias etc. (Optional).
-    std::string generic_search_term;
-};
-
-void
-from_json(const nlohmann::json &obj, Filter &res);
-
-void
-to_json(nlohmann::json &obj, const Filter &res);
-
-} // namespace filters
-} // namespace mtx
diff --git a/include/mtx/public_rooms_chunk.hpp b/include/mtx/public_rooms_chunk.hpp
deleted file mode 100644
index b2410935b0331a6b54effc9d352ed2e97a5a7a4b..0000000000000000000000000000000000000000
--- a/include/mtx/public_rooms_chunk.hpp
+++ /dev/null
@@ -1,45 +0,0 @@
-#pragma once
-
-#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 responses {
-struct PublicRoomsChunk
-{
-    //! Aliases of the room. May be empty.
-    std::vector<std::string> aliases;
-    //! The canonical alias of the room, if any.
-    std::string canonical_alias
-    //! The name of the room, if any.
-    std::string name;
-    //! **Required.** The number of members joined to the room.
-    int num_joined_members;
-    //! **Required.** The ID of the room.
-    std::string room_id;
-    //! The topic of the room, if any.
-    std::string topic;
-    //! **Required.** Whether the room may be viewed by guest users without joining.
-    bool world_readable;
-    //! **Required.** Whether guest users may join the room
-    //! and participate in it. If they can, they will be subject
-    //! to ordinary power level rules like any other user.
-    bool guest_can_join;
-    //! The URL for the room's avatar, if one is set.
-    std::string avatar_url; 
-};
-
-void
-from_json(const nlohmann::json &obj, PublicRoomsChunk &res);
-
-void
-to_json(nlohmann::json &obj, const PublicRoomsChunk &res);
-
-} // namespace responses
-} // namespace mtx
\ No newline at end of file
diff --git a/include/mtx/requests.hpp b/include/mtx/requests.hpp
index 940cf2037423b1b7e1806ed72da47276ab721194..b765c20947829a3406b02618304c50968b904fbe 100644
--- a/include/mtx/requests.hpp
+++ b/include/mtx/requests.hpp
@@ -5,7 +5,6 @@
 
 #include <mtx/common.hpp>
 #include <mtx/events/collections.hpp>
-#include <mtx/filters.hpp>
 #if __has_include(<nlohmann/json_fwd.hpp>)
 #include <nlohmann/json_fwd.hpp>
 #else
@@ -138,6 +137,15 @@ struct TypingNotification
 void
 to_json(json &obj, const TypingNotification &request);
 
+struct PublicRoomsFilter {
+    //! A string to search for in the room metadata,
+    //! e.g. name, topic, canonical alias etc. (Optional).
+    std::string generic_search_term;
+};
+
+void
+to_json(nlohmann::json &obj, const PublicRoomsFilter &req);
+
 //! Request payload for the `POST /_matrix/client/r0/publicRooms` endpoint.
 struct PublicRooms 
 {
@@ -149,7 +157,7 @@ struct PublicRooms
         //! rather than via an explicit flag.
         std::string since;
         //! Filter to apply to the results.
-        Filter filter;
+        PublicRoomsFilter filter;
         //! Whether or not to include all known networks/protocols from
         //! application services on the homeserver. Defaults to false.
         bool include_all_networks = false;
@@ -159,7 +167,7 @@ struct PublicRooms
 };
 
 void
-to_json(json &obj, const PostPublicRooms &request);
+to_json(json &obj, const PublicRooms &request);
 
 struct Empty
 {};
diff --git a/include/mtx/responses/public_rooms.hpp b/include/mtx/responses/public_rooms.hpp
index c0df3419e2249696e8cbfc515279498890118162..61fc55ab549eaf73ff389854357337c0726d3b81 100644
--- a/include/mtx/responses/public_rooms.hpp
+++ b/include/mtx/responses/public_rooms.hpp
@@ -9,10 +9,34 @@
 #include <nlohmann/json.hpp>
 #endif
 
-#include "mtx/public_rooms_chunk.hpp"
-
 namespace mtx {
 namespace responses {
+struct PublicRoomsChunk
+{
+    //! Aliases of the room. May be empty.
+    std::vector<std::string> aliases;
+    //! The canonical alias of the room, if any.
+    std::string canonical_alias;
+    //! The name of the room, if any.
+    std::string name;
+    //! **Required.** The number of members joined to the room.
+    int num_joined_members;
+    //! **Required.** The ID of the room.
+    std::string room_id;
+    //! The topic of the room, if any.
+    std::string topic;
+    //! **Required.** Whether the room may be viewed by guest users without joining.
+    bool world_readable;
+    //! **Required.** Whether guest users may join the room
+    //! and participate in it. If they can, they will be subject
+    //! to ordinary power level rules like any other user.
+    bool guest_can_join;
+    //! The URL for the room's avatar, if one is set.
+    std::string avatar_url; 
+};
+
+void
+from_json(const nlohmann::json &obj, PublicRoomsChunk &res);
 
 //! Response from the `GET /_matrix/client/r0/publicRooms` &
 //! `POST /_matrix/client/r0/publicRooms` endpoints.
diff --git a/include/mtxclient/http/client.hpp b/include/mtxclient/http/client.hpp
index 7b960e43d355640b1c967dc04377036764221f06..345dab66afaacef7b3c7efa962e9991522be8314 100644
--- a/include/mtxclient/http/client.hpp
+++ b/include/mtxclient/http/client.hpp
@@ -449,11 +449,14 @@ public:
           ErrCallback callback);
 
         //! POST a new room listing to the public rooms directory.
-        void post_public_rooms(const std::string &server, const nhlomann::json &j, 
-                                Callback<mtx::responses::PublicRooms> cb); 
+        void post_public_rooms(const mtx::requests::PublicRooms &req, 
+                                Callback<mtx::responses::PublicRooms> cb, 
+                                const std::string &server = "matrix.org"); 
         //! GET the public rooms directory listing.
-        void get_public_rooms(int limit, const std::string &since, const std::string &server,
-                                Callback<mtx::responses::PublicRooms> cb);
+        void get_public_rooms(Callback<mtx::responses::PublicRooms> cb, 
+                                const std::string &server = "matrix.org", 
+                                int limit = std::numeric_limits<int>::max(), 
+                                const std::string &since = "");
         //
         // Group related endpoints.
         //
diff --git a/lib/http/client.cpp b/lib/http/client.cpp
index 0ead749393d4a96d5b79a91df41fbfc8c200141a..9b93b787fcf9676ba16dcb1d9dece4c14d5dc165 100644
--- a/lib/http/client.cpp
+++ b/lib/http/client.cpp
@@ -963,21 +963,27 @@ Client::send_to_device(const std::string &event_type,
 }
 
 void
-Client::post_public_rooms(const std::string &server, const nhlomann::json &j, 
-                                Callback<mtx::responses::PublicRooms> cb)
-{
-        const auto api_path = "/client/r0/publicRooms" + " " + server;
+Client::post_public_rooms(const mtx::requests::PublicRooms &req, 
+                                Callback<mtx::responses::PublicRooms> cb, const std::string &server)
+{       
+        const auto api_path = "/client/r0/publicRooms?" + 
+                                mtx::client::utils::query_params({{"server", server}});
         post<mtx::requests::PublicRooms, mtx::responses::PublicRooms>(
-        api_path, j, cb);    
+        api_path, req, cb);    
 }
 
 void
-Client::get_public_rooms(int limit, const std::string &since, const std::string &server,
-                                Callback<mtx::responses::PublicRooms> cb) 
+Client::get_public_rooms(Callback<mtx::responses::PublicRooms> cb, const std::string &server, 
+                        int limit, const std::string &since) 
 {
         const auto api_path = 
-        "/client/r0/publicRooms" + " " + std::to_string(limit) + " " + since + " " + server;
-        get<mtx::requests::PublicRooms, mtx::responses::PublicRooms>(api_path, cb);
+        "/client/r0/publicRooms" +
+         mtx::client::utils::query_params({{"server", server}, {"limit", std::to_string(limit)}, {"since", since}});
+        
+        get<mtx::responses::PublicRooms>(api_path, 
+                                        [cb](const mtx::responses::PublicRooms &res,
+                                             HeaderFields,
+                                             RequestErr err) { cb(res, err); });
 }
 
 
diff --git a/lib/structs/requests.cpp b/lib/structs/requests.cpp
index c36ee42b3bb14442819a219b349707ba37168bf3..e89d6963653314ff22df72107782c43f93b11db0 100644
--- a/lib/structs/requests.cpp
+++ b/lib/structs/requests.cpp
@@ -111,6 +111,29 @@ to_json(json &obj, const TypingNotification &request)
         obj["timeout"] = request.timeout;
 }
 
+void
+to_json(json &obj, const PublicRoomsFilter &request)
+{
+        obj["generic_search_term"] = request.generic_search_term;
+}
+
+void
+to_json(json &obj, const PublicRooms &request)
+{
+        obj["limit"] = request.limit;
+        obj["since"] = request.since;
+        obj["filter"] = request.filter;
+
+        // Based on the spec, third_party_instance_id can only be used if
+        // include_all_networks is false. A case where the latter is true and
+        // the former is set is invalid.
+        if (request.include_all_networks && !request.third_party_instance_id.empty()) {
+                throw std::invalid_argument("third_party_instance_id id can only be set if include_all_networks is false");
+        }
+        obj["third_party_instance_id"] = request.third_party_instance_id;
+        obj["include_all_networks"] = !request.third_party_instance_id.empty();
+}
+
 void
 to_json(json &obj, const SignedOneTimeKey &request)
 {
diff --git a/lib/structs/responses/public_rooms.cpp b/lib/structs/responses/public_rooms.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b468c64288730ab1463abb601e792e7044029ab7
--- /dev/null
+++ b/lib/structs/responses/public_rooms.cpp
@@ -0,0 +1,71 @@
+#include <nlohmann/json.hpp>
+
+#include "mtx/identifiers.hpp"
+#include "mtx/responses/public_rooms.hpp"
+
+namespace mtx {
+namespace responses {
+
+void
+from_json(const nlohmann::json &obj, PublicRoomsChunk &res) 
+{
+    if (obj.count("aliases") != 0 && !obj.at("aliases").is_null()) {
+        res.aliases = obj.at("aliases").get<std::vector<std::string>>();
+    }
+
+    if (obj.count("canonical_alias") != 0 && !obj.at("canonical_alias").is_null()) {
+        res.canonical_alias = obj.at("canonical_alias").get<std::string>();
+    }
+
+    if (obj.count("name") != 0 && !obj.at("name").is_null()) {
+        res.name = obj.at("name").get<std::string>();
+    }
+
+    if (obj.count("num_joined_members") != 0 && !obj.at("num_joined_members").is_null()) {
+        res.num_joined_members = obj.at("num_joined_members").get<int>();
+    }
+
+    if (obj.count("room_id") != 0 && !obj.at("room_id").is_null()) {
+        res.room_id = obj.at("room_id").get<std::string>();
+    }
+
+    if (obj.count("topic") != 0 && !obj.at("topic").is_null()) {
+        res.topic = obj.at("topic").get<std::string>();
+    }
+
+    if (obj.count("world_readable") != 0 && !obj.at("world_readable").is_null()) {
+        res.world_readable = obj.at("world_readable").get<bool>();
+    }
+
+    if (obj.count("guest_can_join") != 0 && !obj.at("guest_can_join").is_null()) {
+        res.guest_can_join = obj.at("guest_can_join").get<bool>();
+    }
+
+    if (obj.count("avatar_url") != 0 && !obj.at("avatar_url").is_null()) {
+        res.avatar_url = obj.at("avatar_url").get<std::string>();
+    }
+}
+
+void
+from_json(const nlohmann::json &obj, PublicRooms &publicRooms)
+{
+    // PublicRoomsChunk is CopyConstructible & DefaultConstructible
+    if (obj.count("chunk") != 0 && !obj.at("chunk").is_null()) {
+        publicRooms.chunk = obj.at("chunk").get<std::vector<PublicRoomsChunk>>();
+    }
+
+    if (obj.count("next_batch") != 0 && !obj.at("next_batch").is_null()) {
+        publicRooms.next_batch = obj.at("next_batch").get<std::string>();
+    }
+
+    if (obj.count("prev_batch") != 0 && !obj.at("prev_batch").is_null()) {
+        publicRooms.prev_batch = obj.at("prev_batch").get<std::string>();
+    }
+
+    if (obj.count("total_room_count_estimate") != 0 && !obj.at("total_room_count_estimate").is_null()) {
+        publicRooms.total_room_count_estimate = obj.at("total_room_count_estimate").get<int>();
+    }
+}
+
+} // namespace responses
+} // namespace mtx
\ No newline at end of file