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)
Create sender account
Connect a site-scoped SMTP sender with from address and auth credentials.
POST /api/v1/email/sendersRun SMTP test
Validate TLS and credentials before verification.
POST /api/v1/email/senders/:id/testVerify sender
Mark sender as verified only after successful SMTP handshake.
POST /api/v1/email/senders/:id/verifyCreate and queue campaign
Create campaign with senderAccountId and recipients; choose sendNow or scheduledAt.
POST /api/v1/email/campaignsObserve 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:
subscribedalready_subscribedreactivatedaccepted(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-keysPOST /api/v1/email/public-api-keys/:id/rotatePOST /api/v1/email/public-api-keys/:id/revoke
Use newsletter_subscribe scope for newsletter API-only subscriptions.
4. Common errors
400 Invalid email400 consentGranted must be true for API newsletter subscription400 Invalid or revoked API key404 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
| Field | Required | Type | Used by events | Description |
|---|---|---|---|---|
siteId (sender) | Required | uuid | Sender create/list scope | Sender accounts are always site-scoped. |
smtpHost, smtpPort, smtpSecure | Required | string, int, boolean | SMTP transport | TLS-enforced SMTP connection settings for customer mailbox. |
smtpUsername, smtpPassword | Required | string | SMTP auth | Encrypted at rest via EncryptionService. |
senderAccountId (campaign) | Required | uuid | Campaign create/update | Must reference a verified and active sender account in the same site. |
name, subject, htmlContent | Required | string | Campaign message generation | Campaign identity and email body fields. |
audienceConfig | Recommended | object | Unified audience + segment snapshot | sources (manual_csv/newsletter/users), segmentTargetingMode, segmentIds, usersRequireOptIn. |
recipients | Optional (manual_csv source only) | array | Manual/CSV source input | Backward compatible. Required when audienceConfig.sources includes manual_csv. |
sendNow / scheduledAt | Optional (mutually exclusive) | boolean / ISO datetime | Queueing strategy | Cannot 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 neededThe current implementation is SMTP-first and does not use platform fallback sender domains.
Sender Lifecycle
| State/Action | Behavior | Operational Meaning |
|---|---|---|
| pending | Initial state after create or credential change | Sender is not yet allowed for campaign delivery. |
| POST /email/senders/:id/test | Runs SMTP verify and records latency/error | Connectivity and credentials validation step. |
| POST /email/senders/:id/verify | Requires successful test then sets verifiedAt | Sender becomes eligible for campaign sends. |
| error | Set on failed tests or sender-level SMTP failures | Operational intervention needed before new sends. |
| disabled | Soft disable via DELETE endpoint | Sender cannot be used; default sender may rotate automatically. |
Hybrid Telemetry Model
| Signal | Collection Method | Coverage |
|---|---|---|
| Open | Signed token pixel endpoint | Full support in first release. |
| Click | Signed token redirect endpoint | Full support in first release. |
| Unsubscribe | One-click token endpoint + suppression write | Full support in first release. |
| Sent/Fail | SMTP send worker event writes | Full support in first release. |
| Bounce/Complaint | Provider webhook ingestion if connector active | Full for webhook-capable providers, limited for generic SMTP. |
| Generic SMTP bounce fallback | Send-time SMTP errors + suppression | No 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
audienceConfigandemail/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 tokensOutbox 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/subscribewithx-selwise-api-key. - Confirm endpoints under
/api/v1/public/sites/:siteKey/newsletter/confirmare 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