diff --git a/lib/TweetLib.Api/Data/Notification/IDesktopNotificationScreenProvider.cs b/lib/TweetLib.Api/Data/Notification/IDesktopNotificationScreenProvider.cs
new file mode 100644
index 00000000..3c516d4f
--- /dev/null
+++ b/lib/TweetLib.Api/Data/Notification/IDesktopNotificationScreenProvider.cs
@@ -0,0 +1,28 @@
+namespace TweetLib.Api.Data.Notification {
+	/// <summary>
+	/// Allows extensions to decide which screen to use for notifications.
+	///
+	/// Every registered provider becomes available in the Options dialog and has to be explicitly selected by the user. Only one provider
+	/// can be active at any given time.
+	/// </summary>
+	public interface IDesktopNotificationScreenProvider {
+		/// <summary>
+		/// A unique identifier of this provider. Only needs to be unique within the scope of this plugin.
+		/// </summary>
+		Resource Id { get; }
+
+		/// <summary>
+		/// Text displayed in the user interface.
+		/// </summary>
+		string DisplayName { get; }
+
+		/// <summary>
+		/// Returns a screen that will be used to display the next desktop notification.
+		///
+		/// If the return value is <c>null</c> or a screen that is not present in <see cref="IScreenLayout.AllScreens" />, desktop
+		/// notifications will be temporarily paused and this method will be called again after an unspecified amount of time (but
+		/// not sooner than 1 second since the last call).
+		/// </summary>
+		IScreen? PickScreen(IScreenLayout layout);
+	}
+}
diff --git a/lib/TweetLib.Api/Data/Notification/IScreen.cs b/lib/TweetLib.Api/Data/Notification/IScreen.cs
new file mode 100644
index 00000000..d58d738c
--- /dev/null
+++ b/lib/TweetLib.Api/Data/Notification/IScreen.cs
@@ -0,0 +1,7 @@
+namespace TweetLib.Api.Data.Notification {
+	public interface IScreen {
+		ScreenBounds Bounds { get; }
+		string Name { get; }
+		bool IsPrimary { get; }
+	}
+}
diff --git a/lib/TweetLib.Api/Data/Notification/IScreenLayout.cs b/lib/TweetLib.Api/Data/Notification/IScreenLayout.cs
new file mode 100644
index 00000000..b81cfbb3
--- /dev/null
+++ b/lib/TweetLib.Api/Data/Notification/IScreenLayout.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace TweetLib.Api.Data.Notification {
+	public interface IScreenLayout {
+		IScreen PrimaryScreen { get; }
+		IScreen TweetDuckScreen { get; }
+		List<IScreen> AllScreens { get; }
+	}
+}
diff --git a/lib/TweetLib.Api/Data/Notification/ScreenBounds.cs b/lib/TweetLib.Api/Data/Notification/ScreenBounds.cs
new file mode 100644
index 00000000..533b0a25
--- /dev/null
+++ b/lib/TweetLib.Api/Data/Notification/ScreenBounds.cs
@@ -0,0 +1,18 @@
+namespace TweetLib.Api.Data.Notification {
+	public readonly struct ScreenBounds {
+		public int X1 { get; }
+		public int Y1 { get; }
+		public int Width { get; }
+		public int Height { get; }
+
+		public int X2 => X1 + Width;
+		public int Y2 => Y1 + Height;
+
+		public ScreenBounds(int x1, int y1, int width, int height) {
+			X1 = x1;
+			Y1 = y1;
+			Width = width;
+			Height = height;
+		}
+	}
+}
diff --git a/lib/TweetLib.Api/Service/INotificationService.cs b/lib/TweetLib.Api/Service/INotificationService.cs
new file mode 100644
index 00000000..90916916
--- /dev/null
+++ b/lib/TweetLib.Api/Service/INotificationService.cs
@@ -0,0 +1,7 @@
+using TweetLib.Api.Data.Notification;
+
+namespace TweetLib.Api.Service {
+	public interface INotificationService : ITweetDuckService {
+		void RegisterDesktopNotificationScreenProvider(IDesktopNotificationScreenProvider provider);
+	}
+}
diff --git a/lib/TweetLib.Core/Systems/Configuration/ConfigManager.cs b/lib/TweetLib.Core/Systems/Configuration/ConfigManager.cs
index 9dc885db..2044364b 100644
--- a/lib/TweetLib.Core/Systems/Configuration/ConfigManager.cs
+++ b/lib/TweetLib.Core/Systems/Configuration/ConfigManager.cs
@@ -10,7 +10,7 @@
 
 namespace TweetLib.Core.Systems.Configuration {
 	public abstract class ConfigManager {
-		protected static TypeConverterRegistry ConverterRegistry { get; } = new ();
+		public static TypeConverterRegistry ConverterRegistry { get; } = new ();
 
 		static ConfigManager() {
 			ConverterRegistry.Register(typeof(WindowState), new BasicTypeConverter<WindowState> {
diff --git a/windows/TweetDuck/Application/ApiServices.cs b/windows/TweetDuck/Application/ApiServices.cs
index dc4fd7cf..44496f63 100644
--- a/windows/TweetDuck/Application/ApiServices.cs
+++ b/windows/TweetDuck/Application/ApiServices.cs
@@ -1,11 +1,16 @@
 using System;
+using TweetDuck.Application.Service;
 using TweetLib.Api;
 using TweetLib.Api.Data;
+using TweetLib.Api.Service;
 using TweetLib.Core;
 
 namespace TweetDuck.Application {
 	static class ApiServices {
+		public static NotificationService Notifications { get; } = new NotificationService();
+
 		public static void Register() {
+			App.Api.RegisterService<INotificationService>(Notifications);
 		}
 
 		internal static NamespacedResource Namespace(Resource path) {
diff --git a/windows/TweetDuck/Application/Service/NotificationService.cs b/windows/TweetDuck/Application/Service/NotificationService.cs
new file mode 100644
index 00000000..4c9a864a
--- /dev/null
+++ b/windows/TweetDuck/Application/Service/NotificationService.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using TweetLib.Api.Data;
+using TweetLib.Api.Data.Notification;
+using TweetLib.Api.Service;
+
+namespace TweetDuck.Application.Service {
+	sealed class NotificationService : INotificationService {
+		private readonly List<NamespacedProvider> desktopNotificationScreenProviders = new ();
+
+		public void RegisterDesktopNotificationScreenProvider(IDesktopNotificationScreenProvider provider) {
+			desktopNotificationScreenProviders.Add(new NamespacedProvider(ApiServices.Namespace(provider.Id), provider));
+		}
+
+		public List<NamespacedProvider> GetDesktopNotificationScreenProviders() {
+			return desktopNotificationScreenProviders;
+		}
+
+		public sealed class NamespacedProvider : IDesktopNotificationScreenProvider {
+			public NamespacedResource NamespacedId { get; }
+
+			private readonly IDesktopNotificationScreenProvider provider;
+
+			public NamespacedProvider(NamespacedResource id, IDesktopNotificationScreenProvider provider) {
+				this.NamespacedId = id;
+				this.provider = provider;
+			}
+
+			public Resource Id => provider.Id;
+			public string DisplayName => provider.DisplayName;
+			public IScreen? PickScreen(IScreenLayout layout) => provider.PickScreen(layout);
+		}
+	}
+}
diff --git a/windows/TweetDuck/Browser/Notification/FormNotificationBase.cs b/windows/TweetDuck/Browser/Notification/FormNotificationBase.cs
index 59c04d63..df4e33a1 100644
--- a/windows/TweetDuck/Browser/Notification/FormNotificationBase.cs
+++ b/windows/TweetDuck/Browser/Notification/FormNotificationBase.cs
@@ -22,15 +22,7 @@ abstract partial class FormNotificationBase : Form {
 
 		protected virtual Point PrimaryLocation {
 			get {
-				Screen screen;
-
-				if (Config.NotificationDisplay > 0 && Config.NotificationDisplay <= Screen.AllScreens.Length) {
-					screen = Screen.AllScreens[Config.NotificationDisplay - 1];
-				}
-				else {
-					screen = Screen.FromControl(owner);
-				}
-
+				Screen screen = Config.NotificationDisplay.PickScreen(owner);
 				int edgeDist = Config.NotificationEdgeDistance;
 
 				switch (Config.NotificationPosition) {
diff --git a/windows/TweetDuck/Browser/Notification/NotificationScreen.cs b/windows/TweetDuck/Browser/Notification/NotificationScreen.cs
new file mode 100644
index 00000000..28804d7a
--- /dev/null
+++ b/windows/TweetDuck/Browser/Notification/NotificationScreen.cs
@@ -0,0 +1,173 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows.Forms;
+using TweetDuck.Application;
+using TweetDuck.Application.Service;
+using TweetLib.Api.Data;
+using TweetLib.Api.Data.Notification;
+using TweetLib.Utils.Serialization.Converters;
+using TweetLib.Utils.Static;
+
+namespace TweetDuck.Browser.Notification {
+	abstract class NotificationScreen {
+		public static List<NotificationScreen> All {
+			get {
+				var list = new List<NotificationScreen> {
+					SameAsTweetDuck.Instance
+				};
+
+				for (int index = 1; index <= Screen.AllScreens.Length; index++) {
+					list.Add(new Static(index));
+				}
+
+				foreach (var provider in ApiServices.Notifications.GetDesktopNotificationScreenProviders()) {
+					list.Add(new Provided(provider.NamespacedId));
+				}
+
+				return list;
+			}
+		}
+
+		public abstract string DisplayName { get; }
+
+		private NotificationScreen() {}
+
+		public abstract Screen PickScreen(FormBrowser mainWindow);
+
+		protected abstract string Serialize();
+
+		public sealed class SameAsTweetDuck : NotificationScreen {
+			public static SameAsTweetDuck Instance { get; } = new SameAsTweetDuck();
+
+			public override string DisplayName => "(Same as TweetDuck)";
+
+			private SameAsTweetDuck() {}
+
+			public override Screen PickScreen(FormBrowser mainWindow) {
+				return Screen.FromControl(mainWindow);
+			}
+
+			protected override string Serialize() {
+				return "0";
+			}
+
+			public override bool Equals(object? obj) {
+				return obj is SameAsTweetDuck;
+			}
+
+			public override int GetHashCode() {
+				return 1828695039;
+			}
+		}
+
+		private sealed class Static : NotificationScreen {
+			public override string DisplayName {
+				get {
+					Screen? screen = Screen;
+					if (screen == null) {
+						return $"Unknown ({screenIndex})";
+					}
+
+					return screen.DeviceName.TrimStart('\\', '.') + $" ({screen.Bounds.Width}x{screen.Bounds.Height})";
+				}
+			}
+
+			private Screen? Screen => screenIndex >= 1 && screenIndex <= Screen.AllScreens.Length ? Screen.AllScreens[screenIndex - 1] : null;
+
+			private readonly int screenIndex;
+
+			public Static(int screenIndex) {
+				this.screenIndex = screenIndex;
+			}
+
+			public override Screen PickScreen(FormBrowser mainWindow) {
+				return Screen ?? SameAsTweetDuck.Instance.PickScreen(mainWindow);
+			}
+
+			protected override string Serialize() {
+				return screenIndex.ToString();
+			}
+
+			public override bool Equals(object? obj) {
+				return obj is Static other && screenIndex == other.screenIndex;
+			}
+
+			public override int GetHashCode() {
+				return 31 * screenIndex;
+			}
+		}
+
+		private sealed class Provided : NotificationScreen {
+			public override string DisplayName => Provider?.DisplayName ?? $"Unknown ({resource})";
+
+			private readonly NamespacedResource resource;
+
+			private NotificationService.NamespacedProvider? provider;
+			private NotificationService.NamespacedProvider? Provider => provider ??= ApiServices.Notifications.GetDesktopNotificationScreenProviders().Find(p => p.NamespacedId == resource);
+
+			public Provided(NamespacedResource resource) {
+				this.resource = resource;
+			}
+
+			public override Screen PickScreen(FormBrowser mainWindow) {
+				IScreen? pick = Provider?.PickScreen(new WindowsFormsScreenLayout(mainWindow));
+				return pick is WindowsFormsScreen screen ? screen.Screen : SameAsTweetDuck.Instance.PickScreen(mainWindow); // TODO
+			}
+
+			protected override string Serialize() {
+				return resource.Namespace + ":" + resource.Path;
+			}
+
+			public override bool Equals(object? obj) {
+				return obj is Provided other && resource == other.resource;
+			}
+
+			public override int GetHashCode() {
+				return resource.GetHashCode();
+			}
+
+			private sealed class WindowsFormsScreenLayout : IScreenLayout {
+				public IScreen PrimaryScreen => new WindowsFormsScreen(Screen.PrimaryScreen);
+				public IScreen TweetDuckScreen => new WindowsFormsScreen(Screen.FromControl(mainWindow));
+				public List<IScreen> AllScreens => Screen.AllScreens.Select(static screen => new WindowsFormsScreen(screen)).ToList<IScreen>();
+
+				private readonly FormBrowser mainWindow;
+
+				public WindowsFormsScreenLayout(FormBrowser mainWindow) {
+					this.mainWindow = mainWindow;
+				}
+			}
+
+			private sealed class WindowsFormsScreen : IScreen {
+				public Screen Screen { get; }
+				public ScreenBounds Bounds { get; }
+				public string Name => Screen.DeviceName;
+				public bool IsPrimary => Screen.Primary;
+
+				public WindowsFormsScreen(Screen screen) {
+					this.Screen = screen;
+					this.Bounds = new ScreenBounds(screen.Bounds.X, screen.Bounds.Y, screen.Bounds.Width, screen.Bounds.Height);
+				}
+			}
+		}
+
+		public static readonly BasicTypeConverter<NotificationScreen> Converter = new()  {
+			ConvertToString = static value => value.Serialize(),
+			ConvertToObject = static value => {
+				if (value == "0") {
+					return SameAsTweetDuck.Instance;
+				}
+				else if (int.TryParse(value, out int index)) {
+					return new Static(index);
+				}
+
+				var resource = StringUtils.SplitInTwo(value, ':');
+				if (resource != null) {
+					return new Provided(new NamespacedResource(new Resource(resource.Value.before), new Resource(resource.Value.after)));
+				}
+
+				return SameAsTweetDuck.Instance;
+			}
+		};
+	}
+}
diff --git a/windows/TweetDuck/Configuration/UserConfig.cs b/windows/TweetDuck/Configuration/UserConfig.cs
index b9007a5a..bb428cea 100644
--- a/windows/TweetDuck/Configuration/UserConfig.cs
+++ b/windows/TweetDuck/Configuration/UserConfig.cs
@@ -2,6 +2,7 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Drawing;
 using TweetDuck.Browser;
+using TweetDuck.Browser.Notification;
 using TweetDuck.Controls;
 using TweetLib.Core;
 using TweetLib.Core.Application;
@@ -66,7 +67,7 @@ sealed class UserConfig : BaseConfig<UserConfig>, IAppUserConfiguration {
 
 		public DesktopNotification.Position NotificationPosition { get; set; } = DesktopNotification.Position.TopRight;
 		public Point CustomNotificationPosition                  { get; set; } = ControlExtensions.InvisibleLocation;
-		public int NotificationDisplay                           { get; set; } = 0;
+		public NotificationScreen NotificationDisplay            { get; set; } = NotificationScreen.SameAsTweetDuck.Instance;
 		public int NotificationEdgeDistance                      { get; set; } = 8;
 		public int NotificationWindowOpacity                     { get; set; } = 100;
 
diff --git a/windows/TweetDuck/Dialogs/Settings/TabSettingsNotifications.cs b/windows/TweetDuck/Dialogs/Settings/TabSettingsNotifications.cs
index 273c8d85..d644ef6f 100644
--- a/windows/TweetDuck/Dialogs/Settings/TabSettingsNotifications.cs
+++ b/windows/TweetDuck/Dialogs/Settings/TabSettingsNotifications.cs
@@ -86,13 +86,21 @@ public TabSettingsNotifications(FormNotificationExample notification) {
 			}
 
 			comboBoxDisplay.Enabled = trackBarEdgeDistance.Enabled = !radioLocCustom.Checked;
-			comboBoxDisplay.Items.Add("(Same as TweetDuck)");
 
-			foreach (Screen screen in Screen.AllScreens) {
-				comboBoxDisplay.Items.Add($"{screen.DeviceName.TrimStart('\\', '.')} ({screen.Bounds.Width}x{screen.Bounds.Height})");
+			bool foundScreen = false;
+
+			foreach (var screen in NotificationScreen.All) {
+				comboBoxDisplay.Items.Add(new NotificationScreenItem(screen));
+				if (screen.Equals(Config.NotificationDisplay)) {
+					comboBoxDisplay.SelectedIndex = comboBoxDisplay.Items.Count - 1;
+					foundScreen = true;
+				}
 			}
 
-			comboBoxDisplay.SelectedIndex = Math.Min(comboBoxDisplay.Items.Count - 1, Config.NotificationDisplay);
+			if (!foundScreen) {
+				comboBoxDisplay.Items.Add(new NotificationScreenItem(Config.NotificationDisplay));
+				comboBoxDisplay.SelectedIndex = comboBoxDisplay.Items.Count - 1;
+			}
 
 			trackBarEdgeDistance.SetValueSafe(Config.NotificationEdgeDistance);
 			labelEdgeDistanceValue.Text = trackBarEdgeDistance.Value + " px";
@@ -279,7 +287,7 @@ private void radioLocCustom_Click(object? sender, EventArgs e) {
 		}
 
 		private void comboBoxDisplay_SelectedValueChanged(object? sender, EventArgs e) {
-			Config.NotificationDisplay = comboBoxDisplay.SelectedIndex;
+			Config.NotificationDisplay = ((NotificationScreenItem) comboBoxDisplay.SelectedItem).Screen;
 			notification.ShowExampleNotification(false);
 		}
 
@@ -289,6 +297,21 @@ private void trackBarEdgeDistance_ValueChanged(object? sender, EventArgs e) {
 			notification.ShowExampleNotification(false);
 		}
 
+		private class NotificationScreenItem {
+			public NotificationScreen Screen { get; }
+
+			private readonly string displayName;
+
+			public NotificationScreenItem(NotificationScreen screen) {
+				this.Screen = screen;
+				this.displayName = screen.DisplayName;
+			}
+
+			public override string ToString() {
+				return displayName;
+			}
+		}
+
 		#endregion
 
 		#region Size
diff --git a/windows/TweetDuck/Program.cs b/windows/TweetDuck/Program.cs
index a4daa0ee..a193cedf 100644
--- a/windows/TweetDuck/Program.cs
+++ b/windows/TweetDuck/Program.cs
@@ -6,6 +6,7 @@
 using TweetDuck.Application;
 using TweetDuck.Browser;
 using TweetDuck.Browser.Base;
+using TweetDuck.Browser.Notification;
 using TweetDuck.Configuration;
 using TweetDuck.Dialogs;
 using TweetDuck.Management;
@@ -91,6 +92,7 @@ private sealed class Setup : IAppSetup {
 			public string? ResourceRewriteRules => Arguments.GetValue(Arguments.ArgFreeze);
 
 			public ConfigManager CreateConfigManager(string storagePath) {
+				ConfigManager.ConverterRegistry.Register(typeof(NotificationScreen), NotificationScreen.Converter);
 				return new ConfigManager<UserConfig, SystemConfig>(storagePath, Config);
 			}