Email content and domain endpoints

Reusable templates

Campaign-less EmailVariation rows (campaignId = NULL). Managed at /email-campaigns/templates.

GET /api/email-templates

page, pageSize.

POST /api/email-templates

Creates a template. Templates are database-only — no Resend template is created until the template is copied into a campaign.

PATCH /api/email-templates/{id} / DELETE /api/email-templates/{id}

Because every send and selection query filters on campaignId, a template can never be sent directly.

Variation library

GET /api/email-variations

A cross-campaign read-only view of variations. page, pageSize. Backs /email-campaigns/variations.

PATCH /api/email-variations/{id}

Updates a variation from the library view.

Serialisation lives in lib/serializers/email-variation.ts.

Domains

A Domain is a Resend-verified sending domain that owns one or more email accounts and holds the shared daily cap.

GET /api/email-domains

Domains with their accounts and today’s usage (daily_sent_total, the sum of the accounts' sends for the current UTC day). An account whose dailySentDate is an earlier day reports daily_sent_count: 0.

POST /api/email-domains

Creates a domain. Supply the resendDomainId obtained from GET /api/email-domains/resend-domains.

PATCH /api/email-domains/{id} / DELETE /api/email-domains/{id}

Update dailyCap, isActive, warmupStatus, or remove the domain.

GET /api/email-domains/resend-domains

Lists verified domains from Resend, so the UI can offer real domain ids rather than free text.

GET /api/email-domains/stats

Per-account and per-domain counters: sentCount, receivedCount, openedCount, clickedCount, bouncedCount, unsubscribedCount, dailySentCount, totalSentCount.

GET /api/email-domains/summary

Rolled-up figures for the dashboard header. sent_today counts only today’s (UTC) sends. daily_capacity adds, per active domain, the lower of the domain cap and its active accounts' summed caps.

Serialisation lives in lib/serializers/email-domain.ts.

Sending accounts

EmailSendingDomain — a concrete From address under a parent domain. Called an "email account" in the UI.

GET /api/sending-domains

domain_id filters to one parent domain.

POST /api/sending-domains

Creates an account: domainId, domain, fromName, fromEmail, resendDomainId, dailyCap, isActive, isVerified. The fromEmail host must match the parent Domain.name exactly.

PATCH /api/sending-domains/{id} / DELETE /api/sending-domains/{id}

Only active accounts under an active domain, under both daily caps, are eligible for a send. See Email deliverability.

Unsubscribe

GET /api/unsubscribe?token=<base64url>

Public — no authentication. Renders the confirmation page. token is the EmailCampaignTarget id encoded as base64url.

POST /api/unsubscribe

The RFC 8058 one-click endpoint referenced by the List-Unsubscribe-Post header.

Effect

Sets TargetEmails.newsletterOptOut with newsletterOptOutAt, which suppresses the address across every campaign in the workspace, and publishes Contact.Unsubscribe to the CRM queue with legal_basis = LEGITIMATE_INTEREST_OTHER and the explanation constant from lib/constants.ts.

Suppression is per address, not per campaign — an unsubscribe from one campaign stops all of them.

Daily reset

There is no HTTP endpoint. The Inngest function sendingDomainDailyResetCron runs at 00:00 UTC, zeroes dailySentCount and clears dailySentDate on every account. totalSentCount is never reset. The daily cap already ignores counters from an earlier UTC day, so the reset is a backstop. See Cron jobs.