Newsletter Service API
Manage subscribers, campaigns, and send newsletters via the TeamDay Newsletter Service REST API.
Newsletter Service API
The Newsletter Service is a headless microservice that manages subscribers, campaigns, and email delivery via Mailgun. It's the first of TeamDay's backend services — a pattern that will extend to other AI Offices (SEO, Design Studio, etc.).
Base URL: All endpoints are accessed through the Nuxt proxy at /api/services/newsletter/.
Authentication: Include an Organization API Token or Personal Access Token in the Authorization header.
curl -H "Authorization: Bearer otk_your-token-here" \
https://cc.teamday.ai/api/services/newsletter/subscribersArchitecture
Your App / Script
│
▼
Nuxt Proxy (/api/services/newsletter/*)
│ Authenticates (Firebase JWT / PAT / Org Token)
│ Resolves orgId
│ Checks scopes (otk_ tokens)
▼
Newsletter Service (H3, port 3200)
│ Row-Level Security (Postgres RLS)
▼
PostgreSQL (newsletter schema)
│
▼
Mailgun (email delivery)Multi-tenancy: Every table has an org_id column enforced by Postgres Row-Level Security. Even if application code has a bug, data can't leak across organizations.
Subscribers
List Subscribers
GET /api/services/newsletter/subscribers?limit=20&cursor={id}Query parameters:
| Param | Type | Default | Description |
|---|---|---|---|
limit | number | 20 | Items per page (max 100) |
cursor | string | — | Cursor for next page (from pagination.nextCursor) |
status | string | — | Filter by status: active, unsubscribed, bounced, complained |
tag | string | — | Filter by tag (exact match within JSON array) |
Response:
{
"data": [
{
"id": "uuid",
"orgId": "org123",
"email": "jane@example.com",
"name": "Jane Doe",
"status": "active",
"tags": ["signup", "beta"],
"unsubscribeToken": "tok_abc",
"createdAt": "2026-02-24T00:00:00.000Z",
"updatedAt": "2026-02-24T00:00:00.000Z"
}
],
"pagination": {
"nextCursor": "uuid-of-last-item",
"hasMore": true
}
}Create Subscriber
POST /api/services/newsletter/subscribers{
"email": "jane@example.com",
"name": "Jane Doe",
"tags": ["signup"]
}Response: Returns the created subscriber object.
Note: Email addresses are stored lowercase. Duplicate emails within the same org return a 409 Conflict.
Get Subscriber
GET /api/services/newsletter/subscribers/{id}Update Subscriber
PATCH /api/services/newsletter/subscribers/{id}{
"name": "Jane Smith",
"tags": ["vip", "beta"],
"status": "active"
}Delete Subscriber
DELETE /api/services/newsletter/subscribers/{id}Import Subscribers (CSV)
POST /api/services/newsletter/subscribers/import{
"csv": "email,name,tags\njane@example.com,Jane Doe,vip;beta\njohn@example.com,John Smith,beta"
}CSV format:
- Required column:
email - Optional columns:
name,tags(semicolon-separated) - First row must be headers
- Duplicates are silently skipped (ON CONFLICT DO NOTHING)
Response:
{
"imported": 2,
"total": 2
}Campaigns
List Campaigns
GET /api/services/newsletter/campaigns?limit=20&cursor={id}Create Campaign
POST /api/services/newsletter/campaigns{
"name": "Weekly Update #12",
"subject": "What's new this week",
"fromName": "TeamDay",
"fromEmail": "newsletter@mail.teamday.ai",
"htmlBody": "<h1>Weekly Update</h1><p>Here's what happened...</p>",
"textBody": "Weekly Update\n\nHere's what happened..."
}Campaigns are created in draft status.
Get Campaign
GET /api/services/newsletter/campaigns/{id}Update Campaign
PATCH /api/services/newsletter/campaigns/{id}Only draft campaigns can be updated.
{
"subject": "Updated Subject Line",
"htmlBody": "<h1>New content</h1>"
}Send Campaign
POST /api/services/newsletter/campaigns/{id}/sendSends the campaign to all active subscribers. The campaign status transitions: draft -> sending -> sent (or failed).
Response:
{
"success": true,
"stats": {
"total": 150,
"sent": 150,
"failed": 0
}
}Campaign Stats
GET /api/services/newsletter/campaigns/{id}/statsReturns delivery statistics including sends, opens, clicks, bounces, and complaints (populated via Mailgun webhooks).
Campaign Preview
GET /api/services/newsletter/campaigns/{id}/previewReturns the rendered HTML for previewing before send.
Test Send
Send a Test Email
POST /api/services/newsletter/test/send{
"to": "you@example.com",
"subject": "Test Email",
"html": "<h1>Hello!</h1><p>This is a test.</p>",
"text": "Hello! This is a test."
}This is a convenience endpoint that:
- Creates (or reuses) a subscriber record for the recipient
- Creates a test campaign
- Sends the email via Mailgun
- Records the send and marks the campaign as sent
Response:
{
"success": true,
"messageId": "<msg-id@mailgun>",
"to": "you@example.com",
"subject": "Test Email",
"unsubscribeUrl": "https://cc.teamday.ai/unsubscribe/tok_abc"
}Public Endpoints
These endpoints don't require authentication:
Unsubscribe Page
GET /unsubscribe/{token}Returns a minimal HTML confirmation page. Subscribers see this when clicking the unsubscribe link in emails.
Process Unsubscribe
POST /unsubscribe/{token}Sets the subscriber's status to unsubscribed and records the timestamp. Idempotent — calling multiple times is safe.
Mailgun Webhooks
POST /webhooks/mailgunReceives delivery events from Mailgun (delivered, opened, clicked, bounced, complained). Signature verified via HMAC-SHA256.
Events processed:
delivered— Email successfully deliveredopened— Recipient opened the emailclicked— Recipient clicked a linkfailed/bounced— Delivery failed, subscriber auto-marked as bouncedcomplained— Spam complaint, subscriber auto-unsubscribed
Health Check
GET /healthReturns { "status": "ok" }. No auth required.
Pagination
All list endpoints use cursor-based pagination for consistent results even when data changes between requests.
# First page
curl -H "Authorization: Bearer $TOKEN" \
"https://cc.teamday.ai/api/services/newsletter/subscribers?limit=10"
# Next page (use nextCursor from previous response)
curl -H "Authorization: Bearer $TOKEN" \
"https://cc.teamday.ai/api/services/newsletter/subscribers?limit=10&cursor=uuid-from-response"Pagination response format:
{
"data": [...],
"pagination": {
"nextCursor": "last-item-id",
"hasMore": true
}
}When hasMore is false, you've reached the last page.
Quick Start: CLI Workflow
Here's the complete newsletter workflow using curl and a Personal Access Token. Verified end-to-end on production.
1. Test your connection
export TOKEN="td_your-token-here"
curl -s -H "Authorization: Bearer $TOKEN" \
https://cc.teamday.ai/api/services/newsletter/health
# → {"status":"ok","service":"newsletter"}2. Import your team as subscribers
# Fetch org members and import as subscribers via CSV
ORG_ID="your-org-id"
curl -s -H "Authorization: Bearer $TOKEN" \
"https://cc.teamday.ai/api/organizations/$ORG_ID/members" \
| jq -r '"email,name,tags", (.members[] | select(.email) | "\(.email),\(.displayName // ""),team")' \
> /tmp/team.csv
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"csv\": $(jq -Rs . /tmp/team.csv)}" \
https://cc.teamday.ai/api/services/newsletter/subscribers/import
# → {"imported":3,"skipped":0,"total":3}3. Create a campaign, set content, and preview
# Create
CAMPAIGN=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Weekly Update #1"}' \
https://cc.teamday.ai/api/services/newsletter/campaigns)
CAMPAIGN_ID=$(echo $CAMPAIGN | jq -r '.id')
# Set subject + HTML
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"subject":"This Week at TeamDay","htmlBody":"<h1>Hello!</h1><p>News here...</p>"}' \
"https://cc.teamday.ai/api/services/newsletter/campaigns/$CAMPAIGN_ID"
# Preview HTML
curl -s -H "Authorization: Bearer $TOKEN" \
"https://cc.teamday.ai/api/services/newsletter/campaigns/$CAMPAIGN_ID/preview"4. Send a test email first
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"to":"you@example.com","subject":"Test","html":"<h1>Preview</h1>"}' \
https://cc.teamday.ai/api/services/newsletter/test/send
# → {"success":true,"messageId":"<msg-id@mail.teamday.ai>"}5. Send to all subscribers
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
"https://cc.teamday.ai/api/services/newsletter/campaigns/$CAMPAIGN_ID/send"Complete Example: Newsletter Signup Integration
Here's how to connect a website signup form to the TeamDay Newsletter:
// 1. Generate an org token with 'newsletter' scope
// Organization Settings → Org Tokens → Generate Token
// 2. In your website's API handler:
const TEAMDAY_TOKEN = process.env.TEAMDAY_ORG_TOKEN
export async function handleSignup(email: string, name: string) {
// Add to newsletter
const res = await fetch(
'https://cc.teamday.ai/api/services/newsletter/subscribers',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${TEAMDAY_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
name,
tags: ['website-signup', new Date().getFullYear().toString()],
}),
}
)
if (res.status === 409) {
// Subscriber already exists — not an error
return { status: 'already_subscribed' }
}
if (!res.ok) {
throw new Error(`Newsletter sync failed: ${res.statusText}`)
}
return { status: 'subscribed' }
}Related Documentation
- Organization API Tokens — Generate scoped tokens for service access
- Authentication — Overview of all auth methods
- API Reference — Full API documentation
Movie & Ads Studio
Produce films and video ads from a written brief: AI drafts the scenes, renders a storyboard with consistent characters, waits for your approval, then animates and cuts the final video.
Org Secrets
Store encrypted API keys, tokens, and runtime variables that any agent in your organization can use without exposing the raw value.