diff --git a/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/ConfigLoader.cs b/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/ConfigLoader.cs
new file mode 100644
index 00000000..3ab9f8af
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/ConfigLoader.cs
@@ -0,0 +1,38 @@
+using System.Text.Json;
+
+namespace ZB.MOM.WW.ScadaBridge.DelmiaNotifier;
+
+/// Reads and interprets the notifier's configuration and secret. Parse logic is string-based so it is unit-testable without touching disk.
+internal static class ConfigLoader
+{
+ private const string ApiKeyEnvVar = "SCADABRIDGE_API_KEY";
+
+ /// Deserialize the appsettings.json text via the source-gen context.
+ public static NotifierConfig Load(string jsonText) =>
+ JsonSerializer.Deserialize(jsonText, NotifierJsonContext.Default.NotifierConfig) ?? new NotifierConfig();
+
+ /// Read and parse the appsettings.json sitting next to the executable.
+ public static NotifierConfig LoadFromDefaultFile()
+ {
+ var path = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
+ return Load(File.ReadAllText(path));
+ }
+
+ /// Split a comma-separated base-URL list into trimmed, non-empty entries.
+ public static string[] SplitBaseUrls(string? baseUrls)
+ {
+ if (string.IsNullOrWhiteSpace(baseUrls))
+ {
+ return [];
+ }
+
+ return baseUrls.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ }
+
+ /// Resolve the API key from SCADABRIDGE_API_KEY; null/whitespace → null. Env accessor is injected for testability.
+ public static string? ResolveApiKey(Func envGet)
+ {
+ var key = envGet(ApiKeyEnvVar);
+ return string.IsNullOrWhiteSpace(key) ? null : key;
+ }
+}
diff --git a/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/NotifierConfig.cs b/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/NotifierConfig.cs
new file mode 100644
index 00000000..d2c32ad7
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/NotifierConfig.cs
@@ -0,0 +1,19 @@
+namespace ZB.MOM.WW.ScadaBridge.DelmiaNotifier;
+
+/// Root of appsettings.json; mirrors the ScadaBridge section only.
+internal sealed class NotifierConfig
+{
+ public ScadaBridgeSection ScadaBridge { get; set; } = new();
+}
+
+internal sealed class ScadaBridgeSection
+{
+ /// Comma-separated failover list of base URLs (the method path is appended by the app).
+ public string? BaseUrls { get; set; }
+
+ /// Per-attempt HTTP timeout in seconds.
+ public int TimeoutSeconds { get; set; } = 30;
+
+ /// Optional diagnostic log file (relative to the exe); null/omitted → stderr only.
+ public string? LogPath { get; set; }
+}
diff --git a/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/NotifierJsonContext.cs b/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/NotifierJsonContext.cs
index e2684855..91a9cc6f 100644
--- a/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/NotifierJsonContext.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/NotifierJsonContext.cs
@@ -4,4 +4,5 @@ namespace ZB.MOM.WW.ScadaBridge.DelmiaNotifier;
[JsonSerializable(typeof(RecipeDownload))]
[JsonSerializable(typeof(RecipeDownloadResult))]
+[JsonSerializable(typeof(NotifierConfig))]
internal partial class NotifierJsonContext : JsonSerializerContext;
diff --git a/tests/ZB.MOM.WW.ScadaBridge.DelmiaNotifier.Tests/ConfigLoaderTests.cs b/tests/ZB.MOM.WW.ScadaBridge.DelmiaNotifier.Tests/ConfigLoaderTests.cs
new file mode 100644
index 00000000..79fae0f8
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.DelmiaNotifier.Tests/ConfigLoaderTests.cs
@@ -0,0 +1,50 @@
+using ZB.MOM.WW.ScadaBridge.DelmiaNotifier;
+
+namespace ZB.MOM.WW.ScadaBridge.DelmiaNotifier.Tests;
+
+public class ConfigLoaderTests
+{
+ [Fact]
+ public void SplitBaseUrls_trims_and_drops_empties()
+ {
+ var urls = ConfigLoader.SplitBaseUrls("a, b ,,c");
+ Assert.Equal(new[] { "a", "b", "c" }, urls);
+ }
+
+ [Fact]
+ public void SplitBaseUrls_null_or_whitespace_returns_empty()
+ {
+ Assert.Empty(ConfigLoader.SplitBaseUrls(null));
+ Assert.Empty(ConfigLoader.SplitBaseUrls(" "));
+ }
+
+ [Fact]
+ public void Load_defaults_timeout_to_30_when_omitted()
+ {
+ var cfg = ConfigLoader.Load("{\"ScadaBridge\":{\"BaseUrls\":\"http://x\"}}");
+ Assert.Equal(30, cfg.ScadaBridge.TimeoutSeconds);
+ Assert.Equal("http://x", cfg.ScadaBridge.BaseUrls);
+ }
+
+ [Fact]
+ public void Load_reads_all_fields()
+ {
+ var cfg = ConfigLoader.Load("{\"ScadaBridge\":{\"BaseUrls\":\"a,b\",\"TimeoutSeconds\":5,\"LogPath\":\"l.log\"}}");
+ Assert.Equal("a,b", cfg.ScadaBridge.BaseUrls);
+ Assert.Equal(5, cfg.ScadaBridge.TimeoutSeconds);
+ Assert.Equal("l.log", cfg.ScadaBridge.LogPath);
+ }
+
+ [Fact]
+ public void ResolveApiKey_returns_null_when_unset_or_whitespace()
+ {
+ Assert.Null(ConfigLoader.ResolveApiKey(_ => null));
+ Assert.Null(ConfigLoader.ResolveApiKey(_ => " "));
+ }
+
+ [Fact]
+ public void ResolveApiKey_returns_value_when_present()
+ {
+ Assert.Equal("sbk_x", ConfigLoader.ResolveApiKey(n => n == "SCADABRIDGE_API_KEY" ? "sbk_x" : null));
+ }
+}