Skip to main content

Email Marketing

Email Marketing (SMTP-First)

Send campaigns from each customer account, enforce sender verification, and collect delivery telemetry with tracking and suppression.

Audience: Marketing operators and integration engineers running customer-owned email sending.

Critical: Email campaigns require senderAccountId and the sender account must be SMTP tested plus verified before queueing delivery.

Who This Page Is For

Use this page when you need the full operational contract for sender onboarding, SMTP delivery, tracking, unsubscribe handling, provider webhook ingestion, and suppression behavior.

Quick Start (2-5 Minutes)

1

Create sender account

Connect a site-scoped SMTP sender with from address and auth credentials.

POST /api/v1/email/senders
2

Run SMTP test

Validate TLS and credentials before verification.

POST /api/v1/email/senders/:id/test
3

Verify sender

Mark sender as verified only after successful SMTP handshake.

POST /api/v1/email/senders/:id/verify
4

Create and queue campaign

Create campaign with senderAccountId and recipients; choose sendNow or scheduledAt.

POST /api/v1/email/campaigns
5

Observe tracking + suppression

Track open/click/unsubscribe and monitor send failures or provider webhook events.

GET /api/v1/public/sites/:siteKey/email/open/:token
GET /api/v1/public/sites/:siteKey/email/click/:token
GET /api/v1/public/sites/:siteKey/email/unsubscribe?token=...

Core endpoints

# Sender accounts
POST   /api/v1/email/senders
GET    /api/v1/email/senders?siteId=...
GET    /api/v1/email/senders/:id
PUT    /api/v1/email/senders/:id
POST   /api/v1/email/senders/:id/test
POST   /api/v1/email/senders/:id/verify
POST   /api/v1/email/senders/:id/set-default
DELETE /api/v1/email/senders/:id

# Email campaigns
POST /api/v1/email/campaigns
GET  /api/v1/email/campaigns
GET  /api/v1/email/campaigns/:id
PUT  /api/v1/email/campaigns/:id
POST /api/v1/email/campaigns/:id/cancel

# Provider connectors
GET    /api/v1/email/provider-connectors
POST   /api/v1/email/provider-connectors
PUT    /api/v1/email/provider-connectors/:id
DELETE /api/v1/email/provider-connectors/:id
GET    /api/v1/email/provider-connectors/:id/health

# Suppressions
GET  /api/v1/email/suppressions
POST /api/v1/email/suppressions
POST /api/v1/email/suppressions/:id/lift

# Unified audience + site public API keys
GET  /api/v1/email/audience/contacts
POST /api/v1/email/audience/preview
POST /api/v1/email/audience/sync-users
GET  /api/v1/email/public-api-keys
POST /api/v1/email/public-api-keys
POST /api/v1/email/public-api-keys/:id/rotate
POST /api/v1/email/public-api-keys/:id/revoke

# Provider webhook ingestion (optional connector model)
POST /api/v1/email/providers/:provider/events

# Tenant newsletter public endpoints
POST /api/v1/public/sites/:siteKey/newsletter
POST /api/v1/public/sites/:siteKey/newsletter/api/subscribe
# Legacy tenant confirm endpoints (backward compatibility only)
POST /api/v1/public/sites/:siteKey/newsletter/confirm
GET  /api/v1/public/sites/:siteKey/newsletter/confirm?token=...

# Public tracking
GET /api/v1/public/sites/:siteKey/email/open/:token
GET /api/v1/public/sites/:siteKey/email/click/:token
GET /api/v1/public/sites/:siteKey/email/unsubscribe?token=...

Public Newsletter Subscribe Usage

Use this flow when you want to collect tenant newsletter contacts from storefront/widget or API-only clients.

1. Browser/Widget Flow (site scoped)

Endpoint:

POST /api/v1/public/sites/:siteKey/newsletter

Minimum payload:

{
"email": "alice@example.com"
}

Example request:

curl -X POST "https://YOUR_API_DOMAIN/api/v1/public/sites/SITE_KEY/newsletter" \
-H "Content-Type: application/json" \
-H "Origin: https://store.example.com" \
-d '{
"email": "alice@example.com",
"fullName": "Alice Doe",
"visitorId": "v_123",
"siteUserId": "u_456",
"sourcePath": "/newsletter",
"referrer": "https://store.example.com/home",
"autoConnectUsers": true
}'

Typical success response:

{
"success": true,
"status": "subscribed",
"message": "Subscribed successfully.",
"recipientId": "rec_..."
}

Possible status values:

  • subscribed
  • already_subscribed
  • reactivated
  • accepted (honeypot/bot-safe acceptance)

2. API-only Flow (no browser origin dependency)

Endpoint:

POST /api/v1/public/sites/:siteKey/newsletter/api/subscribe

Required header:

  • x-selwise-api-key: swpk_...

Minimum payload:

{
"email": "alice@example.com",
"consentGranted": true
}

Example request:

curl -X POST "https://YOUR_API_DOMAIN/api/v1/public/sites/SITE_KEY/newsletter/api/subscribe" \
-H "Content-Type: application/json" \
-H "x-selwise-api-key: swpk_live_xxxxx" \
-d '{
"email": "alice@example.com",
"fullName": "Alice Doe",
"consentGranted": true,
"siteUserId": "u_456",
"metadataJson": {
"sourceSystem": "crm_sync"
}
}'

3. Create/Rotate API keys (private dashboard API)

  • POST /api/v1/email/public-api-keys
  • POST /api/v1/email/public-api-keys/:id/rotate
  • POST /api/v1/email/public-api-keys/:id/revoke

Use newsletter_subscribe scope for newsletter API-only subscriptions.

4. Common errors

  • 400 Invalid email
  • 400 consentGranted must be true for API newsletter subscription
  • 400 Invalid or revoked API key
  • 404 Site not found (invalid or unverified site)
  • 403 Origin validation failed (browser flow with wrong origin/domain)

5. Backward compatibility note

/public/sites/:siteKey/newsletter/confirm endpoints are legacy compatibility endpoints for old pending-token flows. New browser flow writes directly into tenant audience (EmailRecipient).

Required Fields / Minimum Payload

FieldRequiredTypeUsed by eventsDescription
siteId (sender)RequireduuidSender create/list scopeSender accounts are always site-scoped.
smtpHost, smtpPort, smtpSecureRequiredstring, int, booleanSMTP transportTLS-enforced SMTP connection settings for customer mailbox.
smtpUsername, smtpPasswordRequiredstringSMTP authEncrypted at rest via EncryptionService.
senderAccountId (campaign)RequireduuidCampaign create/updateMust reference a verified and active sender account in the same site.
name, subject, htmlContentRequiredstringCampaign message generationCampaign identity and email body fields.
audienceConfigRecommendedobjectUnified audience + segment snapshotsources (manual_csv/newsletter/users), segmentTargetingMode, segmentIds, usersRequireOptIn.
recipientsOptional (manual_csv source only)arrayManual/CSV source inputBackward compatible. Required when audienceConfig.sources includes manual_csv.
sendNow / scheduledAtOptional (mutually exclusive)boolean / ISO datetimeQueueing strategyCannot use both together in one request.

Create sender (minimal)

{
"siteId": "SITE_UUID",
"name": "Primary Marketing SMTP",
"fromEmail": "marketing@brand.com",
"fromName": "Brand Team",
"smtpHost": "smtp.brand.com",
"smtpPort": 587,
"smtpSecure": false,
"smtpUsername": "smtp-user",
"smtpPassword": "smtp-password",
"isDefault": true
}

Create campaign and send now

{
"siteId": "SITE_UUID",
"senderAccountId": "SENDER_UUID",
"name": "Spring Promo Launch",
"subject": "Spring collection is live",
"previewText": "Early access starts now.",
"htmlContent": "<html><body><a href=\"https://shop.example.com\">Shop now</a></body></html>",
"textContent": "Shop now: https://shop.example.com",
"audienceConfig": {
  "sources": ["manual_csv", "newsletter", "users"],
  "segmentTargetingMode": "include",
  "segmentIds": ["SEGMENT_UUID"],
  "usersRequireOptIn": true
},
"sendNow": true,
"recipients": [
  { "email": "alice@example.com", "name": "Alice" },
  { "email": "bob@example.com", "name": "Bob" }
]
}

Architecture and Delivery Flow

Runtime flow (send-time snapshot)

Campaign create/update
-> persist audienceConfigJson
-> if resolveAtSendTime=true, queue campaign without immediate recipient materialization
-> worker resolves audience snapshot at scheduled/sending time
-> source merge (manual_csv + newsletter + users) + dedupe + suppression + segment include/exclude
-> write audienceSnapshotJson (deliverable/excluded/suppressed reasons)
-> create EmailMessage + EmailMessageOutbox rows from deliverable audience
-> Outbox worker polls pending rows
-> SMTP send via verified sender account
-> EmailEvent write (sent/fail/open/click/unsubscribe/bounce/complaint)
-> EmailSuppression upsert when needed

The current implementation is SMTP-first and does not use platform fallback sender domains.

Sender Lifecycle

State/ActionBehaviorOperational Meaning
pendingInitial state after create or credential changeSender is not yet allowed for campaign delivery.
POST /email/senders/:id/testRuns SMTP verify and records latency/errorConnectivity and credentials validation step.
POST /email/senders/:id/verifyRequires successful test then sets verifiedAtSender becomes eligible for campaign sends.
errorSet on failed tests or sender-level SMTP failuresOperational intervention needed before new sends.
disabledSoft disable via DELETE endpointSender cannot be used; default sender may rotate automatically.

Hybrid Telemetry Model

SignalCollection MethodCoverage
OpenSigned token pixel endpointFull support in first release.
ClickSigned token redirect endpointFull support in first release.
UnsubscribeOne-click token endpoint + suppression writeFull support in first release.
Sent/FailSMTP send worker event writesFull support in first release.
Bounce/ComplaintProvider webhook ingestion if connector activeFull for webhook-capable providers, limited for generic SMTP.
Generic SMTP bounce fallbackSend-time SMTP errors + suppressionNo IMAP/DSN parsing in first release.

What Connectors Do (and Do Not Do)

Connectors are provider webhook ingestion adapters. Their job is to receive provider delivery feedback and convert it into internal email events.

  • They ingest: bounce, complaint, unsubscribe, sent, open, click feedback.
  • They power: suppression updates, deliverability telemetry, and connector health metrics.
  • They do not: import audience contacts or sync users/newsletter subscribers.
  • Audience source unification is handled separately through audienceConfig and email/audience/* endpoints.

Security and Compliance Controls

Concern about plaintext SMTP credentials

Cause: Expectation mismatch.

Fix: Credentials are encrypted at rest (smtpUsernameEncrypted/smtpPasswordEncrypted) and not returned in sender API responses.

Concern about insecure SMTP transport

Cause: Potential plaintext SMTP configuration.

Fix: Transport enforces TLS (requireTLS=true, min TLS 1.2, rejectUnauthorized=true) and invalid port/secure combinations are rejected.

Campaign queued with unverified sender

Cause: Sender verification not completed.

Fix: Campaign create/update validates senderAccountId and blocks non-verified or disabled senders.

Need event authenticity for webhooks

Cause: Provider callback trust boundary.

Fix: Connector can require x-email-webhook-secret header and validates decrypted shared secret.

Additional security notes:

  • All sender create/update/test/verify/disable/default operations are audit logged.
  • Campaign audit metadata includes senderAccountId.
  • Tracking tokens are HMAC-signed and validated with expiration.
  • Unsubscribe and suppression are site-level, so suppression applies across all sender accounts in the same site.

Worker and Environment Controls

Relevant environment variables

EMAIL_OUTBOX_ENABLED=true|false       # disable worker if false
EMAIL_OUTBOX_POLL_MS=5000            # worker poll interval (ms)
EMAIL_OUTBOX_BATCH_SIZE=50           # max outbox rows per poll
EMAIL_TRACKING_BASE_URL=...          # base URL used for open/click/unsubscribe links
EMAIL_TRACKING_SECRET=...            # HMAC secret for tracking tokens

Outbox retries use exponential backoff with jitter and per-row maxAttempts=5. Permanent failures can create suppression records with reason send_failure.

Dashboard Coverage

The web dashboard email marketing coverage includes:

  • /dashboard/email/campaigns
  • /dashboard/email/campaigns/new
  • /dashboard/email/campaigns/:id
  • /dashboard/email/audience
  • /dashboard/email/settings/senders
  • /dashboard/email/settings/connectors
  • /dashboard/email/suppressions

These views support:

  • Campaign create/update/send-now/schedule/cancel flows.
  • Unified audience source management (manual/CSV + newsletter + users).
  • Segment selector integration with send-time snapshot resolution.
  • Audience contacts list and users sync backfill workflow.
  • Site-scoped public API key create/rotate/revoke for API-only newsletter clients.
  • Sender account create/test/verify/default/disable operations.
  • Provider connector create/update/delete + 24h health telemetry.
  • Suppression add/list/lift operations.

Current Scope and Limitations

  • First release SMTP auth is username_password.
  • OAuth2 sender auth type exists in schema for future expansion but is not implemented in sender flows yet.
  • Platform-level fallback sending is intentionally disabled.
  • Generic SMTP asynchronous bounce parsing (IMAP/DSN mailbox parsing) is not included in first release.

Newsletter Separation

Newsletter subscriber operations are intentionally separate from tenant email campaign management:

  • Tenant newsletter browser flow writes directly to tenant audience via /api/v1/public/sites/:siteKey/newsletter (no admin/global sender dependency).
  • API-only tenant subscribe uses /api/v1/public/sites/:siteKey/newsletter/api/subscribe with x-selwise-api-key.
  • Confirm endpoints under /api/v1/public/sites/:siteKey/newsletter/confirm are preserved only for legacy pending-token compatibility.
  • Admin newsletter subscriber operations remain under /api/v1/admin/newsletter/*.
  • Email marketing campaign/sender/connector/suppression data stays under email_* models and routes.

Production Checklist

  • At least one active sender account exists per site and default sender is defined.Required
  • Every sender account passes SMTP test and verification before campaign queueing.Required
  • Campaign payloads always include senderAccountId and valid recipient snapshot.Required
  • Tracking base URL and tracking secret are configured in environment.Required
  • Monitoring tracks send failures, suppression growth, and sender error states.Required

Next Steps