diff --git a/src/PowderCoating.Web/BackgroundServices/ColumbiaCatalogSyncBackgroundService.cs b/src/PowderCoating.Web/BackgroundServices/ColumbiaCatalogSyncBackgroundService.cs
new file mode 100644
index 0000000..0e8a9ae
--- /dev/null
+++ b/src/PowderCoating.Web/BackgroundServices/ColumbiaCatalogSyncBackgroundService.cs
@@ -0,0 +1,112 @@
+using System.Globalization;
+using PowderCoating.Application.Constants;
+using PowderCoating.Application.Interfaces;
+
+namespace PowderCoating.Web.BackgroundServices;
+
+///
+/// Runs the Columbia Coatings catalog sync on a schedule. Wakes hourly and triggers a full sync
+/// only when the master switch (ColumbiaSyncEnabled) is on and the configured interval
+/// (ColumbiaSyncIntervalDays) has elapsed since the last successful run. A full sync is
+/// cheap (~25 API calls), so an hourly due-check is negligible; the actual work runs at most once
+/// per interval. No-ops quietly when disabled or unconfigured.
+///
+public class ColumbiaCatalogSyncBackgroundService : BackgroundService
+{
+ private readonly IServiceScopeFactory _scopeFactory;
+ private readonly ILogger _logger;
+
+ private static readonly TimeSpan CheckInterval = TimeSpan.FromHours(1);
+ private static readonly TimeSpan StartupDelay = TimeSpan.FromMinutes(2);
+
+ ///
+ /// Uses because a is a
+ /// singleton and the sync service / platform settings are scoped.
+ ///
+ public ColumbiaCatalogSyncBackgroundService(
+ IServiceScopeFactory scopeFactory,
+ ILogger logger)
+ {
+ _scopeFactory = scopeFactory;
+ _logger = logger;
+ }
+
+ ///
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ _logger.LogInformation("ColumbiaCatalogSyncBackgroundService started.");
+
+ try
+ {
+ await Task.Delay(StartupDelay, stoppingToken);
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ await RunIfDueAsync(stoppingToken);
+ await Task.Delay(CheckInterval, stoppingToken);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Shutting down — expected.
+ }
+ }
+
+ ///
+ /// Checks the enable switch and the elapsed interval, and runs a sync when due. Failures from
+ /// the sync itself are reported on its result (and recorded in platform settings) rather than
+ /// thrown, so a bad run never tears down the loop.
+ ///
+ private async Task RunIfDueAsync(CancellationToken ct)
+ {
+ using var scope = _scopeFactory.CreateScope();
+ var settings = scope.ServiceProvider.GetRequiredService();
+
+ if (!await settings.GetBoolAsync(ColumbiaIntegrationConstants.SettingEnabled))
+ return; // master switch off
+
+ var intervalDays = Math.Max(1, await settings.GetIntAsync(
+ ColumbiaIntegrationConstants.SettingIntervalDays,
+ ColumbiaIntegrationConstants.DefaultSyncIntervalDays));
+
+ if (!IsDue(await settings.GetAsync(ColumbiaIntegrationConstants.SettingLastSyncedAt), intervalDays))
+ return; // synced recently enough
+
+ var sync = scope.ServiceProvider.GetRequiredService();
+
+ try
+ {
+ _logger.LogInformation("Columbia scheduled sync starting (interval {Days}d).", intervalDays);
+ var result = await sync.RunSyncAsync(ct);
+
+ if (result.Success)
+ _logger.LogInformation("Columbia scheduled sync complete: {Summary}", result.Summary);
+ else
+ _logger.LogWarning("Columbia scheduled sync did not succeed: {Error}", result.ErrorMessage);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Columbia scheduled sync threw unexpectedly.");
+ }
+ }
+
+ ///
+ /// A sync is due when there is no recorded last-sync timestamp, or the configured number of
+ /// days has elapsed since it. An unparseable timestamp is treated as "due".
+ ///
+ private static bool IsDue(string? lastSyncedRaw, int intervalDays)
+ {
+ if (string.IsNullOrWhiteSpace(lastSyncedRaw))
+ return true;
+
+ if (!DateTime.TryParse(lastSyncedRaw, CultureInfo.InvariantCulture,
+ DateTimeStyles.RoundtripKind, out var lastSynced))
+ return true;
+
+ return DateTime.UtcNow - lastSynced.ToUniversalTime() >= TimeSpan.FromDays(intervalDays);
+ }
+}
diff --git a/src/PowderCoating.Web/Program.cs b/src/PowderCoating.Web/Program.cs
index 2a8db78..63e73f2 100644
--- a/src/PowderCoating.Web/Program.cs
+++ b/src/PowderCoating.Web/Program.cs
@@ -259,6 +259,7 @@ builder.Services.AddHostedService();
builder.Services.AddHostedService();
builder.Services.AddHostedService();
builder.Services.AddHostedService();
+builder.Services.AddHostedService();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();