diff --git a/src/LayoutManager.qml b/src/LayoutManager.qml
index d971edb61ba14f98d7063366572413e247ba4c46..5273e1ec76e3681a789dcb39e00ff72bc4798a21 100644
--- a/src/LayoutManager.qml
+++ b/src/LayoutManager.qml
@@ -20,6 +20,8 @@ import QtQuick
 import QtQuick.Controls
 
 import net.jami.Adapters 1.1
+import net.jami.Enums 1.1
+import net.jami.Constants 1.1
 
 import "mainview/components"
 
@@ -49,6 +51,73 @@ QtObject {
             }
             visibility = priv.windowedVisibility
         }
+        appWindow.allowVisibleWindow = true
+    }
+
+    // Start in a hidden state.
+    function startMinimized(visibilitySetting) {
+        // Save the loaded setting for when the app is restored.
+        priv.windowedVisibility = visibilitySetting
+        appWindow.allowVisibleWindow = false
+        appWindow.hide();
+    }
+
+    // Close to a hidden state.
+    function closeToTray(visibilitySetting = undefined) {
+        // Save the current visibility.
+        priv.windowedVisibility = visibility
+        appWindow.hide();
+    }
+
+    // Save the window geometry and visibility settings.
+    function saveWindowSettings() {
+        var geometry = Qt.rect(appWindow.x, appWindow.y,
+                               appWindow.width, appWindow.height)
+        AppSettingsManager.setValue(Settings.WindowGeometry, geometry)
+
+        // If closed-to-tray or minimized, save the cached windowedVisibility
+        // value instead.
+        if (isHidden) {
+            AppSettingsManager.setValue(Settings.WindowState, priv.windowedVisibility)
+        } else {
+            AppSettingsManager.setValue(Settings.WindowState, visibility)
+        }
+    }
+
+    // Restore the window geometry and visibility settings.
+    function restoreWindowSettings() {
+        var geometry = AppSettingsManager.getValue(Settings.WindowGeometry)
+
+        // Position.
+        if (!isNaN(geometry.x) && !isNaN(geometry.y)) {
+            appWindow.x = geometry.x
+            appWindow.y = geometry.y
+        }
+
+        // Dimensions.
+        appWindow.width = geometry.width ?
+                    geometry.width :
+                    JamiTheme.mainViewPreferredWidth
+        appWindow.height = geometry.height ?
+                    geometry.height :
+                    JamiTheme.mainViewPreferredHeight
+        appWindow.minimumWidth = JamiTheme.mainViewMinWidth
+        appWindow.minimumHeight = JamiTheme.mainViewMinHeight
+
+        // State.
+        const visibilityStr = AppSettingsManager.getValue(Settings.WindowState)
+        var visibilitySetting = parseInt(visibilityStr)
+
+        // We should never restore a hidden state here. Default to normal
+        // windowed state in such a case. This shouldn't happen.
+        if (visibilitySetting === Window.Hidden) {
+            visibilitySetting = Window.Windowed
+        }
+        if (MainApplication.startMinimized) {
+            startMinimized(visibilitySetting)
+        } else {
+            visibility = visibilitySetting
+        }
     }
 
     // Adds an item to the fullscreen item stack. Automatically puts
diff --git a/src/MainApplicationWindow.qml b/src/MainApplicationWindow.qml
index 7808b21aab3a26a1f8039ff9a8546e52939d1468..fa337d5eeacc5a5a0a20751cba0ed9678a19ee52 100644
--- a/src/MainApplicationWindow.qml
+++ b/src/MainApplicationWindow.qml
@@ -53,6 +53,7 @@ ApplicationWindow {
     }
 
     property bool windowSettingsLoaded: false
+    property bool allowVisibleWindow: true
 
     function checkLoadedSource() {
         var sourceString = mainApplicationLoader.source.toString()
@@ -83,19 +84,18 @@ ApplicationWindow {
         if (force || !UtilsAdapter.getAppValue(Settings.MinimizeOnClose) ||
                 !UtilsAdapter.getAccountListSize()) {
             // Save the window geometry and state before quitting.
-            var geometry = Qt.rect(appWindow.x, appWindow.y,
-                                   appWindow.width, appWindow.height)
-            AppSettingsManager.setValue(Settings.WindowGeometry, geometry)
-            AppSettingsManager.setValue(Settings.WindowState, appWindow.visibility)
+            layoutManager.saveWindowSettings()
             Qt.quit()
         } else {
-            hide()
+            layoutManager.closeToTray()
         }
     }
 
     title: JamiStrings.appTitle
 
-    visible: mainApplicationLoader.status === Loader.Ready && windowSettingsLoaded
+    visible: mainApplicationLoader.status === Loader.Ready
+             && windowSettingsLoaded
+             && allowVisibleWindow
 
     // To facilitate reparenting of the callview during
     // fullscreen mode, we need QQuickItem based object.
@@ -134,38 +134,21 @@ ApplicationWindow {
         onSourceChanged: windowSettingsLoaded = false
 
         onLoaded: {
-            if (UtilsAdapter.getAppValue(Settings.StartMinimized)) {
-                showMinimized()
+            if (checkLoadedSource() === MainApplicationWindow.LoadedSource.WizardView) {
+                // Onboarding wizard window, these settings are fixed.
+                // - window screen should default to the primary
+                // - position should default to being centered based on the
+                //   following dimensions
+                // - the window will showNormal once windowSettingsLoaded is
+                //   set to true(then forcing visible to true)
+                appWindow.width = JamiTheme.wizardViewMinWidth
+                appWindow.height = JamiTheme.wizardViewMinHeight
+                appWindow.minimumWidth = JamiTheme.wizardViewMinWidth
+                appWindow.minimumHeight = JamiTheme.wizardViewMinHeight
             } else {
-                if (checkLoadedSource() === MainApplicationWindow.LoadedSource.WizardView) {
-                    appWindow.width = JamiTheme.wizardViewMinWidth
-                    appWindow.height = JamiTheme.wizardViewMinHeight
-                    appWindow.minimumWidth = JamiTheme.wizardViewMinWidth
-                    appWindow.minimumHeight = JamiTheme.wizardViewMinHeight
-                } else {
-                    // Main window, load settings if possible.
-                    var geometry = AppSettingsManager.getValue(Settings.WindowGeometry)
-
-                    // Position.
-                    if (!isNaN(geometry.x) && !isNaN(geometry.y)) {
-                        appWindow.x = geometry.x
-                        appWindow.y = geometry.y
-                    }
-
-                    // Dimensions.
-                    appWindow.width = geometry.width ?
-                                geometry.width :
-                                JamiTheme.mainViewPreferredWidth
-                    appWindow.height = geometry.height ?
-                                geometry.height :
-                                JamiTheme.mainViewPreferredHeight
-                    appWindow.minimumWidth = JamiTheme.mainViewMinWidth
-                    appWindow.minimumHeight = JamiTheme.mainViewMinHeight
-
-                    // State.
-                    const visibilityStr = AppSettingsManager.getValue(Settings.WindowState)
-                    appWindow.visibility = parseInt(visibilityStr)
-                }
+                // Main window, load any valid app settings, and allow the
+                // layoutManager to handle as much as possible.
+                layoutManager.restoreWindowSettings()
             }
 
             // This will trigger `visible = true`.
@@ -176,6 +159,9 @@ ApplicationWindow {
                 UpdateManager.checkForUpdates(true)
                 UpdateManager.setAutoUpdateCheck(true)
             }
+
+            // Handle a start URI if set as start option.
+            MainApplication.handleUriAction();
         }
     }
 
diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp
index 11cf7fdec78300bc528c47d8d4ab8e234191eb4a..931218aa7dba218fb9865a47150e6addba4f24cb 100644
--- a/src/instancemanager.cpp
+++ b/src/instancemanager.cpp
@@ -52,13 +52,20 @@ public:
     {}
     ~Impl() = default;
 
-    bool tryToRun()
+    bool tryToRun(const QByteArray& startUri)
     {
         if (isAnotherRunning()) {
-            // This is a secondary instance,
-            // connect to the primary instance to trigger a restore
-            // then fail.
+            // This is a secondary instance, connect to the primary
+            // instance to trigger a restore then die.
             if (connectToLocal()) {
+                // Okay we connected. Send the start uri if not empty.
+                if (startUri.size()) {
+                    qDebug() << "Sending start URI to secondary instance." << startUri;
+                    socket_->write(startUri);
+                    socket_->waitForBytesWritten();
+                }
+
+                // Now this instance can die.
                 return false;
             }
             // If not connected, this means that the server doesn't exist
@@ -99,7 +106,7 @@ public:
             return;
         }
 
-        socket_->write(reinterpret_cast<const char*>(terminateSeq_.data()), 4);
+        socket_->write(terminateSeq_);
         socket_->waitForBytesWritten();
     };
 
@@ -139,10 +146,15 @@ private Q_SLOTS:
             if (recievedData == terminateSeq_) {
                 qWarning() << "Received terminate signal.";
                 mainAppInstance_->quit();
+            } else {
+                qDebug() << "Received start URI:" << recievedData;
+                auto startUri = QString::fromLatin1(recievedData);
+                mainAppInstance_->handleUriAction(startUri);
             }
         });
 
         // Restore primary instance
+        qDebug() << "Received wake-up from secondary instance.";
         mainAppInstance_->restoreApp();
     };
 
@@ -193,9 +205,9 @@ InstanceManager::~InstanceManager()
 }
 
 bool
-InstanceManager::tryToRun()
+InstanceManager::tryToRun(const QByteArray& startUri)
 {
-    return pimpl_->tryToRun();
+    return pimpl_->tryToRun(startUri);
 }
 
 void
diff --git a/src/instancemanager.h b/src/instancemanager.h
index 6da3056470a4706f4995511f90e910b79477734d..afd8fb7b00f614636f3a2dd24681de89cba4dcee 100644
--- a/src/instancemanager.h
+++ b/src/instancemanager.h
@@ -32,7 +32,7 @@ public:
     explicit InstanceManager(MainApplication* mainApp);
     ~InstanceManager();
 
-    bool tryToRun();
+    bool tryToRun(const QByteArray& startUri);
     void tryToKill();
 
 private:
diff --git a/src/main.cpp b/src/main.cpp
index 52f6c9c9eff6770ac378bba436dcdbd8c0fad967..d401027e69c78021d8570925d064aa2d59d6c9b3 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -106,9 +106,12 @@ main(int argc, char* argv[])
         qWarning() << "Attempting to terminate other instances.";
         im.tryToKill();
         return 0;
-    } else if (!im.tryToRun()) {
-        qWarning() << "Another instance is running.";
-        return 0;
+    } else {
+        auto startUri = app.getOpt(MainApplication::Option::StartUri);
+        if (!im.tryToRun(startUri.toByteArray())) {
+            qWarning() << "Another instance is running.";
+            return 0;
+        }
     }
 
     if (!app.init()) {
diff --git a/src/mainapplication.cpp b/src/mainapplication.cpp
index 8af2c3bbe2809c0bc56a89a739f07e2d1366ca74..02616e988ff914a74df257d8651e59f752577742 100644
--- a/src/mainapplication.cpp
+++ b/src/mainapplication.cpp
@@ -241,6 +241,10 @@ MainApplication::init()
     lrcInstance_->accountModel().autoTransferFromTrusted = allowTransferFromTrusted;
     lrcInstance_->accountModel().autoTransferSizeThreshold = acceptTransferBelow;
 
+    auto startMinimizedSetting = settingsManager_->getValue(Settings::Key::StartMinimized).toBool();
+    // The presence of start URI should override the startMinimized setting for this instance.
+    set_startMinimized(startMinimizedSetting && runOptions_[Option::StartUri].isNull());
+
     initQmlLayer();
 
     settingsManager_->setValue(Settings::Key::StartMinimized,
@@ -257,6 +261,20 @@ MainApplication::restoreApp()
     Q_EMIT lrcInstance_->restoreAppRequested();
 }
 
+void
+MainApplication::handleUriAction(const QString& arg)
+{
+    QString uri {};
+    if (arg.isEmpty() && !runOptions_[Option::StartUri].isNull()) {
+        uri = runOptions_[Option::StartUri].toString();
+        qDebug() << "URI action invoked by run option" << uri;
+    } else {
+        uri = arg;
+        qDebug() << "URI action invoked by secondary instance" << uri;
+    }
+    // TODO: implement URI protocol handling.
+}
+
 void
 MainApplication::initLrc(const QString& downloadUrl, ConnectivityMonitor* cm, bool logDaemon)
 {
@@ -290,6 +308,13 @@ MainApplication::initLrc(const QString& downloadUrl, ConnectivityMonitor* cm, bo
 void
 MainApplication::parseArguments()
 {
+    // See if the app is being started with a URI.
+    for (const auto& arg : QApplication::arguments()) {
+        if (arg.startsWith("jami:")) {
+            runOptions_[Option::StartUri] = arg;
+        }
+    }
+
     QCommandLineParser parser;
     parser.addHelpOption();
     parser.addVersionOption();
diff --git a/src/mainapplication.h b/src/mainapplication.h
index f2b06d9747e88f97011fc7fa82abdaebdb792b79..06597d3b3af26494ddbda51cbe4d51b63c8fe8db 100644
--- a/src/mainapplication.h
+++ b/src/mainapplication.h
@@ -58,7 +58,7 @@ class MainApplication : public QApplication
 {
     Q_OBJECT
     Q_DISABLE_COPY(MainApplication)
-
+    QML_RO_PROPERTY(bool, startMinimized)
 public:
     explicit MainApplication(int& argc, char** argv);
     ~MainApplication();
@@ -66,6 +66,8 @@ public:
     bool init();
     void restoreApp();
 
+    Q_INVOKABLE void handleUriAction(const QString& uri = {});
+
     enum class Option {
         StartMinimized = 0,
         Debug,
@@ -73,7 +75,8 @@ public:
         DebugToFile,
         UpdateUrl,
         MuteJamid,
-        TerminationRequested
+        TerminationRequested,
+        StartUri
     };
     QVariant getOpt(const Option opt)
     {