Skip to Content
Newsletters

Email Newsletters

Leadmetrics has a built-in newsletter feature that lets tenants send email campaigns to their contacts. Newsletters are written by the email-writer agent, reviewed by the DM team, approved by the client, and then sent via Resend.


Status Machine

draft → dm_review → approved → sending → sent ↓ ↓ (reject) (fail) draft failed
StatusWho sets itWhat it means
draftemail-writer agent (or DM rejection)Initial state; editable
dm_reviewAgent completes → auto-transitionsWaiting for DM team review
approvedDM reviewer via portalReady for the client to send
sendingSystem when send is queuedSend in progress — no more edits
sentSystem after Resend confirms deliverySent; immutable
failedSystem on send errorDelivery failed; failedReason populated

Data Model

EmailNewsletter ├── id cuid ├── tenantId FK → Tenant ├── activityId FK → Activity (nullable, @unique) ├── subject String ├── previewText String? (shown in email client preview) ├── bodyText String (source Markdown — agent writes this) ├── bodyHtml String? (auto-generated from bodyText on save) ├── fromEmail String? (overrides NEWSLETTER_FROM_EMAIL) ├── fromName String? (overrides NEWSLETTER_FROM_NAME) ├── status String (see state machine above) ├── audienceSegment "all" | "leads" | "customers" ├── recipientCount Int (populated after send) ├── sentAt DateTime? ├── failedReason String? ├── dmApprovedBy String? (userId) ├── dmApprovedAt DateTime? ├── clientApprovedBy String? ├── clientApprovedAt DateTime? ├── dmRejectionNote String? └── clientRejectionNote String?

Audience Segments

SegmentWho receives the email
allAll contacts with isSubscriber: true
leadsContacts where type = "lead" and isSubscriber: true
customersContacts where type = "customer" and isSubscriber: true

API Endpoints

Tenant (client-facing) — prefix /tenant/v1/newsletters

MethodPathAuthDescription
GET/Tenant userList newsletters; filter by ?status=
GET/:idTenant userGet one newsletter
POST/:id/sendTenant userTrigger send (must be approved)
GET/unsubscribe?token=PublicUnsubscribe a contact by token

DM Portal — prefix /dm/v1/newsletters

MethodPathAuthDescription
GET/DM userList newsletters for a tenant (?tenantId=)
GET/:idDM userGet one newsletter
PATCH/:id/dm-approveDM userApprove (must be dm_review) → sets status to approved
PATCH/:id/dm-rejectDM userReject (must be dm_review) → sets status to draft
PATCH/:idDM userEdit subject, body, fromEmail, fromName, audienceSegment

Send Flow

  1. Client clicks “Send” on an approved newsletter in the Dashboard.
  2. POST /tenant/v1/newsletters/:id/send is called — enqueues a newsletter_send job via enqueueNewsletterSend().
  3. The agents worker picks up the job and calls Resend to send to all matching contacts.
  4. Newsletter status transitions: approved → sending → sent (or failed).
  5. recipientCount and sentAt are populated on success.

Unsubscribe

Each contact has a unique unsubscribeToken (generated on creation). Newsletter footers include a link:

/unsubscribe?token={contact.unsubscribeToken}

Clicking the link calls GET /tenant/v1/newsletters/unsubscribe?token=... (public, no auth). This sets contact.isSubscriber = false and contact.unsubscribedAt = now. The contact is excluded from all future sends.


Configuration

Env varRequiredDescription
RESEND_API_KEYYesResend API key for sending
NEWSLETTER_FROM_EMAILYesDefault from address (newsletters@yourdomain.com)
NEWSLETTER_FROM_NAMEYesDefault sender name
APP_URLYesUsed to construct unsubscribe links in the footer

All four live in .env (used by both apps/api and apps/servers/agents).


UI

Dashboard (/newsletters)

List/Calendar toggle in the header. Calendar view uses ContentCalendar from @leadmetrics/ui with orange tiles (bg-orange-100 text-orange-800). Date placed from sentAt ?? createdAt. Clicking a tile opens a floating popup with subject, previewText, audience segment, recipient count, and a “View Newsletter” link. Both views share the same data (all newsletters fetched in one query, no pagination).

DM Portal (/newsletters)

Same list/calendar toggle. Calendar uses the same ContentCalendar component. Status filter tabs (All / In Review / Approved / Sent) and pagination are list-only. A second API call (limit=200, no status filter) feeds the calendar. Orange tile color and popup structure are identical to the dashboard.


Notes

  • bodyHtml is auto-generated from bodyText when the DM edits and saves the body via the portal. The conversion handles Markdown headings, bold, italic, and paragraph breaks.
  • Newsletters in sending or sent status cannot be edited.
  • The DM can edit subject, previewText, bodyText, fromEmail, fromName, and audienceSegment while the newsletter is in draft, dm_review, or approved state.

© 2026 Leadmetrics — Internal use only