diff --git a/src/ScadaLink.Commons/Entities/Notifications/Notification.cs b/src/ScadaLink.Commons/Entities/Notifications/Notification.cs
new file mode 100644
index 0000000..ab94f56
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Notifications/Notification.cs
@@ -0,0 +1,48 @@
+using ScadaLink.Commons.Types.Enums;
+
+namespace ScadaLink.Commons.Entities.Notifications;
+
+///
+/// A single notification queued in the central outbox. Created at a site (where the
+/// GUID is generated) and forwarded to the central cluster
+/// for delivery, retry, and audit. The lifecycle is tracked by .
+///
+public class Notification
+{
+ /// GUID primary key, generated at the originating site.
+ public string NotificationId { get; set; }
+ public NotificationType Type { get; set; }
+ public string ListName { get; set; }
+ public string Subject { get; set; }
+ public string Body { get; set; }
+
+ /// JSON extensibility hook for channel-specific payload data.
+ public string? TypeData { get; set; }
+ public NotificationStatus Status { get; set; } = NotificationStatus.Pending;
+ public int RetryCount { get; set; }
+ public string? LastError { get; set; }
+
+ /// Resolved delivery targets snapshotted at delivery time, for audit.
+ public string? ResolvedTargets { get; set; }
+ public string SourceSiteId { get; set; }
+ public string? SourceInstanceId { get; set; }
+ public string? SourceScript { get; set; }
+ public DateTimeOffset SiteEnqueuedAt { get; set; }
+
+ /// Central ingest time.
+ public DateTimeOffset CreatedAt { get; set; }
+ public DateTimeOffset? LastAttemptAt { get; set; }
+ public DateTimeOffset? NextAttemptAt { get; set; }
+ public DateTimeOffset? DeliveredAt { get; set; }
+
+ public Notification(string notificationId, NotificationType type, string listName,
+ string subject, string body, string sourceSiteId)
+ {
+ NotificationId = notificationId ?? throw new ArgumentNullException(nameof(notificationId));
+ Type = type;
+ ListName = listName ?? throw new ArgumentNullException(nameof(listName));
+ Subject = subject ?? throw new ArgumentNullException(nameof(subject));
+ Body = body ?? throw new ArgumentNullException(nameof(body));
+ SourceSiteId = sourceSiteId ?? throw new ArgumentNullException(nameof(sourceSiteId));
+ }
+}
diff --git a/tests/ScadaLink.Commons.Tests/Entities/NotificationEntityTests.cs b/tests/ScadaLink.Commons.Tests/Entities/NotificationEntityTests.cs
new file mode 100644
index 0000000..75bc2c5
--- /dev/null
+++ b/tests/ScadaLink.Commons.Tests/Entities/NotificationEntityTests.cs
@@ -0,0 +1,33 @@
+using ScadaLink.Commons.Entities.Notifications;
+using ScadaLink.Commons.Types.Enums;
+
+namespace ScadaLink.Commons.Tests.Entities;
+
+///
+/// Verifies the outbox entity's constructor defaults
+/// and null-argument guards on required reference-type parameters.
+///
+public class NotificationEntityTests
+{
+ [Fact]
+ public void Constructor_SetsDefaults()
+ {
+ var n = new Notification("id-1", NotificationType.Email, "ops-team", "subj", "body", "SiteA");
+ Assert.Equal(NotificationStatus.Pending, n.Status);
+ Assert.Equal(0, n.RetryCount);
+ Assert.Equal("id-1", n.NotificationId);
+ Assert.Equal(NotificationType.Email, n.Type);
+ Assert.Equal("ops-team", n.ListName);
+ Assert.Equal("SiteA", n.SourceSiteId);
+ }
+
+ [Fact]
+ public void Constructor_NullListName_Throws()
+ => Assert.Throws(
+ () => new Notification("id", NotificationType.Email, null!, "s", "b", "SiteA"));
+
+ [Fact]
+ public void Constructor_NullNotificationId_Throws()
+ => Assert.Throws(
+ () => new Notification(null!, NotificationType.Email, "list", "s", "b", "SiteA"));
+}