diff --git a/CMakeLists.txt b/CMakeLists.txt
index 018735618ff7493b3923d0663898b7afbff9cfa6..55e296d486ea952f7320d5d4d01316fbc1d5a12c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -382,7 +382,7 @@ if (OPENDHT_TESTS)
        ${CMAKE_THREAD_LIBS_INIT}
        ${CPPUNIT_LIBRARIES}
        ${GNUTLS_LIBRARIES}
-       ssl crypto
+       ssl crypto jsoncpp
     )
     enable_testing()
     add_test(TEST opendht_unit_tests)
diff --git a/include/opendht/dht_proxy_server.h b/include/opendht/dht_proxy_server.h
index 8ffd9ac8a67e76d988985fabc7ab30d70dc892a2..685c56ccc75e7c06821778e0c448954f45a61ca0 100644
--- a/include/opendht/dht_proxy_server.h
+++ b/include/opendht/dht_proxy_server.h
@@ -83,6 +83,8 @@ public:
     DhtProxyServer& operator=(const DhtProxyServer& other) = delete;
     DhtProxyServer& operator=(DhtProxyServer&& other) = delete;
 
+    asio::io_context& io_context() const;
+
     struct ServerStats {
         /** Current number of listen operations */
         size_t listenCount;
@@ -310,8 +312,6 @@ private:
 
     void handlePrintStats(const asio::error_code &ec);
 
-    asio::io_context& io_context() const;
-
     using clock = std::chrono::steady_clock;
     using time_point = clock::time_point;
 
diff --git a/include/opendht/http.h b/include/opendht/http.h
index 2b70366cd7e7050e3b354cb59c662d5402b03c42..c087e9c1933612c88440f87b3ac375453613db3d 100644
--- a/include/opendht/http.h
+++ b/include/opendht/http.h
@@ -34,6 +34,7 @@
 
 #include <memory>
 #include <queue>
+#include <json/json.h>
 
 extern "C" {
 struct http_parser;
@@ -200,8 +201,11 @@ public:
     using OnStatusCb = std::function<void(unsigned int status_code)>;
     using OnDataCb = std::function<void(const char* at, size_t length)>;
     using OnStateChangeCb = std::function<void(State state, const Response& response)>;
+    using OnJsonCb = std::function<void(Json::Value value, unsigned int status_code)>;
 
     // resolves implicitly
+    Request(asio::io_context& ctx, const std::string& url, const Json::Value& json, OnJsonCb jsoncb,
+            std::shared_ptr<dht::Logger> logger = {});
     Request(asio::io_context& ctx, const std::string& url, std::shared_ptr<dht::Logger> logger = {});
     Request(asio::io_context& ctx, const std::string& host, const std::string& service,
             const bool ssl = false, std::shared_ptr<dht::Logger> logger = {});
diff --git a/src/http.cpp b/src/http.cpp
index b60629ff0cdeab3cbee3274bb47ac38f59a1afb8..2ffc60a2671bad04e8875b4344c518b6e9e413bc 100644
--- a/src/http.cpp
+++ b/src/http.cpp
@@ -401,11 +401,42 @@ Resolver::resolve(const std::string& host, const std::string& service)
 
 unsigned int Request::ids_ = 1;
 
+
+Request::Request(asio::io_context& ctx, const std::string& url, const Json::Value& json, OnJsonCb jsoncb,
+                 std::shared_ptr<dht::Logger> logger)
+    : id_(Request::ids_++), ctx_(ctx), logger_(logger)
+{
+    cbs_ = std::make_unique<Callbacks>();
+    resolver_ = std::make_shared<Resolver>(ctx, url, logger_);
+
+    set_header_field(restinio::http_field_t::host, get_url().host + ":" + get_url().service);
+    set_target(resolver_->get_url().target);
+    set_header_field(restinio::http_field_t::content_type, "application/json");
+    set_header_field(restinio::http_field_t::accept, "application/json");
+    Json::StreamWriterBuilder wbuilder;
+    set_body(Json::writeString(wbuilder, json));
+
+    add_on_state_change_callback([this, jsoncb](State state, const Response& response){
+        if (state != Request::State::DONE)
+            return;
+        Json::Value json;
+        std::string err;
+        Json::CharReaderBuilder rbuilder;
+        auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
+        if (!reader->parse(response.body.data(), response.body.data() + response.body.size(), &json, &err) and logger_)
+            logger_->e("[http:client]  [request:%i] can't parse response to json", id_, err.c_str());
+        if (jsoncb)
+            jsoncb(json, response.status_code);
+    });
+}
+
 Request::Request(asio::io_context& ctx, const std::string& url, std::shared_ptr<dht::Logger> logger)
     : id_(Request::ids_++), ctx_(ctx), logger_(logger)
 {
     cbs_ = std::make_unique<Callbacks>();
     resolver_ = std::make_shared<Resolver>(ctx, url, logger_);
+
+    set_header_field(restinio::http_field_t::host, get_url().host + ":" + get_url().service);
     set_target(resolver_->get_url().target);
 }
 
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 3515a15f8ad7d5583408f1f8f74af5a824dd4d66..13ea421dc2fe9641aeb49af949cb0367639fc826 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -1,9 +1,9 @@
 if ENABLE_TESTS
 bin_PROGRAMS = opendht_unit_tests
 
-AM_CPPFLAGS = -I../include
+AM_CPPFLAGS = -I../include -DOPENDHT_JSONCPP
 
 nobase_include_HEADERS = infohashtester.h valuetester.h cryptotester.h dhtrunnertester.h httptester.h dhtproxytester.h
 opendht_unit_tests_SOURCES = tests_runner.cpp cryptotester.cpp infohashtester.cpp valuetester.cpp dhtrunnertester.cpp httptester.cpp dhtproxytester.cpp
-opendht_unit_tests_LDFLAGS = -lopendht -lcppunit -L@top_builddir@/src/.libs @GnuTLS_LIBS@
+opendht_unit_tests_LDFLAGS = -lopendht -lcppunit -ljsoncpp -L@top_builddir@/src/.libs @GnuTLS_LIBS@
 endif
diff --git a/tests/compile_http.sh b/tests/compile_http.sh
new file mode 100755
index 0000000000000000000000000000000000000000..0f2e1a08b2f8acf860f3b3c5000c6779ac6fdb94
--- /dev/null
+++ b/tests/compile_http.sh
@@ -0,0 +1 @@
+g++ -Wall -Wextra -Wfatal-errors -pedantic -g ../src/http.cpp tests_runner.cpp httptester.cpp -o run.out -lopendht -lpthread -lcppunit -ljsoncpp -DOPENDHT_JSONCPP -I../include/opendht -DASIO_STANDALONE
diff --git a/tests/dhtproxytester.cpp b/tests/dhtproxytester.cpp
index 2153870d723469e5d747475f46946bbc9cd603db..97db95787544e03b307cf71d367bb828b6316dd8 100644
--- a/tests/dhtproxytester.cpp
+++ b/tests/dhtproxytester.cpp
@@ -46,7 +46,7 @@ DhtProxyTester::setUp() {
 
     serverProxy = std::unique_ptr<dht::DhtProxyServer>(
         new dht::DhtProxyServer(
-            ///*http*/nullptr,
+            ///*http*/dht::crypto::Identity{},
             /*https*/serverIdentity,
             nodeProxy, 8080, /*pushServer*/"127.0.0.1:8090", logger));
 
diff --git a/tests/httptester.cpp b/tests/httptester.cpp
index 0a6a861573625432bf89006cf6999adf7a13a34b..2493683c004298a688ab6f47fe8d763eb223ae13 100644
--- a/tests/httptester.cpp
+++ b/tests/httptester.cpp
@@ -33,10 +33,27 @@ CPPUNIT_TEST_SUITE_REGISTRATION(HttpTester);
 void
 HttpTester::setUp() {
     logger = dht::log::getStdLogger();
+
+    nodePeer.run(0, /*identity*/{}, /*threaded*/true);
+
+    nodeProxy = std::make_shared<dht::DhtRunner>();
+    nodeProxy->run(0, /*identity*/{}, /*threaded*/true);
+    nodeProxy->bootstrap(nodePeer.getBound());
+
+    serverProxy = std::unique_ptr<dht::DhtProxyServer>(
+        new dht::DhtProxyServer(
+            /*http*/dht::crypto::Identity{}, nodeProxy, 8080, /*pushServer*/"127.0.0.1:8090", logger));
+
 }
 
 void
 HttpTester::tearDown() {
+    logger->d("[tester:http] stopping peer node");
+    nodePeer.join();
+    logger->d("[tester:http] stopping proxy server");
+    serverProxy.reset(nullptr);
+    logger->d("[tester:http] stopping proxy node");
+    nodeProxy->join();
 }
 
 void
@@ -210,4 +227,38 @@ HttpTester::test_parse_url_target_ipv6() {
     CPPUNIT_ASSERT(parsed.target == "/going/under");
 }
 
+void
+HttpTester::test_send_json() {
+    // Arrange
+    std::condition_variable cv;
+    std::mutex cv_m;
+    std::unique_lock<std::mutex> lk(cv_m);
+    bool done = false;
+
+    dht::Value val {"hey"};
+    auto json = val.toJson();
+    json["permanent"] = false;
+    std::cout << "[test_send_json] sending:\n" << json << std::endl;
+    Json::Value resp_val;
+    unsigned int status = 0;
+    std::string url = "http://127.0.0.1:8080/key";
+    // Act
+    auto request = std::make_shared<dht::http::Request>(serverProxy->io_context(), url, json,
+                   [this, &cv, &done, &status, &resp_val](Json::Value value, unsigned int status_code){
+        if (status_code != 200 and logger)
+            logger->e("[tester] [status] failed with code=%i", status_code);
+        std::cout << "[tester] got response:\n" << value << std::endl;
+        resp_val = value;
+        status = status_code;
+        done = true;
+        cv.notify_all();
+    }, logger);
+    request->set_method(restinio::http_method_post());
+    request->send();
+    // Assert
+    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(10), [&]{ return done; }));
+    CPPUNIT_ASSERT(status == 200);
+    CPPUNIT_ASSERT(resp_val["data"] == val.toJson()["data"]);
+}
+
 }  // namespace test
diff --git a/tests/httptester.h b/tests/httptester.h
index 83df193f544c85bb45f716aacad4a2ee98dec183..862ebec0247408b19cc5a458eab79467f33113a3 100644
--- a/tests/httptester.h
+++ b/tests/httptester.h
@@ -23,13 +23,18 @@
 #include <cppunit/TestFixture.h>
 #include <cppunit/extensions/HelperMacros.h>
 
+#include <opendht/value.h>
 #include <opendht/log.h>
 #include <opendht/http.h>
 
+#include <opendht/dhtrunner.h>
+#include <opendht/dht_proxy_server.h>
+
 namespace test {
 
 class HttpTester : public CppUnit::TestFixture {
     CPPUNIT_TEST_SUITE(HttpTester);
+    // parse_url
     CPPUNIT_TEST(test_parse_url);
     CPPUNIT_TEST(test_parse_https_url_no_service);
     CPPUNIT_TEST(test_parse_url_no_prefix_no_target);
@@ -42,6 +47,8 @@ class HttpTester : public CppUnit::TestFixture {
     CPPUNIT_TEST(test_parse_url_ipv6);
     CPPUNIT_TEST(test_parse_url_no_prefix_no_target_ipv6);
     CPPUNIT_TEST(test_parse_url_target_ipv6);
+    // send
+    CPPUNIT_TEST(test_send_json);
     CPPUNIT_TEST_SUITE_END();
 
  public:
@@ -75,9 +82,17 @@ class HttpTester : public CppUnit::TestFixture {
    void test_parse_url_ipv6();
    void test_parse_url_no_prefix_no_target_ipv6();
    void test_parse_url_target_ipv6();
+    /**
+     * Test send(json)
+     */
+   void test_send_json();
 
  private:
     std::shared_ptr<dht::Logger> logger {};
+
+    dht::DhtRunner nodePeer;
+    std::shared_ptr<dht::DhtRunner> nodeProxy;
+    std::unique_ptr<dht::DhtProxyServer> serverProxy;
 };
 
 }  // namespace test