diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 67ac6f187..e44fb61bd 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -135,6 +135,8 @@ PRIVATE
     api/api_peer_photo.h
     api/api_polls.cpp
     api/api_polls.h
+    api/api_ringtones.cpp
+    api/api_ringtones.h
     api/api_self_destruct.cpp
     api/api_self_destruct.h
     api/api_send_progress.cpp
diff --git a/Telegram/SourceFiles/api/api_ringtones.cpp b/Telegram/SourceFiles/api/api_ringtones.cpp
new file mode 100644
index 000000000..3ead9ee41
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_ringtones.cpp
@@ -0,0 +1,119 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#include "api/api_ringtones.h"
+
+#include "apiwrap.h"
+#include "base/random.h"
+#include "base/unixtime.h"
+#include "data/data_session.h"
+#include "main/main_session.h"
+#include "storage/file_upload.h"
+#include "storage/localimageloader.h"
+
+namespace Api {
+namespace {
+
+SendMediaReady PrepareRingtoneDocument(
+		MTP::DcId dcId,
+		const QString &filename,
+		const QString &filemime,
+		const QByteArray &content) {
+	auto attributes = QVector<MTPDocumentAttribute>(
+		1,
+		MTP_documentAttributeFilename(MTP_string(filename)));
+	const auto id = base::RandomValue<DocumentId>();
+	const auto document = MTP_document(
+		MTP_flags(0),
+		MTP_long(id),
+		MTP_long(0),
+		MTP_bytes(),
+		MTP_int(base::unixtime::now()),
+		MTP_string(filemime),
+		MTP_int(content.size()),
+		MTP_vector<MTPPhotoSize>(),
+		MTPVector<MTPVideoSize>(),
+		MTP_int(dcId),
+		MTP_vector<MTPDocumentAttribute>(std::move(attributes)));
+
+	return SendMediaReady(
+		SendMediaType::File,
+		QString(), // filepath
+		filename,
+		content.size(),
+		content,
+		id,
+		0,
+		QString(),
+		PeerId(),
+		MTP_photoEmpty(MTP_long(0)),
+		PreparedPhotoThumbs(),
+		document,
+		QByteArray(),
+		0);
+}
+
+} // namespace
+
+Ringtones::Ringtones(not_null<ApiWrap*> api)
+: _session(&api->session())
+, _api(&api->instance()) {
+	crl::on_main(_session, [=] {
+		// You can't use _session->lifetime() in the constructor,
+		// only queued, because it is not constructed yet.
+		_session->uploader().documentReady(
+		) | rpl::start_with_next([=](const Storage::UploadedMedia &data) {
+			ready(data.fullId, data.info.file);
+		}, _session->lifetime());
+	});
+}
+
+void Ringtones::upload(
+		const QString &filename,
+		const QString &filemime,
+		const QByteArray &content) {
+	const auto ready = PrepareRingtoneDocument(
+		_api.instance().mainDcId(),
+		filename,
+		filemime,
+		content);
+
+	const auto uploadedData = UploadedData{ filename, filemime };
+	const auto fakeId = FullMsgId(
+		_session->userPeerId(),
+		_session->data().nextLocalMessageId());
+	const auto already = ranges::find_if(
+		_uploads,
+		[&](const auto &d) {
+			return uploadedData.filemime == d.second.filemime
+				&& uploadedData.filename == d.second.filename;
+		});
+	if (already != end(_uploads)) {
+		_session->uploader().cancel(already->first);
+		_uploads.erase(already);
+	}
+	_uploads.emplace(fakeId, uploadedData);
+	_session->uploader().uploadMedia(fakeId, ready);
+}
+
+void Ringtones::ready(const FullMsgId &msgId, const MTPInputFile &file) {
+	const auto maybeUploadedData = _uploads.take(msgId);
+	if (!maybeUploadedData) {
+		return;
+	}
+	const auto uploadedData = *maybeUploadedData;
+	_api.request(MTPaccount_UploadRingtone(
+		file,
+		MTP_string(uploadedData.filename),
+		MTP_string(uploadedData.filemime)
+	)).done([=](const MTPDocument &result) {
+		_session->data().processDocument(result);
+	}).fail([](const MTP::Error &error) {
+	}).send();
+}
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/api/api_ringtones.h b/Telegram/SourceFiles/api/api_ringtones.h
new file mode 100644
index 000000000..1bf828c96
--- /dev/null
+++ b/Telegram/SourceFiles/api/api_ringtones.h
@@ -0,0 +1,44 @@
+/*
+This file is part of Telegram Desktop,
+the official desktop application for the Telegram messaging service.
+
+For license and copyright information please follow this link:
+https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
+*/
+#pragma once
+
+#include "mtproto/sender.h"
+
+class ApiWrap;
+class PeerData;
+
+namespace Main {
+class Session;
+} // namespace Main
+
+namespace Api {
+
+class Ringtones final {
+public:
+	explicit Ringtones(not_null<ApiWrap*> api);
+
+	void upload(
+		const QString &filename,
+		const QString &filemime,
+		const QByteArray &content);
+
+private:
+	struct UploadedData {
+		QString filename;
+		QString filemime;
+	};
+	void ready(const FullMsgId &msgId, const MTPInputFile &file);
+
+	const not_null<Main::Session*> _session;
+	MTP::Sender _api;
+
+	base::flat_map<FullMsgId, UploadedData> _uploads;
+
+};
+
+} // namespace Api
diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp
index 9265e809d..64f219445 100644
--- a/Telegram/SourceFiles/apiwrap.cpp
+++ b/Telegram/SourceFiles/apiwrap.cpp
@@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "api/api_views.h"
 #include "api/api_confirm_phone.h"
 #include "api/api_unread_things.h"
+#include "api/api_ringtones.h"
 #include "data/notify/data_notify_settings.h"
 #include "data/stickers/data_stickers.h"
 #include "data/data_drafts.h"
@@ -141,7 +142,8 @@ ApiWrap::ApiWrap(not_null<Main::Session*> session)
 , _peerPhoto(std::make_unique<Api::PeerPhoto>(this))
 , _polls(std::make_unique<Api::Polls>(this))
 , _chatParticipants(std::make_unique<Api::ChatParticipants>(this))
-, _unreadThings(std::make_unique<Api::UnreadThings>(this)) {
+, _unreadThings(std::make_unique<Api::UnreadThings>(this))
+, _ringtones(std::make_unique<Api::Ringtones>(this)) {
 	crl::on_main(session, [=] {
 		// You can't use _session->lifetime() in the constructor,
 		// only queued, because it is not constructed yet.
@@ -4066,3 +4068,7 @@ Api::ChatParticipants &ApiWrap::chatParticipants() {
 Api::UnreadThings &ApiWrap::unreadThings() {
 	return *_unreadThings;
 }
+
+Api::Ringtones &ApiWrap::ringtones() {
+	return *_ringtones;
+}
diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h
index 93870829d..7535336ba 100644
--- a/Telegram/SourceFiles/apiwrap.h
+++ b/Telegram/SourceFiles/apiwrap.h
@@ -68,6 +68,7 @@ class PeerPhoto;
 class Polls;
 class ChatParticipants;
 class UnreadThings;
+class Ringtones;
 
 namespace details {
 
@@ -357,6 +358,7 @@ public:
 	[[nodiscard]] Api::Polls &polls();
 	[[nodiscard]] Api::ChatParticipants &chatParticipants();
 	[[nodiscard]] Api::UnreadThings &unreadThings();
+	[[nodiscard]] Api::Ringtones &ringtones();
 
 	void updatePrivacyLastSeens();
 
@@ -638,6 +640,7 @@ private:
 	const std::unique_ptr<Api::Polls> _polls;
 	const std::unique_ptr<Api::ChatParticipants> _chatParticipants;
 	const std::unique_ptr<Api::UnreadThings> _unreadThings;
+	const std::unique_ptr<Api::Ringtones> _ringtones;
 
 	mtpRequestId _wallPaperRequestId = 0;
 	QString _wallPaperSlug;