diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 95995112a7..e650614290 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -1583,6 +1583,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_passport_confirm_phone" = "We've sent an SMS with a confirmation code to your phone {phone}.";
 "lng_passport_confirm_email" = "We've sent a confirmation code to your email {email}.";
 "lng_passport_sure_cancel" = "If you continue your changes will be lost.";
+"lng_passport_scans_limit_reached" = "Scans limit reached.";
 
 // Wnd specific
 
diff --git a/Telegram/SourceFiles/info/profile/info_profile_button.cpp b/Telegram/SourceFiles/info/profile/info_profile_button.cpp
index 0e3dc71c6b..8258a97a47 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_button.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_button.cpp
@@ -58,6 +58,11 @@ rpl::producer<bool> Button::toggledValue() const {
 	return rpl::never<bool>();
 }
 
+void Button::setColorOverride(base::optional<QColor> textColorOverride) {
+	_textColorOverride = textColorOverride;
+	update();
+}
+
 void Button::paintEvent(QPaintEvent *e) {
 	Painter p(this);
 
@@ -69,7 +74,11 @@ void Button::paintEvent(QPaintEvent *e) {
 
 	auto outerw = width();
 	p.setFont(_st.font);
-	p.setPen(paintOver ? _st.textFgOver : _st.textFg);
+	p.setPen(_textColorOverride
+		? QPen(*_textColorOverride)
+		: paintOver
+		? _st.textFgOver
+		: _st.textFg);
 	p.drawTextLeft(
 		_st.padding.left(),
 		_st.padding.top(),
diff --git a/Telegram/SourceFiles/info/profile/info_profile_button.h b/Telegram/SourceFiles/info/profile/info_profile_button.h
index 44bb2bb5bc..6bf04af406 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_button.h
+++ b/Telegram/SourceFiles/info/profile/info_profile_button.h
@@ -29,6 +29,8 @@ public:
 	Button *toggleOn(rpl::producer<bool> &&toggled);
 	rpl::producer<bool> toggledValue() const;
 
+	void setColorOverride(base::optional<QColor> textColorOverride);
+
 protected:
 	int resizeGetHeight(int newWidth) override;
 	void onStateChanged(
@@ -48,6 +50,7 @@ private:
 	int _originalWidth = 0;
 	int _textWidth = 0;
 	std::unique_ptr<Ui::ToggleView> _toggle;
+	base::optional<QColor> _textColorOverride;
 
 };
 
diff --git a/Telegram/SourceFiles/passport/passport_form_controller.cpp b/Telegram/SourceFiles/passport/passport_form_controller.cpp
index f10047fb12..c55c74c3ce 100644
--- a/Telegram/SourceFiles/passport/passport_form_controller.cpp
+++ b/Telegram/SourceFiles/passport/passport_form_controller.cpp
@@ -25,6 +25,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 namespace Passport {
 namespace {
 
+constexpr auto kDocumentScansLimit = 20;
+
 QImage ReadImage(bytes::const_span buffer) {
 	return App::readImage(QByteArray::fromRawData(
 		reinterpret_cast<const char*>(buffer.data()),
@@ -241,11 +243,14 @@ auto FormController::prepareFinalData() const -> FinalData {
 			addValueToJSON(key, value);
 		}
 	};
+	auto hasErrors = false;
 	const auto scopes = ComputeScopes(this);
 	for (const auto &scope : scopes) {
 		const auto ready = ComputeScopeRowReadyString(scope);
 		if (ready.isEmpty()) {
+			hasErrors = true;
 			_valueError.fire_copy(scope.fields);
+			continue;
 		}
 		addValue(scope.fields);
 		if (!scope.documents.empty()) {
@@ -257,6 +262,9 @@ auto FormController::prepareFinalData() const -> FinalData {
 			}
 		}
 	}
+	if (hasErrors) {
+		return {};
+	}
 
 	auto json = QJsonObject();
 	json.insert("secure_data", secureData);
@@ -274,6 +282,9 @@ void FormController::submit() {
 	}
 
 	const auto prepared = prepareFinalData();
+	if (prepared.hashes.empty()) {
+		return;
+	}
 	const auto credentialsEncryptedData = EncryptData(
 		bytes::make_span(prepared.credentials));
 	const auto credentialsEncryptedSecret = EncryptCredentialsSecret(
@@ -433,6 +444,10 @@ QString FormController::passwordHint() const {
 void FormController::uploadScan(
 		not_null<const Value*> value,
 		QByteArray &&content) {
+	if (!canAddScan(value)) {
+		_view->showToast(lang(lng_passport_scans_limit_reached));
+		return;
+	}
 	const auto nonconst = findValue(value);
 	auto scanIndex = int(nonconst->scansInEdit.size());
 	nonconst->scansInEdit.emplace_back(
@@ -538,6 +553,12 @@ void FormController::scanDeleteRestore(
 
 	const auto nonconst = findValue(value);
 	auto &scan = nonconst->scansInEdit[scanIndex];
+	if (scan.deleted && !deleted) {
+		if (!canAddScan(value)) {
+			_view->showToast(lang(lng_passport_scans_limit_reached));
+			return;
+		}
+	}
 	scan.deleted = deleted;
 	_scanUpdated.fire(&scan);
 }
@@ -553,6 +574,13 @@ void FormController::selfieDeleteRestore(
 	_scanUpdated.fire(&scan);
 }
 
+bool FormController::canAddScan(not_null<const Value*> value) const {
+	const auto scansCount = ranges::count_if(
+		value->scansInEdit,
+		[](const EditFile &scan) { return !scan.deleted; });
+	return (scansCount < kDocumentScansLimit);
+}
+
 void FormController::subscribeToUploader() {
 	if (_uploaderSubscriptions) {
 		return;
diff --git a/Telegram/SourceFiles/passport/passport_form_controller.h b/Telegram/SourceFiles/passport/passport_form_controller.h
index cbe79dd987..9f77d6cd66 100644
--- a/Telegram/SourceFiles/passport/passport_form_controller.h
+++ b/Telegram/SourceFiles/passport/passport_form_controller.h
@@ -212,6 +212,7 @@ public:
 	rpl::producer<QString> passwordError() const;
 	QString passwordHint() const;
 
+	bool canAddScan(not_null<const Value*> value) const;
 	void uploadScan(not_null<const Value*> value, QByteArray &&content);
 	void deleteScan(not_null<const Value*> value, int fileIndex);
 	void restoreScan(not_null<const Value*> value, int fileIndex);
diff --git a/Telegram/SourceFiles/passport/passport_form_view_controller.h b/Telegram/SourceFiles/passport/passport_form_view_controller.h
index e5f8a18fcc..ac3e9398d8 100644
--- a/Telegram/SourceFiles/passport/passport_form_view_controller.h
+++ b/Telegram/SourceFiles/passport/passport_form_view_controller.h
@@ -45,6 +45,7 @@ public:
 	virtual void editScope(int index) = 0;
 
 	virtual void showBox(object_ptr<BoxContent> box) = 0;
+	virtual void showToast(const QString &text) = 0;
 
 	virtual ~ViewController() {
 	}
diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.cpp b/Telegram/SourceFiles/passport/passport_panel_controller.cpp
index f879a53533..f6def4ced0 100644
--- a/Telegram/SourceFiles/passport/passport_panel_controller.cpp
+++ b/Telegram/SourceFiles/passport/passport_panel_controller.cpp
@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "passport/passport_panel_edit_scans.h"
 #include "passport/passport_panel.h"
 #include "boxes/confirm_box.h"
+#include "ui/toast/toast.h"
 #include "ui/countryinput.h"
 #include "layout.h"
 
@@ -363,6 +364,14 @@ QString PanelController::defaultPhoneNumber() const {
 	return _form->defaultPhoneNumber();
 }
 
+bool PanelController::canAddScan() const {
+	Expects(_editScope != nullptr);
+	Expects(_editDocumentIndex >= 0
+		&& _editDocumentIndex < _editScope->documents.size());
+
+	return _form->canAddScan(_editScope->documents[_editDocumentIndex]);
+}
+
 void PanelController::uploadScan(QByteArray &&content) {
 	Expects(_editScope != nullptr);
 	Expects(_editDocumentIndex >= 0
@@ -858,6 +867,14 @@ void PanelController::showBox(object_ptr<BoxContent> box) {
 	_panel->showBox(std::move(box));
 }
 
+void PanelController::showToast(const QString &text) {
+	Expects(_panel != nullptr);
+
+	auto toast = Ui::Toast::Config();
+	toast.text = text;
+	Ui::Toast::Show(_panel.get(), toast);
+}
+
 rpl::lifetime &PanelController::lifetime() {
 	return _lifetime;
 }
diff --git a/Telegram/SourceFiles/passport/passport_panel_controller.h b/Telegram/SourceFiles/passport/passport_panel_controller.h
index b03c642842..bdbd94b3b1 100644
--- a/Telegram/SourceFiles/passport/passport_panel_controller.h
+++ b/Telegram/SourceFiles/passport/passport_panel_controller.h
@@ -60,6 +60,7 @@ public:
 	rpl::producer<QString> passwordError() const;
 	QString passwordHint() const;
 
+	bool canAddScan() const;
 	void uploadScan(QByteArray &&content);
 	void deleteScan(int fileIndex);
 	void restoreScan(int fileIndex);
@@ -89,6 +90,7 @@ public:
 	void cancelEditScope();
 
 	void showBox(object_ptr<BoxContent> box) override;
+	void showToast(const QString &text) override;
 
 	void cancelAuth();
 
diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp
index 47e888552d..990d3c0945 100644
--- a/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp
+++ b/Telegram/SourceFiles/passport/passport_panel_edit_document.cpp
@@ -300,6 +300,13 @@ PanelEditDocument::Result PanelEditDocument::collect() const {
 }
 
 bool PanelEditDocument::validate() {
+	if (const auto error = _editScans->validateGetErrorTop()) {
+		const auto errortop = _editScans->mapToGlobal(QPoint(0, *error));
+		const auto scrolltop = _scroll->mapToGlobal(QPoint(0, 0));
+		const auto scrolldelta = errortop.y() - scrolltop.y();
+		_scroll->scrollToY(_scroll->scrollTop() + scrolldelta);
+		return false;
+	}
 	auto first = QPointer<PanelDetailsRow>();
 	for (const auto [i, field] : base::reversed(_details)) {
 		const auto &row = _scheme.rows[i];
diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp
index c046d77ff3..679b6ef013 100644
--- a/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp
+++ b/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp
@@ -196,6 +196,22 @@ EditScans::EditScans(
 	setupContent(header);
 }
 
+base::optional<int> EditScans::validateGetErrorTop() {
+	const auto exists = ranges::find(
+		_files,
+		false,
+		[](const ScanInfo &file) { return file.deleted; }) != end(_files);
+	if (!exists) {
+		toggleError(true);
+		return (_files.size() > 5) ? _upload->y() : _header->y();
+	}
+	if (_selfie && (!_selfie->key.id || _selfie->deleted)) {
+		toggleSelfieError(true);
+		return _selfieHeader->y();
+	}
+	return base::none;
+}
+
 void EditScans::setupContent(const QString &header) {
 	const auto inner = _content.data();
 	inner->move(0, 0);
@@ -307,6 +323,9 @@ void EditScans::updateScan(ScanInfo &&info) {
 		Assert(_selfie != nullptr);
 		if (_selfie->key.id) {
 			updateRow(_selfieRow->entity(), info);
+			if (!info.deleted) {
+				hideSelfieError();
+			}
 		} else {
 			createSelfieRow(info);
 			_selfieWrap->resizeToWidth(width());
@@ -325,6 +344,9 @@ void EditScans::updateScan(ScanInfo &&info) {
 		scan->setStatus(i->status);
 		scan->setImage(i->thumb);
 		scan->setDeleted(i->deleted);
+		if (!i->deleted) {
+			hideError();
+		}
 	} else {
 		_files.push_back(std::move(info));
 		pushScan(_files.back());
@@ -352,6 +374,8 @@ void EditScans::createSelfieRow(const ScanInfo &info) {
 	) | rpl::start_with_next([=] {
 		_controller->restoreSelfie();
 	}, row->lifetime());
+
+	hideSelfieError();
 }
 
 void EditScans::pushScan(const ScanInfo &info) {
@@ -373,6 +397,8 @@ void EditScans::pushScan(const ScanInfo &info) {
 	) | rpl::start_with_next([=] {
 		_controller->restoreScan(index);
 	}, scan->lifetime());
+
+	hideError();
 }
 
 base::unique_qptr<Ui::SlideWrap<ScanButton>> EditScans::createScan(
@@ -393,6 +419,10 @@ base::unique_qptr<Ui::SlideWrap<ScanButton>> EditScans::createScan(
 }
 
 void EditScans::chooseScan() {
+	if (!_controller->canAddScan()) {
+		_controller->showToast(lang(lng_passport_scans_limit_reached));
+		return;
+	}
 	ChooseScan(base::lambda_guarded(this, [=](QByteArray &&content) {
 		_controller->uploadScan(std::move(content));
 	}));
@@ -437,4 +467,59 @@ rpl::producer<QString> EditScans::uploadButtonText() const {
 		: lng_passport_upload_more) | Info::Profile::ToUpperValue();
 }
 
+void EditScans::hideError() {
+	toggleError(false);
+}
+
+void EditScans::toggleError(bool shown) {
+	if (_errorShown != shown) {
+		_errorShown = shown;
+		_errorAnimation.start(
+			[=] { errorAnimationCallback(); },
+			_errorShown ? 0. : 1.,
+			_errorShown ? 1. : 0.,
+			st::passportDetailsField.duration);
+	}
+}
+
+void EditScans::errorAnimationCallback() {
+	const auto error = _errorAnimation.current(_errorShown ? 1. : 0.);
+	if (error == 0.) {
+		_upload->setColorOverride(base::none);
+	} else {
+		_upload->setColorOverride(anim::color(
+			st::passportUploadButton.textFg,
+			st::boxTextFgError,
+			error));
+	}
+}
+
+void EditScans::hideSelfieError() {
+	toggleSelfieError(false);
+}
+
+void EditScans::toggleSelfieError(bool shown) {
+	if (_selfieErrorShown != shown) {
+		_selfieErrorShown = shown;
+		_selfieErrorAnimation.start(
+			[=] { selfieErrorAnimationCallback(); },
+			_selfieErrorShown ? 0. : 1.,
+			_selfieErrorShown ? 1. : 0.,
+			st::passportDetailsField.duration);
+	}
+}
+
+void EditScans::selfieErrorAnimationCallback() {
+	const auto error = _selfieErrorAnimation.current(
+		_selfieErrorShown ? 1. : 0.);
+	if (error == 0.) {
+		_selfieUpload->setColorOverride(base::none);
+	} else {
+		_selfieUpload->setColorOverride(anim::color(
+			st::passportUploadButton.textFg,
+			st::boxTextFgError,
+			error));
+	}
+}
+
 } // namespace Passport
diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_scans.h b/Telegram/SourceFiles/passport/passport_panel_edit_scans.h
index f11161b93d..e9da863ee2 100644
--- a/Telegram/SourceFiles/passport/passport_panel_edit_scans.h
+++ b/Telegram/SourceFiles/passport/passport_panel_edit_scans.h
@@ -39,6 +39,8 @@ public:
 		std::vector<ScanInfo> &&files,
 		std::unique_ptr<ScanInfo> &&selfie);
 
+	base::optional<int> validateGetErrorTop();
+
 	static void ChooseScan(base::lambda<void(QByteArray&&)> callback);
 
 private:
@@ -55,6 +57,14 @@ private:
 
 	rpl::producer<QString> uploadButtonText() const;
 
+	void toggleError(bool shown);
+	void hideError();
+	void errorAnimationCallback();
+
+	void toggleSelfieError(bool shown);
+	void hideSelfieError();
+	void selfieErrorAnimationCallback();
+
 	not_null<PanelController*> _controller;
 	std::vector<ScanInfo> _files;
 	std::unique_ptr<ScanInfo> _selfie;
@@ -66,11 +76,15 @@ private:
 	std::vector<base::unique_qptr<Ui::SlideWrap<ScanButton>>> _rows;
 	QPointer<Info::Profile::Button> _upload;
 	rpl::event_stream<rpl::producer<QString>> _uploadTexts;
+	bool _errorShown = false;
+	Animation _errorAnimation;
 
 	QPointer<Ui::SlideWrap<Ui::FlatLabel>> _selfieHeader;
 	QPointer<Ui::VerticalLayout> _selfieWrap;
 	base::unique_qptr<Ui::SlideWrap<ScanButton>> _selfieRow;
 	QPointer<Info::Profile::Button> _selfieUpload;
+	bool _selfieErrorShown = false;
+	Animation _selfieErrorAnimation;
 
 };