diff --git a/docs/sections/Mailer.md b/docs/sections/Mailer.md
index 4bdc9b6c4..d095acabc 100644
--- a/docs/sections/Mailer.md
+++ b/docs/sections/Mailer.md
@@ -62,9 +62,9 @@ All routes are `AdminOnly`.
- **Profiles**: reads `IUserEmailService.FindVerifiedEmailWithUserAsync`, `FindAnyUserIdByEmailAsync`, `DeleteEmailAsync`, `GetPrimaryEmailsByUserIdsAsync`; reads/writes `ICommunicationPreferenceService.GetAsync` / `UpdatePreferenceAsync` / `GetCountByCategoryAndStateAsync`.
- **Users**: writes via `IAccountProvisioningService.FindOrCreateUserByEmailAsync`; reads `IUserService.GetByIdAsync` (tombstone follow), `IUserService.GetCountByContactSourceAsync`.
-- **Tickets**: `ITicketQueryService.GetUserIdsWithTicketsAsync` — audience-side ticket-holder enumeration for `TicketNoShiftsAudience` and `HasTicketAudience`. Scoped to the active vendor event by the cached decorator (see `TicketSyncState.VendorEventId`).
+- **Tickets**: `ITicketQueryService.GetUserIdsWithTicketsAsync` — audience-side ticket-holder enumeration for `TicketNoShiftsAudience`, `HasTicketAudience`, and `MarketingNoTicketAudience`. Scoped to the active vendor event by the cached decorator (see `TicketSyncState.VendorEventId`).
- **Shifts**: `IShiftView.GetUsersAsync` + `IUserService.GetAllUserInfosAsync` — cached per-user shift signups, used by `TicketNoShiftsAudience` and `HasShiftAudience` (encode Pending/Confirmed-on-active-event via `ShiftUserView.HasShift`).
-- **Users**: `IUserService.GetAllUserInfosAsync` — audience-side enumeration of explicit Marketing opt-ins for `MarketingAudience` (`UserInfo.MarketingOptedOut == false`).
+- **Users**: `IUserService.GetAllUserInfosAsync` — audience-side enumeration of explicit Marketing opt-ins for `MarketingAudience` and `MarketingNoTicketAudience` (`UserInfo.MarketingOptedOut == false`).
- **AuditLog**: writes via `IAuditLogService.LogAsync` (job overload).
## Architecture
diff --git a/src/Humans.Application/Services/Mailer/Audiences/MarketingNoTicketAudience.cs b/src/Humans.Application/Services/Mailer/Audiences/MarketingNoTicketAudience.cs
new file mode 100644
index 000000000..0f316724b
--- /dev/null
+++ b/src/Humans.Application/Services/Mailer/Audiences/MarketingNoTicketAudience.cs
@@ -0,0 +1,30 @@
+using Humans.Application.Interfaces.Mailer;
+using Humans.Application.Interfaces.Tickets;
+using Humans.Application.Interfaces.Users;
+
+namespace Humans.Application.Services.Mailer.Audiences;
+
+///
+/// "Humans - Marketing no Ticket" — humans who have explicitly opted in to the
+/// Marketing communication category ( == false)
+/// AND do not currently hold a ticket in the active vendor event.
+/// Users with no Marketing preference row (default-off) or who hold a ticket are excluded.
+///
+public sealed class MarketingNoTicketAudience(
+ IUserService users,
+ ITicketQueryService tickets) : IMailerAudience
+{
+ public string Key => "marketing-no-ticket";
+ public string DisplayName => "Marketing opt-ins without a ticket";
+ public string MailerLiteGroupName => "Humans - Marketing no Ticket";
+
+ public async Task> ComputeMemberUserIdsAsync(CancellationToken ct)
+ {
+ var ticketHolders = await tickets.GetUserIdsWithTicketsAsync();
+ var allUsers = await users.GetAllUserInfosAsync(ct);
+ return allUsers
+ .Where(u => u.MarketingOptedOut == false && !ticketHolders.Contains(u.Id))
+ .Select(u => u.Id)
+ .ToHashSet();
+ }
+}
diff --git a/src/Humans.Web/Extensions/Sections/MailerSectionExtensions.cs b/src/Humans.Web/Extensions/Sections/MailerSectionExtensions.cs
index 061fb022b..176a67e50 100644
--- a/src/Humans.Web/Extensions/Sections/MailerSectionExtensions.cs
+++ b/src/Humans.Web/Extensions/Sections/MailerSectionExtensions.cs
@@ -52,6 +52,7 @@ internal static IServiceCollection AddMailerSection(
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddTransient();
return services;
diff --git a/tests/Humans.Application.Tests/Services/Mailer/Audiences/MarketingNoTicketAudienceTests.cs b/tests/Humans.Application.Tests/Services/Mailer/Audiences/MarketingNoTicketAudienceTests.cs
new file mode 100644
index 000000000..4b5f34ca8
--- /dev/null
+++ b/tests/Humans.Application.Tests/Services/Mailer/Audiences/MarketingNoTicketAudienceTests.cs
@@ -0,0 +1,86 @@
+using AwesomeAssertions;
+using Humans.Application.Interfaces.Tickets;
+using Humans.Application.Interfaces.Users;
+using Humans.Application.Services.Mailer.Audiences;
+using Humans.Application.Tests.Infrastructure;
+using Humans.Domain.Entities;
+using Humans.Domain.Enums;
+using NodaTime;
+using NSubstitute;
+
+namespace Humans.Application.Tests.Services.Mailer.Audiences;
+
+public class MarketingNoTicketAudienceTests
+{
+ [HumansFact]
+ public async Task ComputeMemberUserIdsAsync_ExcludesTicketHolders_AndNonOptIns()
+ {
+ var optInNoTicket = Guid.NewGuid(); // OptedOut=false, no ticket → IN
+ var optInWithTicket = Guid.NewGuid(); // OptedOut=false, has ticket → OUT
+ var optedOut = Guid.NewGuid(); // OptedOut=true → OUT
+ var noPrefRow = Guid.NewGuid(); // no row → OUT
+
+ var audience = NewAudience(
+ users: new[]
+ {
+ UserWithMarketingPref(optInNoTicket, optedOut: false),
+ UserWithMarketingPref(optInWithTicket, optedOut: false),
+ UserWithMarketingPref(optedOut, optedOut: true),
+ UserWithoutMarketingPref(noPrefRow),
+ },
+ ticketHolders: [optInWithTicket]);
+
+ var members = await audience.ComputeMemberUserIdsAsync(CancellationToken.None);
+
+ members.Should().BeEquivalentTo([optInNoTicket]);
+ }
+
+ [HumansFact]
+ public async Task ComputeMemberUserIdsAsync_NoUsers_ReturnsEmpty()
+ {
+ var audience = NewAudience([], []);
+
+ var members = await audience.ComputeMemberUserIdsAsync(CancellationToken.None);
+
+ members.Should().BeEmpty();
+ }
+
+ [HumansFact]
+ public void Metadata_UsesHumansPrefix()
+ {
+ var audience = NewAudience([], []);
+ audience.Key.Should().Be("marketing-no-ticket");
+ audience.MailerLiteGroupName.Should().Be("Humans - Marketing no Ticket");
+ audience.MailerLiteGroupName.Should().StartWith("Humans - ");
+ }
+
+ private static MarketingNoTicketAudience NewAudience(
+ IReadOnlyList users,
+ HashSet ticketHolders)
+ {
+ var userService = Substitute.For();
+ userService.GetAllUserInfosAsync(Arg.Any()).Returns(users);
+
+ var ticketService = Substitute.For();
+ ticketService.GetUserIdsWithTicketsAsync().Returns(ticketHolders);
+
+ return new MarketingNoTicketAudience(userService, ticketService);
+ }
+
+ private static UserInfo UserWithMarketingPref(Guid userId, bool optedOut) =>
+ UserInfo.Create(
+ new User { Id = userId, DisplayName = "u", PreferredLanguage = "en" },
+ [], [], [], profile: null, [], [], [],
+ [new CommunicationPreference
+ {
+ Id = Guid.NewGuid(),
+ UserId = userId,
+ Category = MessageCategory.Marketing,
+ OptedOut = optedOut,
+ UpdatedAt = Instant.FromUnixTimeSeconds(0),
+ UpdateSource = "Test",
+ }]);
+
+ private static UserInfo UserWithoutMarketingPref(Guid userId) =>
+ UserInfoStubHelpers.MakeUserInfo(userId);
+}