diff --git a/TweetDuck.sln b/TweetDuck.sln
index a3aef2f2..65927bae 100644
--- a/TweetDuck.sln
+++ b/TweetDuck.sln
@@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetImpl.CefSharp", "windo
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.WinForms.Legacy", "windows\TweetLib.WinForms.Legacy\TweetLib.WinForms.Legacy.csproj", "{B54E732A-4090-4DAA-9ABD-311368C17B68}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Api", "lib\TweetLib.Api\TweetLib.Api.csproj", "{85596C10-F76E-4619-9CC6-6C1593880F83}"
+EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Communication", "lib\TweetLib.Communication\TweetLib.Communication.csproj", "{72473763-4B9D-4FB6-A923-9364B2680F06}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TweetLib.Core", "lib\TweetLib.Core\TweetLib.Core.csproj", "{93BA3CB4-A812-4949-B07D-8D393FB38937}"
@@ -54,6 +56,10 @@ Global
 		{B54E732A-4090-4DAA-9ABD-311368C17B68}.Debug|x86.Build.0 = Debug|x86
 		{B54E732A-4090-4DAA-9ABD-311368C17B68}.Release|x86.ActiveCfg = Release|x86
 		{B54E732A-4090-4DAA-9ABD-311368C17B68}.Release|x86.Build.0 = Release|x86
+		{85596C10-F76E-4619-9CC6-6C1593880F83}.Debug|x86.ActiveCfg = Debug|x86
+		{85596C10-F76E-4619-9CC6-6C1593880F83}.Debug|x86.Build.0 = Debug|x86
+		{85596C10-F76E-4619-9CC6-6C1593880F83}.Release|x86.ActiveCfg = Release|x86
+		{85596C10-F76E-4619-9CC6-6C1593880F83}.Release|x86.Build.0 = Release|x86
 		{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.ActiveCfg = Debug|x86
 		{72473763-4B9D-4FB6-A923-9364B2680F06}.Debug|x86.Build.0 = Debug|x86
 		{72473763-4B9D-4FB6-A923-9364B2680F06}.Release|x86.ActiveCfg = Release|x86
diff --git a/lib/TweetLib.Api/Data/NamespacedResource.cs b/lib/TweetLib.Api/Data/NamespacedResource.cs
new file mode 100644
index 00000000..91189e6e
--- /dev/null
+++ b/lib/TweetLib.Api/Data/NamespacedResource.cs
@@ -0,0 +1,37 @@
+namespace TweetLib.Api.Data {
+	public readonly struct NamespacedResource {
+		public Resource Namespace { get; }
+		public Resource Path { get; }
+
+		public NamespacedResource(Resource ns, Resource path) {
+			Namespace = ns;
+			Path = path;
+		}
+
+		private bool Equals(NamespacedResource other) {
+			return Namespace.Equals(other.Namespace) && Path.Equals(other.Path);
+		}
+
+		public override bool Equals(object? obj) {
+			return obj is NamespacedResource other && Equals(other);
+		}
+
+		public static bool operator ==(NamespacedResource left, NamespacedResource right) {
+			return left.Equals(right);
+		}
+
+		public static bool operator !=(NamespacedResource left, NamespacedResource right) {
+			return !left.Equals(right);
+		}
+
+		public override int GetHashCode() {
+			unchecked {
+				return (Namespace.GetHashCode() * 397) ^ Path.GetHashCode();
+			}
+		}
+
+		public override string ToString() {
+			return $"{Namespace}:{Path}";
+		}
+	}
+}
diff --git a/lib/TweetLib.Api/Data/Resource.cs b/lib/TweetLib.Api/Data/Resource.cs
new file mode 100644
index 00000000..e106da66
--- /dev/null
+++ b/lib/TweetLib.Api/Data/Resource.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace TweetLib.Api.Data {
+	public readonly struct Resource {
+		private const string ValidCharacterPattern = "^[a-z0-9_]+$";
+		private static readonly Regex ValidCharacterRegex = new Regex(ValidCharacterPattern, RegexOptions.Compiled);
+
+		public string Name { get; }
+
+		public Resource(string name) {
+			if (!ValidCharacterRegex.IsMatch(name)) {
+				throw new ArgumentException("Resource name must match the regex: " + ValidCharacterPattern);
+			}
+
+			Name = name;
+		}
+
+		private bool Equals(Resource other) {
+			return Name == other.Name;
+		}
+
+		public override bool Equals(object? obj) {
+			return obj is Resource other && Equals(other);
+		}
+
+		public static bool operator ==(Resource left, Resource right) {
+			return left.Equals(right);
+		}
+
+		public static bool operator !=(Resource left, Resource right) {
+			return !left.Equals(right);
+		}
+
+		public override int GetHashCode() {
+			return Name.GetHashCode();
+		}
+
+		public override string ToString() {
+			return Name;
+		}
+	}
+}
diff --git a/lib/TweetLib.Api/ITweetDuckApi.cs b/lib/TweetLib.Api/ITweetDuckApi.cs
new file mode 100644
index 00000000..7a3d84cb
--- /dev/null
+++ b/lib/TweetLib.Api/ITweetDuckApi.cs
@@ -0,0 +1,5 @@
+namespace TweetLib.Api {
+	public interface ITweetDuckApi {
+		T? FindService<T>() where T : class, ITweetDuckService;
+	}
+}
diff --git a/lib/TweetLib.Api/ITweetDuckService.cs b/lib/TweetLib.Api/ITweetDuckService.cs
new file mode 100644
index 00000000..94d21d47
--- /dev/null
+++ b/lib/TweetLib.Api/ITweetDuckService.cs
@@ -0,0 +1,3 @@
+namespace TweetLib.Api {
+	public interface ITweetDuckService {}
+}
diff --git a/lib/TweetLib.Api/TweetDuckExtension.cs b/lib/TweetLib.Api/TweetDuckExtension.cs
new file mode 100644
index 00000000..20882ca3
--- /dev/null
+++ b/lib/TweetLib.Api/TweetDuckExtension.cs
@@ -0,0 +1,15 @@
+using TweetLib.Api.Data;
+
+namespace TweetLib.Api {
+	public abstract class TweetDuckExtension {
+		/// <summary>
+		/// Unique identifier of the extension.
+		/// </summary>
+		public abstract Resource Id { get; }
+
+		/// <summary>
+		/// Called when the extension is loaded on startup, or enabled at runtime.
+		/// </summary>
+		public abstract void Enable(ITweetDuckApi api);
+	}
+}
diff --git a/lib/TweetLib.Api/TweetLib.Api.csproj b/lib/TweetLib.Api/TweetLib.Api.csproj
new file mode 100644
index 00000000..078d5d76
--- /dev/null
+++ b/lib/TweetLib.Api/TweetLib.Api.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  
+  <PropertyGroup>
+    <TargetFramework>net6.0</TargetFramework>
+    <Configurations>Debug;Release</Configurations>
+    <Platforms>x86;x64</Platforms>
+    <LangVersion>10</LangVersion>
+    <Nullable>enable</Nullable>
+    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+  </PropertyGroup>
+  
+  <ItemGroup>
+    <Compile Include="..\..\Version.cs" Link="Version.cs" />
+  </ItemGroup>
+
+</Project>
diff --git a/lib/TweetLib.Core/App.cs b/lib/TweetLib.Core/App.cs
index 1e2c1114..99b0e7d3 100644
--- a/lib/TweetLib.Core/App.cs
+++ b/lib/TweetLib.Core/App.cs
@@ -5,6 +5,8 @@
 using TweetLib.Core.Application;
 using TweetLib.Core.Features;
 using TweetLib.Core.Features.Plugins;
+using TweetLib.Core.Resources;
+using TweetLib.Core.Systems.Api;
 using TweetLib.Core.Systems.Configuration;
 using TweetLib.Core.Systems.Logging;
 using TweetLib.Utils.Static;
@@ -21,8 +23,11 @@ public static class App {
 		internal static readonly string PluginPath    = Path.Combine(ProgramPath, "plugins");
 		internal static readonly string GuidePath     = Path.Combine(ProgramPath, "guide");
 
-		public static readonly string StoragePath = IsPortable ? Path.Combine(ProgramPath, "portable", "storage") : GetDataFolder();
-		public static readonly string LogoPath    = Path.Combine(ResourcesPath, "images/logo.png");
+		public static readonly string ExtensionPath = Path.Combine(ProgramPath, "extensions");
+		public static readonly string StoragePath   = IsPortable ? Path.Combine(ProgramPath, "portable", "storage") : GetDataFolder();
+		public static readonly string LogoPath      = Path.Combine(ResourcesPath, "images/logo.png");
+
+		public static ApiImplementation Api { get; } = new ();
 
 		public static Logger Logger               { get; } = new (Path.Combine(StoragePath, "TD_Log.txt"), Setup.IsDebugLogging);
 		public static ConfigManager ConfigManager { get; } = Setup.CreateConfigManager(StoragePath);
diff --git a/lib/TweetLib.Core/Features/Extensions/ExtensionLoader.cs b/lib/TweetLib.Core/Features/Extensions/ExtensionLoader.cs
new file mode 100644
index 00000000..09bf3a6f
--- /dev/null
+++ b/lib/TweetLib.Core/Features/Extensions/ExtensionLoader.cs
@@ -0,0 +1,41 @@
+using System;
+using System.IO;
+using System.Reflection;
+using TweetLib.Api;
+using TweetLib.Core.Systems.Api;
+
+namespace TweetLib.Core.Features.Extensions {
+	public static class ExtensionLoader {
+		public static void LoadAllInFolder(string extensionFolder) {
+			if (!Directory.Exists(extensionFolder)) {
+				return;
+			}
+
+			try {
+				foreach (string file in Directory.EnumerateFiles(extensionFolder, "*.dll", SearchOption.TopDirectoryOnly)) {
+					try {
+						Assembly assembly = Assembly.LoadFile(file);
+						foreach (Type type in assembly.GetTypes()) {
+							if (typeof(TweetDuckExtension).IsAssignableFrom(type) && Activator.CreateInstance(type) is TweetDuckExtension extension) {
+								EnableExtension(extension);
+							}
+						}
+					} catch (Exception e) {
+						App.ErrorHandler.HandleException("Extension Error", "Could not load extension: " + Path.GetFileNameWithoutExtension(file), true, e);
+					}
+				}
+			} catch (DirectoryNotFoundException) {
+				// ignore
+			} catch (Exception e) {
+				App.ErrorHandler.HandleException("Extension Error", "Could not load extensions.", true, e);
+			}
+		}
+
+		private static void EnableExtension(TweetDuckExtension extension) {
+			ApiImplementation apiImplementation = App.Api;
+			apiImplementation.CurrentExtension = extension;
+			extension.Enable(apiImplementation);
+			apiImplementation.CurrentExtension = null;
+		}
+	}
+}
diff --git a/lib/TweetLib.Core/Systems/Api/ApiImplementation.cs b/lib/TweetLib.Core/Systems/Api/ApiImplementation.cs
new file mode 100644
index 00000000..e4ff5991
--- /dev/null
+++ b/lib/TweetLib.Core/Systems/Api/ApiImplementation.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using TweetLib.Api;
+
+namespace TweetLib.Core.Systems.Api {
+	public class ApiImplementation : ITweetDuckApi {
+		public TweetDuckExtension? CurrentExtension { get; internal set; }
+
+		private readonly Dictionary<Type, ITweetDuckService> services = new Dictionary<Type, ITweetDuckService>();
+
+		internal ApiImplementation() {}
+
+		public void RegisterService<T>(T service) where T : class, ITweetDuckService {
+			if (!typeof(T).IsInterface) {
+				throw new ArgumentException("Api service implementation must be registered with its interface type.");
+			}
+
+			services.Add(typeof(T), service);
+		}
+
+		public T? FindService<T>() where T : class, ITweetDuckService {
+			return services.TryGetValue(typeof(T), out ITweetDuckService? service) ? service as T : null;
+		}
+	}
+}
diff --git a/lib/TweetLib.Core/TweetLib.Core.csproj b/lib/TweetLib.Core/TweetLib.Core.csproj
index 084688a1..ff0cfa16 100644
--- a/lib/TweetLib.Core/TweetLib.Core.csproj
+++ b/lib/TweetLib.Core/TweetLib.Core.csproj
@@ -14,6 +14,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <ProjectReference Include="..\TweetLib.Api\TweetLib.Api.csproj" />
     <ProjectReference Include="..\TweetLib.Browser\TweetLib.Browser.csproj" />
     <ProjectReference Include="..\TweetLib.Utils\TweetLib.Utils.csproj" />
   </ItemGroup>
diff --git a/windows/TweetDuck/Application/ApiServices.cs b/windows/TweetDuck/Application/ApiServices.cs
new file mode 100644
index 00000000..dc4fd7cf
--- /dev/null
+++ b/windows/TweetDuck/Application/ApiServices.cs
@@ -0,0 +1,16 @@
+using System;
+using TweetLib.Api;
+using TweetLib.Api.Data;
+using TweetLib.Core;
+
+namespace TweetDuck.Application {
+	static class ApiServices {
+		public static void Register() {
+		}
+
+		internal static NamespacedResource Namespace(Resource path) {
+			TweetDuckExtension currentExtension = App.Api.CurrentExtension ?? throw new InvalidOperationException("Cannot use API services outside of designated method calls.");
+			return new NamespacedResource(currentExtension.Id, path);
+		}
+	}
+}
diff --git a/windows/TweetDuck/Program.cs b/windows/TweetDuck/Program.cs
index dfc02d37..a4daa0ee 100644
--- a/windows/TweetDuck/Program.cs
+++ b/windows/TweetDuck/Program.cs
@@ -16,6 +16,7 @@
 using TweetLib.Browser.Request;
 using TweetLib.Core;
 using TweetLib.Core.Application;
+using TweetLib.Core.Features.Extensions;
 using TweetLib.Core.Features.Plugins;
 using TweetLib.Core.Features.Plugins.Config;
 using TweetLib.Core.Features.TweetDeck;
@@ -137,6 +138,9 @@ public void Launch(ResourceCache resourceCache, PluginManager pluginManager) {
 				Cef.Initialize(settings, false, new BrowserProcessHandler());
 
 				Win.Application.ApplicationExit += static (_, _) => ExitCleanup();
+
+				ApiServices.Register();
+				ExtensionLoader.LoadAllInFolder(App.ExtensionPath);
 				var updateCheckClient = new UpdateCheckClient(Path.Combine(storagePath, InstallerFolder));
 				var mainForm = new FormBrowser(resourceCache, pluginManager, updateCheckClient, lockManager.WindowRestoreMessage);
 				Win.Application.Run(mainForm);
diff --git a/windows/TweetDuck/TweetDuck.csproj b/windows/TweetDuck/TweetDuck.csproj
index b75b0cb7..0aa504c8 100644
--- a/windows/TweetDuck/TweetDuck.csproj
+++ b/windows/TweetDuck/TweetDuck.csproj
@@ -40,6 +40,7 @@
   </ItemGroup>
   
   <ItemGroup>
+    <ProjectReference Include="..\..\lib\TweetLib.Api\TweetLib.Api.csproj" />
     <ProjectReference Include="..\..\lib\TweetLib.Browser.CEF\TweetLib.Browser.CEF.csproj" />
     <ProjectReference Include="..\..\lib\TweetLib.Browser\TweetLib.Browser.csproj" />
     <ProjectReference Include="..\..\lib\TweetLib.Communication\TweetLib.Communication.csproj" />