# 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](https://docs.teamday.ai/api/org-tokens) or [Personal Access Token](https://docs.teamday.ai/api/tokens) in the `Authorization` header.

```bash
curl -H "Authorization: Bearer otk_your-token-here" \
  https://cc.teamday.ai/api/services/newsletter/subscribers
```

---

## Architecture

```
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

```http
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:**

```json
{
  "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

```http
POST /api/services/newsletter/subscribers
```

```json
{
  "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

```http
GET /api/services/newsletter/subscribers/{id}
```

### Update Subscriber

```http
PATCH /api/services/newsletter/subscribers/{id}
```

```json
{
  "name": "Jane Smith",
  "tags": ["vip", "beta"],
  "status": "active"
}
```

### Delete Subscriber

```http
DELETE /api/services/newsletter/subscribers/{id}
```

### Import Subscribers (CSV)

```http
POST /api/services/newsletter/subscribers/import
```

```json
{
  "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:**

```json
{
  "imported": 2,
  "total": 2
}
```

---

## Campaigns

### List Campaigns

```http
GET /api/services/newsletter/campaigns?limit=20&cursor={id}
```

### Create Campaign

```http
POST /api/services/newsletter/campaigns
```

```json
{
  "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

```http
GET /api/services/newsletter/campaigns/{id}
```

### Update Campaign

```http
PATCH /api/services/newsletter/campaigns/{id}
```

Only `draft` campaigns can be updated.

```json
{
  "subject": "Updated Subject Line",
  "htmlBody": "<h1>New content</h1>"
}
```

### Send Campaign

```http
POST /api/services/newsletter/campaigns/{id}/send
```

Sends the campaign to all active subscribers. The campaign status transitions: `draft` -> `sending` -> `sent` (or `failed`).

**Response:**

```json
{
  "success": true,
  "stats": {
    "total": 150,
    "sent": 150,
    "failed": 0
  }
}
```

### Campaign Stats

```http
GET /api/services/newsletter/campaigns/{id}/stats
```

Returns delivery statistics including sends, opens, clicks, bounces, and complaints (populated via Mailgun webhooks).

### Campaign Preview

```http
GET /api/services/newsletter/campaigns/{id}/preview
```

Returns the rendered HTML for previewing before send.

---

## Test Send

### Send a Test Email

```http
POST /api/services/newsletter/test/send
```

```json
{
  "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:
1. Creates (or reuses) a subscriber record for the recipient
2. Creates a test campaign
3. Sends the email via Mailgun
4. Records the send and marks the campaign as sent

**Response:**

```json
{
  "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

```http
GET /unsubscribe/{token}
```

Returns a minimal HTML confirmation page. Subscribers see this when clicking the unsubscribe link in emails.

### Process Unsubscribe

```http
POST /unsubscribe/{token}
```

Sets the subscriber's status to `unsubscribed` and records the timestamp. Idempotent — calling multiple times is safe.

### Mailgun Webhooks

```http
POST /webhooks/mailgun
```

Receives delivery events from Mailgun (delivered, opened, clicked, bounced, complained). Signature verified via HMAC-SHA256.

**Events processed:**
- `delivered` — Email successfully delivered
- `opened` — Recipient opened the email
- `clicked` — Recipient clicked a link
- `failed` / `bounced` — Delivery failed, subscriber auto-marked as bounced
- `complained` — Spam complaint, subscriber auto-unsubscribed

### Health Check

```http
GET /health
```

Returns `{ "status": "ok" }`. No auth required.

---

## Pagination

All list endpoints use cursor-based pagination for consistent results even when data changes between requests.

```bash
# 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:**

```json
{
  "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

```bash
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

```bash
# 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

```bash
# 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

```bash
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

```bash
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:

```typescript
// 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](https://docs.teamday.ai/api/org-tokens) — Generate scoped tokens for service access
- [Authentication](https://docs.teamday.ai/api/authentication) — Overview of all auth methods
- [API Reference](https://docs.teamday.ai/api/endpoints) — Full API documentation
