diff --git a/src/client.cpp b/src/client.cpp
index b370b0fdce37c3e09c0c6ef111a5718f4b65173a..71ebb954c8a443ff9f28261edf0817af9f8fb15e 100644
--- a/src/client.cpp
+++ b/src/client.cpp
@@ -562,47 +562,9 @@ Client::flow_response(const std::string &user,
 
 void
 Client::upload_keys(
-  const nlohmann::json &identity_keys,
-  const std::map<std::string, nlohmann::json> &one_time_keys,
+  const mtx::requests::UploadKeys &req,
   std::function<void(const mtx::responses::UploadKeys &res, RequestErr err)> callback)
 {
-        mtx::requests::UploadKeys req;
-        req.one_time_keys = one_time_keys;
-
-        req.device_keys.user_id   = user_id().to_string();
-        req.device_keys.device_id = device_id();
-        req.device_keys.keys.emplace("curve25519:" + device_id(), identity_keys["curve25519"]);
-        req.device_keys.keys.emplace("ed25519:" + device_id(), identity_keys["ed25519"]);
-
-        post<mtx::requests::UploadKeys, mtx::responses::UploadKeys>(
-          "/client/r0/keys/upload", req, callback);
-}
-
-void
-Client::upload_identity_keys(
-  const nlohmann::json &identity_keys,
-  std::function<void(const mtx::responses::UploadKeys &res, RequestErr err)> callback)
-{
-        mtx::requests::UploadKeys req;
-        req.device_keys.user_id   = user_id().to_string();
-        req.device_keys.device_id = device_id();
-        req.device_keys.keys.emplace("curve25519:" + device_id(), identity_keys["curve25519"]);
-        req.device_keys.keys.emplace("ed25519:" + device_id(), identity_keys["ed25519"]);
-
-        post<mtx::requests::UploadKeys, mtx::responses::UploadKeys>(
-          "/client/r0/keys/upload", req, callback);
-}
-
-void
-Client::upload_one_time_keys(
-  const std::map<std::string, nlohmann::json> &one_time_keys,
-  std::function<void(const mtx::responses::UploadKeys &res, RequestErr err)> callback)
-{
-        mtx::requests::UploadKeys req;
-        req.device_keys.user_id   = user_id().to_string();
-        req.device_keys.device_id = device_id();
-        req.one_time_keys         = one_time_keys;
-
         post<mtx::requests::UploadKeys, mtx::responses::UploadKeys>(
           "/client/r0/keys/upload", req, callback);
 }
diff --git a/src/client.hpp b/src/client.hpp
index 19ea23c353eff60d7f38e12cadd507144ff1d9cb..94509748ef32a2fca6a5f1ed381bf5a763da317e 100644
--- a/src/client.hpp
+++ b/src/client.hpp
@@ -12,6 +12,7 @@
 #include <boost/thread/thread.hpp>
 #include <json.hpp>
 
+#include "crypto.hpp"
 #include "errors.hpp"
 #include "mtx/requests.hpp"
 #include "mtx/responses.hpp"
@@ -201,22 +202,8 @@ public:
         //
 
         //! Upload identity keys & one time keys.
-        // TODO: Replace json with a proper type. API methods shouldn't throw.
         void upload_keys(
-          const nlohmann::json &identity_keys,
-          const std::map<std::string, nlohmann::json> &one_time_keys,
-          std::function<void(const mtx::responses::UploadKeys &res, RequestErr err)> cb);
-
-        //! Upload identity keys.
-        // TODO: Replace json with a proper type. API methods shouldn't throw.
-        void upload_identity_keys(
-          const nlohmann::json &identity_keys,
-          std::function<void(const mtx::responses::UploadKeys &res, RequestErr err)> cb);
-
-        //! Upload one time keys.
-        // TODO: Replace json with a proper type. API methods shouldn't throw.
-        void upload_one_time_keys(
-          const std::map<std::string, nlohmann::json> &one_time_keys,
+          const mtx::requests::UploadKeys &req,
           std::function<void(const mtx::responses::UploadKeys &res, RequestErr err)> cb);
 
 private:
diff --git a/src/crypto.cpp b/src/crypto.cpp
index 843d41574d7ce586741b54a33727faa9da39d18c..b9d260cb6fb6e459eacaf8116f1095609eae3c71 100644
--- a/src/crypto.cpp
+++ b/src/crypto.cpp
@@ -34,7 +34,7 @@ mtx::client::crypto::olm_new_account()
         return olm_account;
 }
 
-json
+IdentityKeys
 mtx::client::crypto::identity_keys(std::shared_ptr<olm::Account> account)
 {
         const auto nbytes = account->get_identity_json_length();
@@ -46,8 +46,27 @@ mtx::client::crypto::identity_keys(std::shared_ptr<olm::Account> account)
                 throw olm_exception("identity_keys", account->last_error);
 
         std::string data(buf.get(), buf.get() + nbytes);
+        IdentityKeys keys = json::parse(data);
 
-        return json::parse(data);
+        return keys;
+}
+
+std::string
+mtx::client::crypto::sign_identity_keys(std::shared_ptr<olm::Account> account,
+                                        const IdentityKeys &keys,
+                                        const mtx::identifiers::User &user_id,
+                                        const std::string &device_id)
+{
+        json body{{"algorithms", {"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}},
+                  {"user_id", user_id.to_string()},
+                  {"device_id", device_id},
+                  {"keys",
+                   {
+                     {"curve25519:" + device_id, keys.curve25519},
+                     {"ed25519:" + device_id, keys.ed25519},
+                   }}};
+
+        return encode_base64(sign_message(account, body.dump()).get(), SIGNATURE_SIZE);
 }
 
 std::size_t
@@ -88,6 +107,24 @@ mtx::client::crypto::sign_one_time_key(std::shared_ptr<olm::Account> account,
         return encode_base64(signature_buf.get(), SIGNATURE_SIZE);
 }
 
+std::map<std::string, json>
+mtx::client::crypto::sign_one_time_keys(std::shared_ptr<olm::Account> account,
+                                        const mtx::client::crypto::OneTimeKeys &keys,
+                                        const mtx::identifiers::User &user_id,
+                                        const std::string &device_id)
+{
+        // Sign & append the one time keys.
+        std::map<std::string, json> signed_one_time_keys;
+        for (const auto &elem : keys.curve25519) {
+                auto sig = sign_one_time_key(account, elem.second);
+
+                signed_one_time_keys["signed_curve25519:" + elem.first] =
+                  signed_one_time_key_json(user_id, device_id, elem.second, sig);
+        }
+
+        return signed_one_time_keys;
+}
+
 std::unique_ptr<uint8_t[]>
 mtx::client::crypto::sign_message(std::shared_ptr<olm::Account> account, const std::string &msg)
 {
diff --git a/src/crypto.hpp b/src/crypto.hpp
index 3c961daeb809efc9900533b011b0a1f4d71be910..ff6dbf0a6270b4104b16ef17df184159c733c225 100644
--- a/src/crypto.hpp
+++ b/src/crypto.hpp
@@ -8,10 +8,53 @@
 #include <olm/account.hh>
 #include <olm/error.h>
 
+static constexpr const char *ED25519    = "ed25519";
+static constexpr const char *CURVE25519 = "curve25519";
+
 namespace mtx {
 namespace client {
 namespace crypto {
 
+struct IdentityKeys
+{
+        std::string curve25519;
+        std::string ed25519;
+};
+
+inline void
+to_json(nlohmann::json &obj, const IdentityKeys &keys)
+{
+        obj[ED25519]    = keys.ed25519;
+        obj[CURVE25519] = keys.curve25519;
+}
+
+inline void
+from_json(const nlohmann::json &obj, IdentityKeys &keys)
+{
+        keys.ed25519    = obj.at(ED25519).get<std::string>();
+        keys.curve25519 = obj.at(CURVE25519).get<std::string>();
+}
+
+struct OneTimeKeys
+{
+        using KeyId      = std::string;
+        using EncodedKey = std::string;
+
+        std::map<KeyId, EncodedKey> curve25519;
+};
+
+inline void
+to_json(nlohmann::json &obj, const OneTimeKeys &keys)
+{
+        obj["curve25519"] = keys.curve25519;
+}
+
+inline void
+from_json(const nlohmann::json &obj, OneTimeKeys &keys)
+{
+        keys.curve25519 = obj.at("curve25519").get<std::map<std::string, std::string>>();
+}
+
 class olm_exception : public std::exception
 {
 public:
@@ -35,7 +78,7 @@ std::shared_ptr<olm::Account>
 olm_new_account();
 
 //! Retrieve the json representation of the identity keys for the given account.
-nlohmann::json
+IdentityKeys
 identity_keys(std::shared_ptr<olm::Account> user);
 
 //! Generate a number of one time keys.
@@ -54,6 +97,13 @@ create_buffer(std::size_t nbytes);
 std::string
 sign_one_time_key(std::shared_ptr<olm::Account> account, const std::string &key);
 
+//! Sign the identity keys. The result should be used as part of the /keys/upload/ request.
+std::string
+sign_identity_keys(std::shared_ptr<olm::Account> account,
+                   const IdentityKeys &keys,
+                   const mtx::identifiers::User &user_id,
+                   const std::string &device_id);
+
 //! Sign the given message.
 std::unique_ptr<uint8_t[]>
 sign_message(std::shared_ptr<olm::Account> account, const std::string &msg);
@@ -65,6 +115,13 @@ signed_one_time_key_json(const mtx::identifiers::User &user_id,
                          const std::string &key,
                          const std::string &signature);
 
+//! Sign one_time_keys and generate the appropriate structure for the /keys/upload request.
+std::map<std::string, nlohmann::json>
+sign_one_time_keys(std::shared_ptr<olm::Account> account,
+                   const mtx::client::crypto::OneTimeKeys &keys,
+                   const mtx::identifiers::User &user_id,
+                   const std::string &device_id);
+
 std::string
 encode_base64(const uint8_t *data, std::size_t len);
 
diff --git a/tests/e2ee.cpp b/tests/e2ee.cpp
index 10573c8b6131dcd2672e2c716bbb192fe3e7199e..fbe7e8415ec1363d9096743a678760147b53a226 100644
--- a/tests/e2ee.cpp
+++ b/tests/e2ee.cpp
@@ -24,6 +24,52 @@ using namespace std;
 
 using ErrType = std::experimental::optional<errors::ClientError>;
 
+std::map<std::string, json>
+sign_one_time_keys(std::shared_ptr<olm::Account> account,
+                   const mtx::client::crypto::OneTimeKeys &keys,
+                   const mtx::identifiers::User &user_id,
+                   const std::string &device_id)
+{
+        // Sign & append the one time keys.
+        std::map<std::string, json> signed_one_time_keys;
+        for (const auto &elem : keys.curve25519) {
+                auto sig = mtx::client::crypto::sign_one_time_key(account, elem.second);
+
+                signed_one_time_keys["signed_curve25519:" + elem.first] =
+                  mtx::client::crypto::signed_one_time_key_json(
+                    user_id, device_id, elem.second, sig);
+        }
+
+        return signed_one_time_keys;
+}
+
+mtx::requests::UploadKeys
+create_upload_keys_request(std::shared_ptr<olm::Account> account,
+                           const mtx::client::crypto::IdentityKeys &identity_keys,
+                           const mtx::client::crypto::OneTimeKeys &one_time_keys,
+                           const mtx::identifiers::User &user_id,
+                           const string &device_id)
+{
+        mtx::requests::UploadKeys req;
+        req.device_keys.user_id   = user_id.to_string();
+        req.device_keys.device_id = device_id;
+
+        req.device_keys.keys["curve25519:" + device_id] = identity_keys.curve25519;
+        req.device_keys.keys["ed25519:" + device_id]    = identity_keys.ed25519;
+
+        // Generate and add the signature to the request.
+        auto sig = sign_identity_keys(account, identity_keys, user_id, device_id);
+        req.device_keys.signatures[user_id.to_string()]["ed25519:" + device_id] = sig;
+
+        if (one_time_keys.curve25519.empty())
+                return req;
+
+        // Sign & append the one time keys.
+        req.one_time_keys = ::sign_one_time_keys(account, one_time_keys, user_id, device_id);
+
+        return req;
+}
+
 void
 check_error(ErrType err)
 {
@@ -52,19 +98,20 @@ TEST(Encryption, UploadIdentityKeys)
         while (alice->access_token().empty())
                 std::this_thread::sleep_for(std::chrono::milliseconds(100));
 
-        json identity_keys = mtx::client::crypto::identity_keys(olm_account);
+        auto identity_keys = mtx::client::crypto::identity_keys(olm_account);
 
-        ASSERT_TRUE(identity_keys.find("curve25519") != identity_keys.end());
-        ASSERT_TRUE(identity_keys.at("curve25519").get<std::string>().size() > 10);
+        ASSERT_TRUE(identity_keys.curve25519.size() > 10);
+        ASSERT_TRUE(identity_keys.curve25519.size() > 10);
 
-        ASSERT_TRUE(identity_keys.find("ed25519") != identity_keys.end());
-        ASSERT_TRUE(identity_keys.at("ed25519").get<std::string>().size() > 10);
+        mtx::client::crypto::OneTimeKeys unused;
+        auto request = ::create_upload_keys_request(
+          olm_account, identity_keys, unused, alice->user_id(), alice->device_id());
 
-        alice->upload_identity_keys(identity_keys,
-                                    [](const mtx::responses::UploadKeys &res, ErrType err) {
-                                            check_error(err);
-                                            EXPECT_EQ(res.one_time_key_counts.size(), 0);
-                                    });
+        // Make the request with the signed identity keys.
+        alice->upload_keys(request, [](const mtx::responses::UploadKeys &res, ErrType err) {
+                check_error(err);
+                EXPECT_EQ(res.one_time_key_counts.size(), 0);
+        });
 
         alice->close();
 }
@@ -85,14 +132,18 @@ TEST(Encryption, UploadOneTimeKeys)
 
         auto one_time_keys = mtx::client::crypto::one_time_keys(olm_account);
 
+        mtx::requests::UploadKeys req;
+
         // Create the proper structure for uploading.
-        std::map<std::string, json> keys;
+        std::map<std::string, json> unsigned_keys;
 
         auto obj = one_time_keys.at("curve25519");
         for (auto it = obj.begin(); it != obj.end(); ++it)
-                keys["curve25519:" + it.key()] = it.value();
+                unsigned_keys["curve25519:" + it.key()] = it.value();
 
-        alice->upload_one_time_keys(keys, [](const mtx::responses::UploadKeys &res, ErrType err) {
+        req.one_time_keys = unsigned_keys;
+
+        alice->upload_keys(req, [](const mtx::responses::UploadKeys &res, ErrType err) {
                 check_error(err);
                 EXPECT_EQ(res.one_time_key_counts.size(), 1);
                 EXPECT_EQ(res.one_time_key_counts.at("curve25519"), 5);
@@ -117,24 +168,15 @@ TEST(Encryption, UploadSignedOneTimeKeys)
 
         auto one_time_keys = mtx::client::crypto::one_time_keys(olm_account);
 
-        // Create the proper structure for uploading.
-        std::map<std::string, json> signed_keys;
-
-        auto obj = one_time_keys.at("curve25519");
-        for (auto it = obj.begin(); it != obj.end(); ++it) {
-                auto sig = mtx::client::crypto::sign_one_time_key(olm_account, it.value());
-
-                signed_keys["signed_curve25519:" + it.key()] =
-                  mtx::client::crypto::signed_one_time_key_json(
-                    alice->user_id(), alice->device_id(), it.value(), sig);
-        }
+        mtx::requests::UploadKeys req;
+        req.one_time_keys =
+          ::sign_one_time_keys(olm_account, one_time_keys, alice->user_id(), alice->device_id());
 
-        alice->upload_one_time_keys(
-          signed_keys, [nkeys](const mtx::responses::UploadKeys &res, ErrType err) {
-                  check_error(err);
-                  EXPECT_EQ(res.one_time_key_counts.size(), 1);
-                  EXPECT_EQ(res.one_time_key_counts.at("signed_curve25519"), nkeys);
-          });
+        alice->upload_keys(req, [nkeys](const mtx::responses::UploadKeys &res, ErrType err) {
+                check_error(err);
+                EXPECT_EQ(res.one_time_key_counts.size(), 1);
+                EXPECT_EQ(res.one_time_key_counts.at("signed_curve25519"), nkeys);
+        });
 
         alice->close();
 }
@@ -155,24 +197,14 @@ TEST(Encryption, UploadKeys)
         mtx::client::crypto::generate_one_time_keys(olm_account, 1);
         auto one_time_keys = mtx::client::crypto::one_time_keys(olm_account);
 
-        // Create the proper structure for uploading.
-        std::map<std::string, json> signed_keys;
-
-        auto obj = one_time_keys.at("curve25519");
-        for (auto it = obj.begin(); it != obj.end(); ++it) {
-                auto sig = mtx::client::crypto::sign_one_time_key(olm_account, it.value());
+        auto req = ::create_upload_keys_request(
+          olm_account, identity_keys, one_time_keys, alice->user_id(), alice->device_id());
 
-                signed_keys["signed_curve25519:" + it.key()] =
-                  mtx::client::crypto::signed_one_time_key_json(
-                    alice->user_id(), alice->device_id(), it.value(), sig);
-        }
-
-        alice->upload_keys(
-          identity_keys, signed_keys, [](const mtx::responses::UploadKeys &res, ErrType err) {
-                  check_error(err);
-                  EXPECT_EQ(res.one_time_key_counts.size(), 1);
-                  EXPECT_EQ(res.one_time_key_counts.at("signed_curve25519"), 1);
-          });
+        alice->upload_keys(req, [](const mtx::responses::UploadKeys &res, ErrType err) {
+                check_error(err);
+                EXPECT_EQ(res.one_time_key_counts.size(), 1);
+                EXPECT_EQ(res.one_time_key_counts.at("signed_curve25519"), 1);
+        });
 
         alice->close();
 }